fix(windows): preserve extended media key translation (#218)

This commit is contained in:
Dave Lane authored and GitHub committed 2026-11-03 15:59:17 -05:00
1 parent bf32e971b2
commit 9f4cac3fff
4 files changed
+191 -6

No files matched your search

+12 -1
View File
@@ -486,6 +486,17 @@ namespace lvh::detail {
pointer_info.ptPixelLocation = pointer_location(viewport, x, y);
}
/**
* @brief Identify keys requiring the E0 prefix during Win32 and HID translation.
*
* Preserve the established navigation and modifier handling, and ask Windows
* for the prefix of other keys so media, volume, and browser keys cannot be
* mistaken for ordinary keys sharing the same scan code. The E1 prefix used
* by Pause does not represent KEYEVENTF_EXTENDEDKEY.
*
* @param key_code Windows virtual-key code after any consumer remapping.
* @return Whether the key requires E0 extended-key handling.
*/
bool extended_key(KeyboardKeyCode key_code) {
switch (key_code) {
case VK_LWIN:
@@ -506,7 +517,7 @@ namespace lvh::detail {
case VK_APPS:
return true;
default:
return false;
return (::MapVirtualKeyW(key_code, MAPVK_VK_TO_VSC_EX) & 0xFF00U) == 0xE000U;
}
}
@@ -7,6 +7,7 @@
// standard includes
#include <cstddef>
#include <cstdint>
#include <span>
#include <string>
#include <vector>
@@ -207,6 +208,16 @@ namespace lvh::detail::test {
WindowsSendInputRecord keypad_enter_input;
};
/**
* @brief Captured delivery of a sequence of Windows keyboard events.
*/
struct WindowsKeyboardTranslationResult {
OperationStatus create_status; ///< Keyboard creation result.
std::vector<OperationStatus> submit_statuses; ///< Result of each submitted event.
std::vector<WindowsSendInputRecord> sent_inputs; ///< Intercepted Win32 input events.
std::vector<std::vector<std::uint8_t>> reports; ///< Intercepted HID keyboard reports.
};
struct WindowsMouseSendInputResult {
OperationStatus relative_status;
OperationStatus absolute_status;
@@ -278,6 +289,18 @@ namespace lvh::detail::test {
WindowsOverlappedIoResult windows_backend_overlapped_device_io();
WindowsBackendSendInputResult windows_backend_send_input_devices();
/**
* @brief Submit keyboard events while intercepting Win32 input and HID reports.
*
* @param events Keyboard events to translate without delivering input to the host.
* @param use_hid Whether to use the fake HID control channel instead of the Win32 fallback.
* @return Submission results and captured input.
*/
WindowsKeyboardTranslationResult windows_backend_translate_keyboard_events(
std::span<const KeyboardEvent> events,
bool use_hid
);
/**
* @brief Scale a source coordinate through a target viewport into a virtual desktop axis.
*
+26
View File
@@ -1135,6 +1135,32 @@ namespace lvh::detail {
return result;
}
WindowsKeyboardTranslationResult windows_backend_translate_keyboard_events(
std::span<const KeyboardEvent> events,
bool use_hid
) {
WindowsKeyboardTranslationResult result;
FakeSendInputState send_input_state;
ScopedFakeSendInput fake_send_input {send_input_state};
auto command_state = std::make_shared<FakeWindowsControlChannelState>();
auto backend = use_hid ?
make_fake_windows_backend(command_state, std::make_shared<FakeWindowsControlChannelState>()) :
std::make_unique<WindowsBackend>(nullptr, nullptr);
CreateKeyboardOptions options;
options.profile = profiles::keyboard();
auto created = backend->create_keyboard(1U, options);
result.create_status = created.status;
if (created) {
for (const auto &event : events) {
result.submit_statuses.push_back(created.keyboard->submit(event));
}
}
result.sent_inputs = std::move(send_input_state.sent_inputs);
result.reports = command_state->submit_reports();
return result;
}
WindowsBackendSendInputResult windows_backend_send_input_devices() {
using enum MouseEventKind;
+130 -5
View File
@@ -24,17 +24,21 @@
#endif
#endif
// local includes
// test includes
#include "fixtures/fixtures.hpp"
#include "fixtures/windows_backend_test_hooks.hpp"
#include "lvh_windows_protocol.h"
// standard includes
#include <algorithm>
#include <array>
#include <utility>
#include <vector>
// lib includes
#include <libvirtualhid/profiles.hpp>
// standard includes
#include <algorithm>
#include <vector>
// local includes
#include "lvh_windows_protocol.h"
#ifndef NOMINMAX
#define NOMINMAX
@@ -330,6 +334,127 @@ TEST_F(WindowsBackendTest, OverlappedDeviceIoUsesAnEventAndWaitsForPendingComple
EXPECT_EQ(result.completion_failure_status.code(), lvh::ErrorCode::backend_failure);
}
TEST_F(WindowsBackendTest, PreservesExtendedMediaAndBrowserKeysInSendInput) {
// These scan codes overlap letters unless KEYEVENTF_EXTENDEDKEY accompanies them.
const auto keys = std::to_array<std::pair<lvh::KeyboardKeyCode, std::uint16_t>>({
{VK_BROWSER_BACK, 0x6AU},
{VK_BROWSER_FORWARD, 0x69U},
{VK_BROWSER_REFRESH, 0x67U},
{VK_BROWSER_STOP, 0x68U},
{VK_BROWSER_SEARCH, 0x65U},
{VK_BROWSER_FAVORITES, 0x66U},
{VK_BROWSER_HOME, 0x32U},
{VK_VOLUME_MUTE, 0x20U},
{VK_VOLUME_DOWN, 0x2EU},
{VK_VOLUME_UP, 0x30U},
{VK_MEDIA_NEXT_TRACK, 0x19U},
{VK_MEDIA_PREV_TRACK, 0x10U},
{VK_MEDIA_STOP, 0x24U},
{VK_MEDIA_PLAY_PAUSE, 0x22U},
{VK_LAUNCH_MAIL, 0x6CU},
{VK_LAUNCH_MEDIA_SELECT, 0x6DU},
{VK_LAUNCH_APP1, 0x6BU},
{VK_LAUNCH_APP2, 0x21U},
});
for (const auto &[key, scan_code] : keys) {
for (const bool normalized : {false, true}) {
SCOPED_TRACE(key);
SCOPED_TRACE(normalized);
const std::array events {
lvh::KeyboardEvent {.key_code = key, .pressed = true, .uses_normalized_key_code = normalized, .prefer_native_scan_code = true},
lvh::KeyboardEvent {.key_code = key, .pressed = false, .uses_normalized_key_code = normalized, .prefer_native_scan_code = true},
};
const auto result = lvh::detail::test::windows_backend_translate_keyboard_events(events, false);
expect_ok(result.create_status);
ASSERT_EQ(result.submit_statuses.size(), 2U);
ASSERT_EQ(result.sent_inputs.size(), 2U);
for (std::size_t i = 0; i < events.size(); ++i) {
expect_ok(result.submit_statuses[i]);
const auto &input = result.sent_inputs[i];
EXPECT_EQ(input.type, INPUT_KEYBOARD);
EXPECT_EQ(input.virtual_key, 0U);
EXPECT_EQ(input.scan_code, scan_code);
EXPECT_EQ(input.key_flags, KEYEVENTF_SCANCODE | KEYEVENTF_EXTENDEDKEY | (i == 0U ? 0U : KEYEVENTF_KEYUP));
EXPECT_EQ(::MapVirtualKeyW(0xE000U | input.scan_code, MAPVK_VSC_TO_VK_EX), key);
}
}
}
}
TEST_F(WindowsBackendTest, KeepsOrdinaryKeysAndE1PauseDistinctFromE0Keys) {
for (const auto key : std::to_array<lvh::KeyboardKeyCode>({'B', 'C', VK_RETURN, VK_PAUSE, VK_SNAPSHOT, VK_NUMLOCK})) {
SCOPED_TRACE(key);
const std::array events {
lvh::KeyboardEvent {.key_code = static_cast<lvh::KeyboardKeyCode>(key), .pressed = true, .uses_normalized_key_code = true},
lvh::KeyboardEvent {.key_code = static_cast<lvh::KeyboardKeyCode>(key), .pressed = false, .prefer_native_scan_code = true},
};
const auto result = lvh::detail::test::windows_backend_translate_keyboard_events(events, false);
expect_ok(result.create_status);
ASSERT_EQ(result.submit_statuses.size(), 2U);
ASSERT_EQ(result.sent_inputs.size(), 2U);
for (std::size_t i = 0; i < events.size(); ++i) {
expect_ok(result.submit_statuses[i]);
EXPECT_EQ(result.sent_inputs[i].key_flags & KEYEVENTF_EXTENDEDKEY, 0U);
}
}
}
TEST_F(WindowsBackendTest, PreservesVolumeKeysInHidKeyboardReports) {
const auto keys = std::to_array<std::pair<lvh::KeyboardKeyCode, std::uint8_t>>({
{VK_VOLUME_MUTE, 0x7FU},
{VK_VOLUME_DOWN, 0x81U},
{VK_VOLUME_UP, 0x80U},
{'C', 0x06U},
{'B', 0x05U},
});
for (const auto &[key, usage] : keys) {
for (const bool normalized : {false, true}) {
SCOPED_TRACE(key);
SCOPED_TRACE(normalized);
const std::array events {
lvh::KeyboardEvent {.key_code = key, .pressed = true, .uses_normalized_key_code = normalized, .prefer_native_scan_code = true},
lvh::KeyboardEvent {.key_code = key, .pressed = false, .uses_normalized_key_code = normalized, .prefer_native_scan_code = true},
};
const auto result = lvh::detail::test::windows_backend_translate_keyboard_events(events, true);
expect_ok(result.create_status);
ASSERT_EQ(result.submit_statuses.size(), 2U);
for (const auto &status : result.submit_statuses) {
expect_ok(status);
}
EXPECT_TRUE(result.sent_inputs.empty());
std::vector<std::uint8_t> pressed_report(LVH_WINDOWS_KEYBOARD_INPUT_REPORT_SIZE, 0U);
pressed_report[2U] = usage;
const std::vector<std::uint8_t> released_report(LVH_WINDOWS_KEYBOARD_INPUT_REPORT_SIZE, 0U);
EXPECT_EQ(result.reports, (std::vector<std::vector<std::uint8_t>> {pressed_report, released_report}));
}
}
}
TEST_F(WindowsBackendTest, PreservesConsumerKeysWhenHidKeyboardFallsBackToSendInput) {
for (const auto key : {VK_BROWSER_HOME, VK_MEDIA_NEXT_TRACK, VK_LAUNCH_APP2}) {
for (const bool normalized : {false, true}) {
SCOPED_TRACE(key);
SCOPED_TRACE(normalized);
const std::array events {
lvh::KeyboardEvent {.key_code = static_cast<lvh::KeyboardKeyCode>(key), .pressed = true, .uses_normalized_key_code = normalized, .prefer_native_scan_code = true},
lvh::KeyboardEvent {.key_code = static_cast<lvh::KeyboardKeyCode>(key), .pressed = false, .uses_normalized_key_code = normalized, .prefer_native_scan_code = true},
};
const auto result = lvh::detail::test::windows_backend_translate_keyboard_events(events, true);
expect_ok(result.create_status);
ASSERT_EQ(result.submit_statuses.size(), 2U);
ASSERT_EQ(result.sent_inputs.size(), 2U);
EXPECT_TRUE(result.reports.empty());
for (std::size_t i = 0; i < events.size(); ++i) {
expect_ok(result.submit_statuses[i]);
const auto &input = result.sent_inputs[i];
EXPECT_EQ(input.virtual_key, 0U);
EXPECT_EQ(input.key_flags, KEYEVENTF_SCANCODE | KEYEVENTF_EXTENDEDKEY | (i == 0U ? 0U : KEYEVENTF_KEYUP));
EXPECT_EQ(::MapVirtualKeyW(0xE000U | input.scan_code, MAPVK_VSC_TO_VK_EX), static_cast<UINT>(key));
}
}
}
}
TEST_F(WindowsBackendTest, SendInputDevicesTranslateKeyboardMouseFailuresAndUnsupportedProfiles) {
const auto result = lvh::detail::test::windows_backend_send_input_devices();