diff --git a/src/input.cpp b/src/input.cpp index 9264ed9d854..e27d42187b7 100644 --- a/src/input.cpp +++ b/src/input.cpp @@ -13,6 +13,7 @@ extern "C" { #include #include #include +#include #include #include #include @@ -68,6 +69,16 @@ namespace input { constexpr auto VKEY_LMENU = 0xA4; ///< Windows virtual-key code for lmenu. constexpr auto VKEY_RMENU = 0xA5; ///< Windows virtual-key code for rmenu. + constexpr auto SS_KEY_UNDO = 0x0100; ///< Non-normalized client keycode for undo. + constexpr auto SS_KEY_CUT = 0x0101; ///< Non-normalized client keycode for cut. + constexpr auto SS_KEY_COPY = 0x0102; ///< Non-normalized client keycode for copy. + constexpr auto SS_KEY_PASTE = 0x0103; ///< Non-normalized client keycode for paste. + + constexpr auto SS_SCANCODE_UNDO = 0x7A; ///< SDL scancode delivered for undo when non-normalized. + constexpr auto SS_SCANCODE_CUT = 0x7B; ///< SDL scancode delivered for cut when non-normalized. + constexpr auto SS_SCANCODE_COPY = 0x7C; ///< SDL scancode delivered for copy when non-normalized. + constexpr auto SS_SCANCODE_PASTE = 0x7D; ///< SDL scancode delivered for paste when non-normalized. + /** * @brief Enumerates supported button state options. */ @@ -1150,12 +1161,43 @@ namespace input { } auto release = util::endian::little(packet->header.magic) == KEY_UP_EVENT_MAGIC; - auto keyCode = packet->keyCode & 0x00FF; + // Keycodes carrying the legacy 0x8000 flag are normalized Windows virtual-key + // codes for which only the low byte is significant. Non-normalized keycodes + // (such as the Undo/Cut/Copy/Paste editing keys) are interpreted as-is. + const bool normalized_prefix = (packet->keyCode & 0x8000) != 0; + auto keyCode = normalized_prefix ? (packet->keyCode & 0x00FF) : (packet->keyCode & 0xFFFF); + + int modifiers = packet->modifiers; + + // Clients deliver the Undo/Cut/Copy/Paste editing keys as non-normalized + // keycodes, either as the SDL scancode or a dedicated value. Normalize the + // scancode form to the canonical keycode that the platform backend maps to + // the native editing key. Requiring the absence of the 0x8000 prefix keeps + // the F11-F14 virtual keys (which some clients send with the non-normalized + // flag set) from being reinterpreted as editing keys. + if (!normalized_prefix && + (static_cast(packet->flags) & static_cast(SS_KBE_FLAG_NON_NORMALIZED)) != std::byte {}) { + switch (keyCode) { + case SS_SCANCODE_UNDO: + keyCode = SS_KEY_UNDO; + break; + case SS_SCANCODE_CUT: + keyCode = SS_KEY_CUT; + break; + case SS_SCANCODE_COPY: + keyCode = SS_KEY_COPY; + break; + case SS_SCANCODE_PASTE: + keyCode = SS_KEY_PASTE; + break; + default: + break; + } + } update_modifier_state(*input, keyCode, release); // Right-alt maps to meta, so it must not also register as ALT - int modifiers = packet->modifiers; if (config::input.key_rightalt_to_key_win && input->alt_keys.right_pressed && !input->alt_keys.left_pressed) { modifiers &= ~MODIFIER_ALT; } diff --git a/tests/unit/test_input.cpp b/tests/unit/test_input.cpp index e8854721882..ae7d34f8504 100644 --- a/tests/unit/test_input.cpp +++ b/tests/unit/test_input.cpp @@ -9,6 +9,7 @@ // standard includes #include #include +#include #include #include #include @@ -297,3 +298,70 @@ TEST_F(InputGamepadSessionTest, RefreshesSharedVirtualInputAfterLicenseStateChan EXPECT_NE(context().mouse->device_id(), original_mouse_id); EXPECT_EQ(runtime().active_device_count(), active_devices); } + +TEST_F(InputGamepadSessionTest, TranslatesEditingKeysToCanonicalKeycodes) { + config::input.keyboard = true; + config::input.key_repeat_delay = std::chrono::milliseconds {0}; + + auto mail = std::make_shared(); + auto session = input::alloc(mail, "editing-keys-client"); + ASSERT_NE(session, nullptr); + ASSERT_NE(context().keyboard, nullptr); + + constexpr std::uint8_t non_normalized = 0x01; // SS_KBE_FLAG_NON_NORMALIZED + + struct editing_key_case { + std::uint16_t client_key; ///< Keycode sent by the client. + std::uint16_t expected_key; ///< Canonical keycode forwarded to the backend. + }; + const std::array cases {{ + {0x7A, 0x0100}, // SDL scancode Undo + {0x7B, 0x0101}, // SDL scancode Cut + {0x7C, 0x0102}, // SDL scancode Copy + {0x7D, 0x0103}, // SDL scancode Paste + {0x0100, 0x0100}, // Dedicated Undo + {0x0101, 0x0101}, // Dedicated Cut + {0x0102, 0x0102}, // Dedicated Copy + {0x0103, 0x0103}, // Dedicated Paste + }}; + + for (const auto &editing_key : cases) { + const auto before = context().keyboard->submit_count(); + + input::testing::send_keyboard_packet(session, editing_key.client_key, 0, non_normalized, false); + EXPECT_EQ(context().keyboard->submit_count(), before + 1); + const auto press_event = context().keyboard->last_submitted_event(); + EXPECT_EQ(press_event.key_code, editing_key.expected_key); + EXPECT_TRUE(press_event.pressed); + + input::testing::send_keyboard_packet(session, editing_key.client_key, 0, non_normalized, true); + EXPECT_EQ(context().keyboard->submit_count(), before + 2); + const auto release_event = context().keyboard->last_submitted_event(); + EXPECT_EQ(release_event.key_code, editing_key.expected_key); + EXPECT_FALSE(release_event.pressed); + } + + // Without the non-normalized flag, 0x7A-0x7D are the F11-F14 virtual keys and + // must pass through untranslated. + const auto before_fn = context().keyboard->submit_count(); + input::testing::send_keyboard_packet(session, 0x7A, 0, 0, false); + EXPECT_EQ(context().keyboard->submit_count(), before_fn + 1); + const auto fn_event = context().keyboard->last_submitted_event(); + EXPECT_EQ(fn_event.key_code, 0x7A); + EXPECT_TRUE(fn_event.pressed); + input::testing::send_keyboard_packet(session, 0x7A, 0, 0, true); + + // A normalized F11-F14 keycode (0x807A-0x807D) that also carries the + // non-normalized flag must remain a function key and not be aliased to an + // editing key. Only the low byte reaches the backend after prefix stripping. + const std::array normalized_function_keys {0x807A, 0x807B, 0x807C, 0x807D}; + for (const auto normalized_key : normalized_function_keys) { + const auto before = context().keyboard->submit_count(); + input::testing::send_keyboard_packet(session, normalized_key, 0, non_normalized, false); + EXPECT_EQ(context().keyboard->submit_count(), before + 1); + const auto event = context().keyboard->last_submitted_event(); + EXPECT_EQ(event.key_code, normalized_key & 0x00FF); + EXPECT_TRUE(event.pressed); + input::testing::send_keyboard_packet(session, normalized_key, 0, non_normalized, true); + } +} diff --git a/third-party/libvirtualhid b/third-party/libvirtualhid index e4317b28c6b..b1eeb725040 160000 --- a/third-party/libvirtualhid +++ b/third-party/libvirtualhid @@ -1 +1 @@ -Subproject commit e4317b28c6bf9cbf080e78ec0e7d1e1fec4384d5 +Subproject commit b1eeb7250408c0ae749cb4abdf7219de42b10b23