Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 44 additions & 2 deletions src/input.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ extern "C" {
#include <bitset>
#include <chrono>
#include <cmath>
#include <cstddef>
#include <cstring>
#include <functional>
#include <list>
Expand Down Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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<std::byte>(packet->flags) & static_cast<std::byte>(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;
}
Expand Down
68 changes: 68 additions & 0 deletions tests/unit/test_input.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
// standard includes
#include <algorithm>
#include <array>
#include <chrono>
#include <cstdint>
#include <cstring>
#include <limits>
Expand Down Expand Up @@ -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<safe::mail_raw_t>();
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<editing_key_case, 8> 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<std::uint16_t, 4> 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);
}
}