From ee5ab1770bdd4d6beeae1117a763934affd13617 Mon Sep 17 00:00:00 2001 From: Chris Burel Date: Mon, 4 Oct 2021 15:54:50 -0700 Subject: [PATCH 1/5] [XCB] Respond to keyboard state changes This allows the keyboard to correctly store modifier keys, as the keyboard state changes. Signed-off-by: Chris Burel --- .../AzFramework/XcbInputDeviceKeyboard.cpp | 103 ++++++++++++++---- .../Xcb/AzFramework/XcbInputDeviceKeyboard.h | 5 + 2 files changed, 89 insertions(+), 19 deletions(-) diff --git a/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbInputDeviceKeyboard.cpp b/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbInputDeviceKeyboard.cpp index c971c3b793..d9db239522 100644 --- a/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbInputDeviceKeyboard.cpp +++ b/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbInputDeviceKeyboard.cpp @@ -20,6 +20,15 @@ namespace AzFramework { + // xcb-xkb does not provide a generic event type, so we define our own. + // These fields are enough to get to the xkbType field, which can then be + // read to typecast the event to the right concrete type. + struct XcbXkbGenericEventT + { + uint8_t response_type; + uint8_t xkbType; + }; + XcbInputDeviceKeyboard::XcbInputDeviceKeyboard(InputDeviceKeyboard& inputDevice) : InputDeviceKeyboard::Implementation(inputDevice) { @@ -39,19 +48,22 @@ namespace AzFramework return; } - XcbStdFreePtr xkbUseExtensionReply{ - xcb_xkb_use_extension_reply(connection, xcb_xkb_use_extension(connection, 1, 0), nullptr) - }; - if (!xkbUseExtensionReply) + int initializeXkbExtensionSuccess = xkb_x11_setup_xkb_extension( + connection, + 1, + 0, + XKB_X11_SETUP_XKB_EXTENSION_NO_FLAGS, + nullptr, + nullptr, + &m_xkbEventCode, + nullptr + ); + + if (!initializeXkbExtensionSuccess) { AZ_Warning("ApplicationLinux", false, "Failed to initialize the xkb extension"); return; } - if (!xkbUseExtensionReply->supported) - { - AZ_Warning("ApplicationLinux", false, "The X server does not support the xkb extension"); - return; - } m_coreDeviceId = xkb_x11_get_core_keyboard_device_id(connection); @@ -59,6 +71,43 @@ namespace AzFramework m_xkbKeymap.reset(xkb_x11_keymap_new_from_device(m_xkbContext.get(), connection, m_coreDeviceId, XKB_KEYMAP_COMPILE_NO_FLAGS)); m_xkbState.reset(xkb_x11_state_new_from_device(m_xkbKeymap.get(), connection, m_coreDeviceId)); + const uint16_t affectMap = + XCB_XKB_MAP_PART_KEY_TYPES + | XCB_XKB_MAP_PART_KEY_SYMS + | XCB_XKB_MAP_PART_MODIFIER_MAP + | XCB_XKB_MAP_PART_EXPLICIT_COMPONENTS + | XCB_XKB_MAP_PART_KEY_ACTIONS + | XCB_XKB_MAP_PART_KEY_BEHAVIORS + | XCB_XKB_MAP_PART_VIRTUAL_MODS + | XCB_XKB_MAP_PART_VIRTUAL_MOD_MAP + ; + + const uint16_t selectedEvents = + XCB_XKB_EVENT_TYPE_NEW_KEYBOARD_NOTIFY + | XCB_XKB_EVENT_TYPE_MAP_NOTIFY + | XCB_XKB_EVENT_TYPE_STATE_NOTIFY + ; + + XcbStdFreePtr error{xcb_request_check( + connection, + xcb_xkb_select_events( + connection, + /* deviceSpec = */ XCB_XKB_ID_USE_CORE_KBD, + /* affectWhich = */ selectedEvents, + /* clear = */ 0, + /* selectAll = */ selectedEvents, + /* affectMap = */ affectMap, + /* map = */ affectMap, + /* details = */ nullptr + ) + )}; + + if (error) + { + AZ_Warning("ApplicationLinux", false, "failed to select notify events from XKB"); + return; + } + m_initialized = true; } @@ -93,30 +142,38 @@ namespace AzFramework return; } - switch (event->response_type & ~0x80) + const auto responseType = event->response_type & ~0x80; + if (responseType == XCB_KEY_PRESS) { - case XCB_KEY_PRESS: - { - auto* keyPress = reinterpret_cast(event); + const auto* keyPress = reinterpret_cast(event); - const InputChannelId* key = InputChannelFromKeyEvent(keyPress->detail); - if (key) + if (const InputChannelId* key = InputChannelFromKeyEvent(keyPress->detail)) { QueueRawKeyEvent(*key, true); } - break; } - case XCB_KEY_RELEASE: + else if (responseType == XCB_KEY_RELEASE) { - auto* keyRelease = reinterpret_cast(event); + const auto* keyRelease = reinterpret_cast(event); const InputChannelId* key = InputChannelFromKeyEvent(keyRelease->detail); if (key) { QueueRawKeyEvent(*key, false); } - break; } + else if (responseType == m_xkbEventCode) + { + const auto* xkbEvent = reinterpret_cast(event); + switch (xkbEvent->xkbType) + { + case XCB_XKB_STATE_NOTIFY: + { + const auto* stateNotifyEvent = reinterpret_cast(event); + UpdateState(stateNotifyEvent); + break; + } + } } } @@ -268,4 +325,12 @@ namespace AzFramework default: return nullptr; } } + + void XcbInputDeviceKeyboard::UpdateState(const xcb_xkb_state_notify_event_t* state) + { + if (m_initialized) + { + xkb_state_update_mask(m_xkbState.get(), state->baseMods, state->latchedMods, state->lockedMods, state->baseGroup, state->latchedGroup, state->lockedGroup); + } + } } // namespace AzFramework diff --git a/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbInputDeviceKeyboard.h b/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbInputDeviceKeyboard.h index de65941a67..9adc9ad758 100644 --- a/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbInputDeviceKeyboard.h +++ b/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbInputDeviceKeyboard.h @@ -13,6 +13,8 @@ #include #include +struct xcb_xkb_state_notify_event_t; + namespace AzFramework { class XcbInputDeviceKeyboard @@ -37,10 +39,13 @@ namespace AzFramework private: [[nodiscard]] const InputChannelId* InputChannelFromKeyEvent(xcb_keycode_t code) const; + void UpdateState(const xcb_xkb_state_notify_event_t* state); + XcbUniquePtr m_xkbContext; XcbUniquePtr m_xkbKeymap; XcbUniquePtr m_xkbState; int m_coreDeviceId{-1}; + uint8_t m_xkbEventCode{0}; bool m_initialized{false}; }; } // namespace AzFramework From ba4842675065fde92054af9f17f39033d797549d Mon Sep 17 00:00:00 2001 From: Chris Burel Date: Mon, 4 Oct 2021 15:56:04 -0700 Subject: [PATCH 2/5] [XCB] Generate raw text events as well as key press events Fixes LYN-6997 Signed-off-by: Chris Burel --- .../AzFramework/XcbInputDeviceKeyboard.cpp | 28 ++++++++++++++++++- .../Xcb/AzFramework/XcbInputDeviceKeyboard.h | 3 ++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbInputDeviceKeyboard.cpp b/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbInputDeviceKeyboard.cpp index d9db239522..454313ec5b 100644 --- a/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbInputDeviceKeyboard.cpp +++ b/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbInputDeviceKeyboard.cpp @@ -119,15 +119,17 @@ namespace AzFramework bool XcbInputDeviceKeyboard::HasTextEntryStarted() const { - return false; + return m_hasTextEntryStarted; } void XcbInputDeviceKeyboard::TextEntryStart(const InputDeviceKeyboard::VirtualKeyboardOptions& options) { + m_hasTextEntryStarted = true; } void XcbInputDeviceKeyboard::TextEntryStop() { + m_hasTextEntryStarted = false; } void XcbInputDeviceKeyboard::TickInputDevice() @@ -147,6 +149,8 @@ namespace AzFramework { const auto* keyPress = reinterpret_cast(event); + QueueRawTextEvent(TextFromKeycode(m_xkbState.get(), keyPress->detail)); + if (const InputChannelId* key = InputChannelFromKeyEvent(keyPress->detail)) { QueueRawKeyEvent(*key, true); @@ -326,6 +330,28 @@ namespace AzFramework } } + AZStd::string XcbInputDeviceKeyboard::TextFromKeycode(xkb_state* state, xkb_keycode_t code) + { + // Find out how much of a buffer we need + const size_t size = xkb_state_key_get_utf8(state, code, nullptr, 0); + if (!size) + { + return {}; + } + // xkb_state_key_get_utf8 will null-terminate the resulting string, and + // will truncate the result to `size - 1` if there is not enough space + // for the null byte. The first call returns the size of the resulting + // string without including the null byte. AZStd::string internally + // includes space for the null byte, but that is not included in its + // `size()`. xkb_state_key_get_utf8 will always set `buf[size - 1] = + // 0`, so add 1 to `chars.size()` to include that internal null byte in + // the string. + AZStd::string chars; + chars.resize_no_construct(size); + xkb_state_key_get_utf8(state, code, chars.data(), chars.size() + 1); + return chars; + } + void XcbInputDeviceKeyboard::UpdateState(const xcb_xkb_state_notify_event_t* state) { if (m_initialized) diff --git a/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbInputDeviceKeyboard.h b/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbInputDeviceKeyboard.h index 9adc9ad758..00383abf6c 100644 --- a/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbInputDeviceKeyboard.h +++ b/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbInputDeviceKeyboard.h @@ -39,6 +39,8 @@ namespace AzFramework private: [[nodiscard]] const InputChannelId* InputChannelFromKeyEvent(xcb_keycode_t code) const; + static AZStd::string TextFromKeycode(xkb_state* state, xkb_keycode_t code); + void UpdateState(const xcb_xkb_state_notify_event_t* state); XcbUniquePtr m_xkbContext; @@ -47,5 +49,6 @@ namespace AzFramework int m_coreDeviceId{-1}; uint8_t m_xkbEventCode{0}; bool m_initialized{false}; + bool m_hasTextEntryStarted{false}; }; } // namespace AzFramework From b1b52c201f085afcf8c472a2ea3f2225311918e2 Mon Sep 17 00:00:00 2001 From: Chris Burel Date: Tue, 5 Oct 2021 12:12:40 -0700 Subject: [PATCH 3/5] [XCB] Add test fixtures for xcb connection and keyboard setup Signed-off-by: Chris Burel --- .../Platform/Common/Xcb/MockXcbInterface.cpp | 38 +++++- .../Platform/Common/Xcb/MockXcbInterface.h | 17 ++- .../Common/Xcb/XcbBaseTestFixture.cpp | 25 ++++ .../Platform/Common/Xcb/XcbBaseTestFixture.h | 29 +++++ .../Xcb/XcbInputDeviceKeyboardTests.cpp | 121 +++++++++++------- .../Xcb/azframework_xcb_tests_files.cmake | 2 + 6 files changed, 177 insertions(+), 55 deletions(-) create mode 100644 Code/Framework/AzFramework/Tests/Platform/Common/Xcb/XcbBaseTestFixture.cpp create mode 100644 Code/Framework/AzFramework/Tests/Platform/Common/Xcb/XcbBaseTestFixture.h diff --git a/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/MockXcbInterface.cpp b/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/MockXcbInterface.cpp index 64c3967fdc..4797f1133d 100644 --- a/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/MockXcbInterface.cpp +++ b/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/MockXcbInterface.cpp @@ -28,6 +28,10 @@ xcb_generic_event_t* xcb_poll_for_event(xcb_connection_t* c) { return MockXcbInterface::Instance()->xcb_poll_for_event(c); } +xcb_generic_error_t* xcb_request_check(xcb_connection_t* c, xcb_void_cookie_t cookie) +{ + return MockXcbInterface::Instance()->xcb_request_check(c, cookie); +} // ---------------------------------------------------------------------------- // xcb-xkb @@ -39,6 +43,10 @@ xcb_xkb_use_extension_reply_t* xcb_xkb_use_extension_reply(xcb_connection_t* c, { return MockXcbInterface::Instance()->xcb_xkb_use_extension_reply(c, cookie, e); } +xcb_void_cookie_t xcb_xkb_select_events(xcb_connection_t* c, xcb_xkb_device_spec_t deviceSpec, uint16_t affectWhich, uint16_t clear, uint16_t selectAll, uint16_t affectMap, uint16_t map, const void* details) +{ + return MockXcbInterface::Instance()->xcb_xkb_select_events(c, deviceSpec, affectWhich, clear, selectAll, affectMap, map, details); +} // ---------------------------------------------------------------------------- // xkb-x11 @@ -46,7 +54,7 @@ int32_t xkb_x11_get_core_keyboard_device_id(xcb_connection_t* connection) { return MockXcbInterface::Instance()->xkb_x11_get_core_keyboard_device_id(connection); } -struct xkb_keymap* xkb_x11_keymap_new_from_device(struct xkb_context* context, xcb_connection_t* connection, int32_t device_id, enum xkb_keymap_compile_flags flags) +xkb_keymap* xkb_x11_keymap_new_from_device(xkb_context* context, xcb_connection_t* connection, int32_t device_id, xkb_keymap_compile_flags flags) { return MockXcbInterface::Instance()->xkb_x11_keymap_new_from_device(context, connection, device_id, flags); } @@ -54,28 +62,46 @@ xkb_state* xkb_x11_state_new_from_device(xkb_keymap* keymap, xcb_connection_t* c { return MockXcbInterface::Instance()->xkb_x11_state_new_from_device(keymap, connection, device_id); } +int xkb_x11_setup_xkb_extension( + xcb_connection_t* connection, + uint16_t major_xkb_version, + uint16_t minor_xkb_version, + xkb_x11_setup_xkb_extension_flags flags, + uint16_t* major_xkb_version_out, + uint16_t* minor_xkb_version_out, + uint8_t* base_event_out, + uint8_t* base_error_out) +{ + return MockXcbInterface::Instance()->xkb_x11_setup_xkb_extension( + connection, major_xkb_version, minor_xkb_version, flags, major_xkb_version_out, minor_xkb_version_out, base_event_out, + base_error_out); +} // ---------------------------------------------------------------------------- // xkbcommon -xkb_context* xkb_context_new(enum xkb_context_flags flags) +xkb_context* xkb_context_new(xkb_context_flags flags) { return MockXcbInterface::Instance()->xkb_context_new(flags); } -void xkb_context_unref(xkb_context *context) +void xkb_context_unref(xkb_context* context) { return MockXcbInterface::Instance()->xkb_context_unref(context); } -void xkb_keymap_unref(xkb_keymap *keymap) +void xkb_keymap_unref(xkb_keymap* keymap) { return MockXcbInterface::Instance()->xkb_keymap_unref(keymap); } -void xkb_state_unref(xkb_state *state) +void xkb_state_unref(xkb_state* state) { return MockXcbInterface::Instance()->xkb_state_unref(state); } -xkb_keysym_t xkb_state_key_get_one_sym(xkb_state *state, xkb_keycode_t key) +xkb_keysym_t xkb_state_key_get_one_sym(xkb_state* state, xkb_keycode_t key) { return MockXcbInterface::Instance()->xkb_state_key_get_one_sym(state, key); } +int xkb_state_key_get_utf8(xkb_state* state, xkb_keycode_t key, char* buffer, size_t size) +{ + return MockXcbInterface::Instance()->xkb_state_key_get_utf8(state, key, buffer, size); +} } diff --git a/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/MockXcbInterface.h b/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/MockXcbInterface.h index ecda005830..4adb73caeb 100644 --- a/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/MockXcbInterface.h +++ b/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/MockXcbInterface.h @@ -17,6 +17,7 @@ #include #undef explicit #include +#include #include "Printers.h" @@ -35,6 +36,14 @@ struct xkb_keymap struct xkb_state { + enum class Modifier + { + None = 0, + Shift = 1, + Ctrl = 1 << 1, + Alt = 1 << 2, + }; + Modifier m_modifiers; }; class MockXcbInterface @@ -48,7 +57,7 @@ public: MockXcbInterface(MockXcbInterface&&) = delete; MockXcbInterface& operator=(const MockXcbInterface&) = delete; MockXcbInterface& operator=(MockXcbInterface&&) = delete; - ~MockXcbInterface() + virtual ~MockXcbInterface() { self = nullptr; } @@ -59,22 +68,26 @@ public: MOCK_CONST_METHOD2(xcb_connect, xcb_connection_t*(const char* displayname, int* screenp)); MOCK_CONST_METHOD1(xcb_disconnect, void(xcb_connection_t* c)); MOCK_CONST_METHOD1(xcb_poll_for_event, xcb_generic_event_t*(xcb_connection_t* c)); + MOCK_CONST_METHOD2(xcb_request_check, xcb_generic_error_t*(xcb_connection_t* c, xcb_void_cookie_t cookie)); // xcb-xkb MOCK_CONST_METHOD3(xcb_xkb_use_extension, xcb_xkb_use_extension_cookie_t(xcb_connection_t* c, uint16_t wantedMajor, uint16_t wantedMinor)); MOCK_CONST_METHOD3(xcb_xkb_use_extension_reply, xcb_xkb_use_extension_reply_t*(xcb_connection_t* c, xcb_xkb_use_extension_cookie_t cookie, xcb_generic_error_t** e)); + MOCK_CONST_METHOD8(xcb_xkb_select_events, xcb_void_cookie_t(xcb_connection_t* c, xcb_xkb_device_spec_t deviceSpec, uint16_t affectWhich, uint16_t clear, uint16_t selectAll, uint16_t affectMap, uint16_t map, const void* details)); // xkb-x11 MOCK_CONST_METHOD1(xkb_x11_get_core_keyboard_device_id, int32_t(xcb_connection_t* connection)); MOCK_CONST_METHOD4(xkb_x11_keymap_new_from_device, xkb_keymap*(xkb_context* context, xcb_connection_t* connection, int32_t device_id, xkb_keymap_compile_flags flags)); MOCK_CONST_METHOD3(xkb_x11_state_new_from_device, xkb_state*(xkb_keymap* keymap, xcb_connection_t* connection, int32_t device_id)); + MOCK_CONST_METHOD8(xkb_x11_setup_xkb_extension, int(xcb_connection_t* connection, uint16_t major_xkb_version, uint16_t minor_xkb_version, xkb_x11_setup_xkb_extension_flags flags, uint16_t* major_xkb_version_out, uint16_t* minor_xkb_version_out, uint8_t* base_event_out, uint8_t* base_error_out)); // xkbcommon MOCK_CONST_METHOD1(xkb_context_new, xkb_context*(xkb_context_flags flags)); MOCK_CONST_METHOD1(xkb_context_unref, void(xkb_context* context)); MOCK_CONST_METHOD1(xkb_keymap_unref, void(xkb_keymap* keymap)); MOCK_CONST_METHOD1(xkb_state_unref, void(xkb_state* state)); - MOCK_CONST_METHOD2(xkb_state_key_get_one_sym, xkb_keysym_t(xkb_state *state, xkb_keycode_t key)); + MOCK_CONST_METHOD2(xkb_state_key_get_one_sym, xkb_keysym_t(xkb_state* state, xkb_keycode_t key)); + MOCK_CONST_METHOD4(xkb_state_key_get_utf8, int(struct xkb_state* state, xkb_keycode_t key, char* buffer, size_t size)); private: static inline MockXcbInterface* self = nullptr; diff --git a/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/XcbBaseTestFixture.cpp b/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/XcbBaseTestFixture.cpp new file mode 100644 index 0000000000..937cb04e14 --- /dev/null +++ b/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/XcbBaseTestFixture.cpp @@ -0,0 +1,25 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include "XcbBaseTestFixture.h" + +namespace AzFramework +{ + void XcbBaseTestFixture::SetUp() + { + using testing::Return; + using testing::_; + + testing::Test::SetUp(); + + EXPECT_CALL(m_interface, xcb_connect(_, _)) + .WillOnce(Return(&m_connection)); + EXPECT_CALL(m_interface, xcb_disconnect(&m_connection)) + .Times(1); + } +} // namespace AzFramework diff --git a/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/XcbBaseTestFixture.h b/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/XcbBaseTestFixture.h new file mode 100644 index 0000000000..8e9b008fc1 --- /dev/null +++ b/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/XcbBaseTestFixture.h @@ -0,0 +1,29 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#pragma once + +#include +#include + +#include "MockXcbInterface.h" + +namespace AzFramework +{ + // Sets up mock behavior for the xcb library, providing an xcb_connection_t that is returned from a call to xcb_connect + class XcbBaseTestFixture + : public testing::Test + { + public: + void SetUp() override; + + protected: + testing::NiceMock m_interface; + xcb_connection_t m_connection{}; + }; +} // namespace AzFramework diff --git a/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/XcbInputDeviceKeyboardTests.cpp b/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/XcbInputDeviceKeyboardTests.cpp index 1b494c9525..9eb93fb4ae 100644 --- a/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/XcbInputDeviceKeyboardTests.cpp +++ b/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/XcbInputDeviceKeyboardTests.cpp @@ -6,6 +6,7 @@ * */ +#include #include #include @@ -15,6 +16,7 @@ #include #include "MockXcbInterface.h" #include "Actions.h" +#include "XcbBaseTestFixture.h" template xcb_generic_event_t MakeEvent(T event) @@ -24,26 +26,74 @@ xcb_generic_event_t MakeEvent(T event) namespace AzFramework { - TEST(XcbInputDeviceKeyboard, InputChannelsUpdateStateFromXcbEvents) + // Sets up default behavior for mock keyboard responses to xcb methods + class XcbInputDeviceKeyboardTests + : public XcbBaseTestFixture { - using testing::Return; + void SetUp() override + { + using testing::Return; + using testing::SetArgPointee; + using testing::_; + + XcbBaseTestFixture::SetUp(); + EXPECT_CALL(m_interface, xkb_context_new(XKB_CONTEXT_NO_FLAGS)) + .WillOnce(Return(&m_xkbContext)); + EXPECT_CALL(m_interface, xkb_context_unref(&m_xkbContext)) + .Times(1); + + EXPECT_CALL(m_interface, xkb_x11_keymap_new_from_device(&m_xkbContext, &m_connection, s_coreDeviceId, XKB_KEYMAP_COMPILE_NO_FLAGS)) + .WillOnce(Return(&m_xkbKeymap)); + EXPECT_CALL(m_interface, xkb_keymap_unref(&m_xkbKeymap)) + .Times(1); + + EXPECT_CALL(m_interface, xkb_x11_state_new_from_device(&m_xkbKeymap, &m_connection, s_coreDeviceId)) + .WillOnce(Return(&m_xkbState)); + EXPECT_CALL(m_interface, xkb_state_unref(&m_xkbState)) + .Times(1); + + ON_CALL(m_interface, xkb_x11_setup_xkb_extension(&m_connection, 1, 0, XKB_X11_SETUP_XKB_EXTENSION_NO_FLAGS, _, _, _, _)) + .WillByDefault(DoAll( + SetArgPointee<6>(s_xkbEventCode), // Set the "base_event_out" argument to the xkbEventCode, the value to identify XKB events + Return(1) + )); + + constexpr unsigned int xcbXkbSelectEventsSequence = 1; + ON_CALL(m_interface, xcb_xkb_select_events(&m_connection, _, _, _, _, _, _, _)) + .WillByDefault(Return(xcb_void_cookie_t{/*.sequence = */ xcbXkbSelectEventsSequence})); + ON_CALL(m_interface, xcb_request_check(&m_connection, testing::Field(&xcb_void_cookie_t::sequence, testing::Eq(xcbXkbSelectEventsSequence)))) + .WillByDefault(Return(nullptr)); // indicates success + + ON_CALL(m_interface, xkb_x11_get_core_keyboard_device_id(&m_connection)) + .WillByDefault(Return(s_coreDeviceId)); + + ON_CALL(m_interface, xkb_state_key_get_one_sym(&m_xkbState, s_keycodeForAKey)) + .WillByDefault(Return(XKB_KEY_a)) + ; + } + + protected: + xkb_context m_xkbContext{}; + xkb_keymap m_xkbKeymap{}; + xkb_state m_xkbState{}; + static constexpr int32_t s_coreDeviceId{1}; + static constexpr uint8_t s_xkbEventCode{85}; + static constexpr xcb_keycode_t s_keycodeForAKey{38}; + }; + + TEST_F(XcbInputDeviceKeyboardTests, InputChannelsUpdateStateFromXcbEvents) + { + using testing::DoAll; using testing::Eq; + using testing::Return; + using testing::SetArgPointee; using testing::_; - MockXcbInterface interface; - - xcb_connection_t connection{}; - xkb_context xkbContext{}; - xkb_keymap xkbKeymap{}; - xkb_state xkbState{}; - const int32_t coreDeviceId{1}; - - constexpr xcb_keycode_t keycodeForAKey = 38; const AZStd::array events { MakeEvent(xcb_key_press_event_t{ /*.response_type = */ XCB_KEY_PRESS, - /*.detail = */ keycodeForAKey, + /*.detail = */ s_keycodeForAKey, /*.sequence = */ 0, /*.time = */ 0, /*.root = */ 0, @@ -59,7 +109,7 @@ namespace AzFramework }), MakeEvent(xcb_key_release_event_t{ /*.response_type = */ XCB_KEY_RELEASE, - /*.detail = */ keycodeForAKey, + /*.detail = */ s_keycodeForAKey, /*.sequence = */ 0, /*.time = */ 0, /*.root = */ 0, @@ -75,51 +125,28 @@ namespace AzFramework }), }; - EXPECT_CALL(interface, xcb_connect(_, _)) - .WillOnce(Return(&connection)); - EXPECT_CALL(interface, xcb_disconnect(&connection)) - .Times(1); - - EXPECT_CALL(interface, xkb_context_new(XKB_CONTEXT_NO_FLAGS)) - .WillOnce(Return(&xkbContext)); - EXPECT_CALL(interface, xkb_context_unref(&xkbContext)) - .Times(1); - - EXPECT_CALL(interface, xkb_x11_keymap_new_from_device(&xkbContext, &connection, coreDeviceId, XKB_KEYMAP_COMPILE_NO_FLAGS)) - .WillOnce(Return(&xkbKeymap)); - EXPECT_CALL(interface, xkb_keymap_unref(&xkbKeymap)) - .Times(1); - - EXPECT_CALL(interface, xkb_x11_state_new_from_device(&xkbKeymap, &connection, coreDeviceId)) - .WillOnce(Return(&xkbState)); - EXPECT_CALL(interface, xkb_state_unref(&xkbState)) - .Times(1); - - EXPECT_CALL(interface, xcb_xkb_use_extension(&connection, 1, 0)); - EXPECT_CALL(interface, xcb_xkb_use_extension_reply(&connection, _, _)) - .WillOnce(ReturnMalloc( - /* .response_type =*/static_cast(XCB_XKB_USE_EXTENSION), - /* .supported =*/ static_cast(1)) - ); - EXPECT_CALL(interface, xkb_x11_get_core_keyboard_device_id(&connection)) - .WillRepeatedly(Return(coreDeviceId)); - // Set the expectations for the events that will be generated // nullptr entries represent when the event queue is empty, and will cause // PumpSystemEventLoopUntilEmpty to return // event pointers are freed by the calling code, so we malloc new copies // here - EXPECT_CALL(interface, xcb_poll_for_event(&connection)) + EXPECT_CALL(m_interface, xcb_poll_for_event(&m_connection)) .WillOnce(ReturnMalloc(events[0])) .WillOnce(Return(nullptr)) .WillOnce(ReturnMalloc(events[1])) .WillOnce(Return(nullptr)) ; - EXPECT_CALL(interface, xkb_state_key_get_one_sym(&xkbState, keycodeForAKey)) - .WillOnce(Return(XKB_KEY_a)) - .WillOnce(Return(XKB_KEY_a)) - ; + EXPECT_CALL(m_interface, xkb_state_key_get_one_sym(&m_xkbState, s_keycodeForAKey)) + .Times(2); + + EXPECT_CALL(m_interface, xkb_state_key_get_utf8(&m_xkbState, s_keycodeForAKey, nullptr, 0)) + .WillRepeatedly(Return(1)); + EXPECT_CALL(m_interface, xkb_state_key_get_utf8(&m_xkbState, s_keycodeForAKey, _, 2)) + .WillRepeatedly(DoAll( + SetArgPointee<2>('a'), + Return(1) + )); Application application; application.Start({}, {}); diff --git a/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/azframework_xcb_tests_files.cmake b/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/azframework_xcb_tests_files.cmake index aebd28222b..0ba1ece9c7 100644 --- a/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/azframework_xcb_tests_files.cmake +++ b/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/azframework_xcb_tests_files.cmake @@ -13,5 +13,7 @@ set(FILES MockXcbInterface.h Printers.cpp Printers.h + XcbBaseTestFixture.cpp + XcbBaseTestFixture.h XcbInputDeviceKeyboardTests.cpp ) From 3e0535e2110538fdf2376b1aae3edd19551daf37 Mon Sep 17 00:00:00 2001 From: Chris Burel Date: Tue, 5 Oct 2021 15:38:24 -0700 Subject: [PATCH 4/5] [XCB] Add tests to cover text events from key presses Signed-off-by: Chris Burel --- .../Tests/Platform/Common/Xcb/Matchers.h | 17 ++ .../Platform/Common/Xcb/MockXcbInterface.cpp | 12 + .../Platform/Common/Xcb/MockXcbInterface.h | 12 +- .../Xcb/XcbInputDeviceKeyboardTests.cpp | 237 +++++++++++++++++- .../Xcb/azframework_xcb_tests_files.cmake | 1 + 5 files changed, 261 insertions(+), 18 deletions(-) create mode 100644 Code/Framework/AzFramework/Tests/Platform/Common/Xcb/Matchers.h diff --git a/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/Matchers.h b/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/Matchers.h new file mode 100644 index 0000000000..77acb1c3e4 --- /dev/null +++ b/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/Matchers.h @@ -0,0 +1,17 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#pragma once + +#include +#include + +inline testing::PolymorphicMatcher> StrEq(const AZStd::string& str) +{ + return ::testing::MakePolymorphicMatcher(testing::internal::StrEqualityMatcher(str, true, true)); +} diff --git a/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/MockXcbInterface.cpp b/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/MockXcbInterface.cpp index 4797f1133d..b15809a4c6 100644 --- a/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/MockXcbInterface.cpp +++ b/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/MockXcbInterface.cpp @@ -103,5 +103,17 @@ int xkb_state_key_get_utf8(xkb_state* state, xkb_keycode_t key, char* buffer, si { return MockXcbInterface::Instance()->xkb_state_key_get_utf8(state, key, buffer, size); } +xkb_state_component xkb_state_update_mask( + xkb_state* state, + xkb_mod_mask_t depressed_mods, + xkb_mod_mask_t latched_mods, + xkb_mod_mask_t locked_mods, + xkb_layout_index_t depressed_layout, + xkb_layout_index_t latched_layout, + xkb_layout_index_t locked_layout) +{ + return MockXcbInterface::Instance()->xkb_state_update_mask( + state, depressed_mods, latched_mods, locked_mods, depressed_layout, latched_layout, locked_layout); +} } diff --git a/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/MockXcbInterface.h b/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/MockXcbInterface.h index 4adb73caeb..b57751344e 100644 --- a/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/MockXcbInterface.h +++ b/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/MockXcbInterface.h @@ -36,14 +36,7 @@ struct xkb_keymap struct xkb_state { - enum class Modifier - { - None = 0, - Shift = 1, - Ctrl = 1 << 1, - Alt = 1 << 2, - }; - Modifier m_modifiers; + xkb_mod_mask_t m_modifiers{}; }; class MockXcbInterface @@ -87,7 +80,8 @@ public: MOCK_CONST_METHOD1(xkb_keymap_unref, void(xkb_keymap* keymap)); MOCK_CONST_METHOD1(xkb_state_unref, void(xkb_state* state)); MOCK_CONST_METHOD2(xkb_state_key_get_one_sym, xkb_keysym_t(xkb_state* state, xkb_keycode_t key)); - MOCK_CONST_METHOD4(xkb_state_key_get_utf8, int(struct xkb_state* state, xkb_keycode_t key, char* buffer, size_t size)); + MOCK_CONST_METHOD4(xkb_state_key_get_utf8, int(xkb_state* state, xkb_keycode_t key, char* buffer, size_t size)); + MOCK_CONST_METHOD7(xkb_state_update_mask, xkb_state_component(xkb_state* state, xkb_mod_mask_t depressed_mods, xkb_mod_mask_t latched_mods, xkb_mod_mask_t locked_mods, xkb_layout_index_t depressed_layout, xkb_layout_index_t latched_layout, xkb_layout_index_t locked_layout)); private: static inline MockXcbInterface* self = nullptr; diff --git a/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/XcbInputDeviceKeyboardTests.cpp b/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/XcbInputDeviceKeyboardTests.cpp index 9eb93fb4ae..c01cf1e7cd 100644 --- a/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/XcbInputDeviceKeyboardTests.cpp +++ b/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/XcbInputDeviceKeyboardTests.cpp @@ -14,7 +14,9 @@ #include #include +#include #include "MockXcbInterface.h" +#include "Matchers.h" #include "Actions.h" #include "XcbBaseTestFixture.h" @@ -58,7 +60,7 @@ namespace AzFramework Return(1) )); - constexpr unsigned int xcbXkbSelectEventsSequence = 1; + constexpr unsigned int xcbXkbSelectEventsSequence = 342; ON_CALL(m_interface, xcb_xkb_select_events(&m_connection, _, _, _, _, _, _, _)) .WillByDefault(Return(xcb_void_cookie_t{/*.sequence = */ xcbXkbSelectEventsSequence})); ON_CALL(m_interface, xcb_request_check(&m_connection, testing::Field(&xcb_void_cookie_t::sequence, testing::Eq(xcbXkbSelectEventsSequence)))) @@ -70,15 +72,64 @@ namespace AzFramework ON_CALL(m_interface, xkb_state_key_get_one_sym(&m_xkbState, s_keycodeForAKey)) .WillByDefault(Return(XKB_KEY_a)) ; + ON_CALL(m_interface, xkb_state_key_get_one_sym(&m_xkbState, s_keycodeForShiftLKey)) + .WillByDefault(Return(XKB_KEY_Shift_L)) + ; + + ON_CALL(m_interface, xkb_state_update_mask(&m_xkbState, _, _, _, _, _, _)) + .WillByDefault(testing::Invoke(this, &XcbInputDeviceKeyboardTests::UpdateStateMask)); + + ON_CALL(m_interface, xkb_state_key_get_utf8(&m_xkbState, s_keycodeForAKey, nullptr, 0)) + .WillByDefault(Return(1)); + ON_CALL(m_interface, xkb_state_key_get_utf8(m_matchesStateWithoutShift, s_keycodeForAKey, _, 2)) + .WillByDefault(DoAll( + SetArgPointee<2>('a'), + Return(1) + )); + ON_CALL(m_interface, xkb_state_key_get_utf8(m_matchesStateWithShift, s_keycodeForAKey, _, 2)) + .WillByDefault(DoAll( + SetArgPointee<2>('A'), + Return(1) + )); + } + + private: + xkb_state_component UpdateStateMask( + xkb_state* state, + xkb_mod_mask_t depressed_mods, + xkb_mod_mask_t latched_mods, + xkb_mod_mask_t locked_mods, + xkb_layout_index_t depressed_layout, + xkb_layout_index_t latched_layout, + xkb_layout_index_t locked_layout) + { + state->m_modifiers = depressed_mods | locked_mods; + return {}; } protected: xkb_context m_xkbContext{}; xkb_keymap m_xkbKeymap{}; xkb_state m_xkbState{}; + const testing::Matcher m_matchesStateWithoutShift = testing::AllOf(&m_xkbState, testing::Field(&xkb_state::m_modifiers, 0)); + const testing::Matcher m_matchesStateWithShift = testing::AllOf(&m_xkbState, testing::Field(&xkb_state::m_modifiers, XCB_MOD_MASK_SHIFT)); + static constexpr int32_t s_coreDeviceId{1}; static constexpr uint8_t s_xkbEventCode{85}; + static constexpr xcb_keycode_t s_keycodeForAKey{38}; + static constexpr xcb_keycode_t s_keycodeForShiftLKey{50}; + }; + + class InputTextNotificationListener + : public InputTextNotificationBus::Handler + { + public: + InputTextNotificationListener() + { + BusConnect(); + } + MOCK_METHOD2(OnInputTextEvent, void(const AZStd::string& /*textUTF8*/, bool& /*o_hasBeenConsumed*/)); }; TEST_F(XcbInputDeviceKeyboardTests, InputChannelsUpdateStateFromXcbEvents) @@ -140,14 +191,6 @@ namespace AzFramework EXPECT_CALL(m_interface, xkb_state_key_get_one_sym(&m_xkbState, s_keycodeForAKey)) .Times(2); - EXPECT_CALL(m_interface, xkb_state_key_get_utf8(&m_xkbState, s_keycodeForAKey, nullptr, 0)) - .WillRepeatedly(Return(1)); - EXPECT_CALL(m_interface, xkb_state_key_get_utf8(&m_xkbState, s_keycodeForAKey, _, 2)) - .WillRepeatedly(DoAll( - SetArgPointee<2>('a'), - Return(1) - )); - Application application; application.Start({}, {}); @@ -169,4 +212,180 @@ namespace AzFramework application.Stop(); } + + TEST_F(XcbInputDeviceKeyboardTests, TextEnteredFromXcbKeyPressEvents) + { + using testing::DoAll; + using testing::Eq; + using testing::Return; + using testing::SetArgPointee; + using testing::_; + + // press a + // release a + // press shift + // press a + // release a + // release shift + const AZStd::array events + { + MakeEvent(xcb_key_press_event_t{ + /*.response_type = */ XCB_KEY_PRESS, + /*.detail = */ s_keycodeForAKey, + /*.sequence = */ 0, + /*.time = */ 0, + /*.root = */ 0, + /*.event = */ 0, + /*.child = */ 0, + /*.root_x = */ 0, + /*.root_y = */ 0, + /*.event_x = */ 0, + /*.event_y = */ 0, + /*.state = */ 0, + /*.same_screen = */ 0, + /*.pad0 = */ 0 + }), + MakeEvent(xcb_key_release_event_t{ + /*.response_type = */ XCB_KEY_RELEASE, + /*.detail = */ s_keycodeForAKey, + /*.sequence = */ 0, + /*.time = */ 0, + /*.root = */ 0, + /*.event = */ 0, + /*.child = */ 0, + /*.root_x = */ 0, + /*.root_y = */ 0, + /*.event_x = */ 0, + /*.event_y = */ 0, + /*.state = */ 0, + /*.same_screen = */ 0, + /*.pad0 = */ 0 + }), + MakeEvent(xcb_xkb_state_notify_event_t{ + /*.response_type = */ s_xkbEventCode, + /*.xkbType = */ XCB_XKB_STATE_NOTIFY, + /*.sequence = */ 0, + /*.time = */ 0, + /*.deviceID = */ s_coreDeviceId, + /*.mods = */ XCB_MOD_MASK_SHIFT, + /*.baseMods = */ XCB_MOD_MASK_SHIFT, + /*.latchedMods = */ 0, + /*.lockedMods = */ 0, + /*.group = */ 0, + /*.baseGroup = */ 0, + /*.latchedGroup = */ 0, + /*.lockedGroup = */ 0, + /*.compatState = */ XCB_MOD_MASK_SHIFT, + /*.grabMods = */ XCB_MOD_MASK_SHIFT, + /*.compatGrabMods = */ XCB_MOD_MASK_SHIFT, + /*.lookupMods = */ XCB_MOD_MASK_SHIFT, + /*.compatLoockupMods = */ XCB_MOD_MASK_SHIFT, + /*.ptrBtnState = */ 0, + /*.changed = */ 0, + /*.keycode = */ s_keycodeForShiftLKey, + /*.eventType = */ XCB_KEY_PRESS, + /*.requestMajor = */ 0, + /*.requestMinor = */ 0, + }), + MakeEvent(xcb_key_press_event_t{ + /*.response_type = */ XCB_KEY_PRESS, + /*.detail = */ s_keycodeForAKey, + /*.sequence = */ 0, + /*.time = */ 0, + /*.root = */ 0, + /*.event = */ 0, + /*.child = */ 0, + /*.root_x = */ 0, + /*.root_y = */ 0, + /*.event_x = */ 0, + /*.event_y = */ 0, + /*.state = */ 0, + /*.same_screen = */ 0, + /*.pad0 = */ 0 + }), + MakeEvent(xcb_key_release_event_t{ + /*.response_type = */ XCB_KEY_RELEASE, + /*.detail = */ s_keycodeForAKey, + /*.sequence = */ 0, + /*.time = */ 0, + /*.root = */ 0, + /*.event = */ 0, + /*.child = */ 0, + /*.root_x = */ 0, + /*.root_y = */ 0, + /*.event_x = */ 0, + /*.event_y = */ 0, + /*.state = */ 0, + /*.same_screen = */ 0, + /*.pad0 = */ 0 + }), + MakeEvent(xcb_xkb_state_notify_event_t{ + /*.response_type = */ s_xkbEventCode, + /*.xkbType = */ XCB_XKB_STATE_NOTIFY, + /*.sequence = */ 0, + /*.time = */ 0, + /*.deviceID = */ s_coreDeviceId, + /*.mods = */ 0, + /*.baseMods = */ 0, + /*.latchedMods = */ 0, + /*.lockedMods = */ 0, + /*.group = */ 0, + /*.baseGroup = */ 0, + /*.latchedGroup = */ 0, + /*.lockedGroup = */ 0, + /*.compatState = */ 0, + /*.grabMods = */ 0, + /*.compatGrabMods = */ 0, + /*.lookupMods = */ 0, + /*.compatLoockupMods = */ 0, + /*.ptrBtnState = */ 0, + /*.changed = */ 0, + /*.keycode = */ s_keycodeForShiftLKey, + /*.eventType = */ XCB_KEY_RELEASE, + /*.requestMajor = */ 0, + /*.requestMinor = */ 0, + }), + }; + + // Set the expectations for the events that will be generated + // nullptr entries represent when the event queue is empty, and will cause + // PumpSystemEventLoopUntilEmpty to return + // event pointers are freed by the calling code, so we malloc new copies + // here + EXPECT_CALL(m_interface, xcb_poll_for_event(&m_connection)) + .WillOnce(ReturnMalloc(events[0])) + .WillOnce(Return(nullptr)) + .WillOnce(ReturnMalloc(events[1])) + .WillOnce(Return(nullptr)) + .WillOnce(ReturnMalloc(events[2])) + .WillOnce(ReturnMalloc(events[3])) + .WillOnce(Return(nullptr)) + .WillOnce(ReturnMalloc(events[4])) + .WillOnce(ReturnMalloc(events[5])) + .WillRepeatedly(Return(nullptr)) + ; + + EXPECT_CALL(m_interface, xkb_state_key_get_utf8(&m_xkbState, s_keycodeForAKey, nullptr, 0)) + .Times(2); + EXPECT_CALL(m_interface, xkb_state_key_get_utf8(m_matchesStateWithoutShift, s_keycodeForAKey, _, 2)) + .Times(1); + EXPECT_CALL(m_interface, xkb_state_key_get_utf8(m_matchesStateWithShift, s_keycodeForAKey, _, 2)) + .Times(1); + + InputTextNotificationListener textListener; + EXPECT_CALL(textListener, OnInputTextEvent(StrEq("a"), _)).Times(1); + EXPECT_CALL(textListener, OnInputTextEvent(StrEq("A"), _)).Times(1); + + Application application; + application.Start({}, {}); + + for (int i = 0; i < 4; ++i) + { + application.PumpSystemEventLoopUntilEmpty(); + application.TickSystem(); + application.Tick(); + } + + application.Stop(); + } } // namespace AzFramework diff --git a/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/azframework_xcb_tests_files.cmake b/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/azframework_xcb_tests_files.cmake index 0ba1ece9c7..762d5d74fe 100644 --- a/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/azframework_xcb_tests_files.cmake +++ b/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/azframework_xcb_tests_files.cmake @@ -9,6 +9,7 @@ set(FILES Actions.h Main.cpp + Matchers.h MockXcbInterface.cpp MockXcbInterface.h Printers.cpp From 8668d8b2fe9c4e2f7c7c4f057b3ea4c25b58fddd Mon Sep 17 00:00:00 2001 From: Chris Burel Date: Wed, 6 Oct 2021 09:09:05 -0700 Subject: [PATCH 5/5] [XCB] Avoid emitting text events when a key press does not generate text Many keys will generate key press events but return an empty string from `xkb_state_key_get_utf8`, like modifier keys, arrow keys, function keys, etc. This checks if the string retrieved from such a key press is empty before emitting an associated text event for it, to avoid notifying a potentially large number of listeners about an empty string. Signed-off-by: Chris Burel --- .../AzFramework/XcbInputDeviceKeyboard.cpp | 9 +++- .../Xcb/XcbInputDeviceKeyboardTests.cpp | 54 ++++++++++++++++--- 2 files changed, 55 insertions(+), 8 deletions(-) diff --git a/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbInputDeviceKeyboard.cpp b/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbInputDeviceKeyboard.cpp index 454313ec5b..de326df200 100644 --- a/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbInputDeviceKeyboard.cpp +++ b/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbInputDeviceKeyboard.cpp @@ -148,8 +148,13 @@ namespace AzFramework if (responseType == XCB_KEY_PRESS) { const auto* keyPress = reinterpret_cast(event); - - QueueRawTextEvent(TextFromKeycode(m_xkbState.get(), keyPress->detail)); + { + auto text = TextFromKeycode(m_xkbState.get(), keyPress->detail); + if (!text.empty()) + { + QueueRawTextEvent(AZStd::move(text)); + } + } if (const InputChannelId* key = InputChannelFromKeyEvent(keyPress->detail)) { diff --git a/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/XcbInputDeviceKeyboardTests.cpp b/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/XcbInputDeviceKeyboardTests.cpp index c01cf1e7cd..3abbcdc564 100644 --- a/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/XcbInputDeviceKeyboardTests.cpp +++ b/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/XcbInputDeviceKeyboardTests.cpp @@ -91,6 +91,9 @@ namespace AzFramework SetArgPointee<2>('A'), Return(1) )); + + ON_CALL(m_interface, xkb_state_key_get_utf8(&m_xkbState, s_keycodeForShiftLKey, nullptr, 0)) + .WillByDefault(Return(0)); } private: @@ -261,6 +264,24 @@ namespace AzFramework /*.same_screen = */ 0, /*.pad0 = */ 0 }), + // Pressing a modifier key will generate a key press event followed + // by a state notify event + MakeEvent(xcb_key_press_event_t{ + /*.response_type = */ XCB_KEY_PRESS, + /*.detail = */ s_keycodeForShiftLKey, + /*.sequence = */ 0, + /*.time = */ 0, + /*.root = */ 0, + /*.event = */ 0, + /*.child = */ 0, + /*.root_x = */ 0, + /*.root_y = */ 0, + /*.event_x = */ 0, + /*.event_y = */ 0, + /*.state = */ 0, + /*.same_screen = */ 0, + /*.pad0 = */ 0 + }), MakeEvent(xcb_xkb_state_notify_event_t{ /*.response_type = */ s_xkbEventCode, /*.xkbType = */ XCB_XKB_STATE_NOTIFY, @@ -319,6 +340,22 @@ namespace AzFramework /*.same_screen = */ 0, /*.pad0 = */ 0 }), + MakeEvent(xcb_key_release_event_t{ + /*.response_type = */ XCB_KEY_RELEASE, + /*.detail = */ s_keycodeForShiftLKey, + /*.sequence = */ 0, + /*.time = */ 0, + /*.root = */ 0, + /*.event = */ 0, + /*.child = */ 0, + /*.root_x = */ 0, + /*.root_y = */ 0, + /*.event_x = */ 0, + /*.event_y = */ 0, + /*.state = */ 0, + /*.same_screen = */ 0, + /*.pad0 = */ 0 + }), MakeEvent(xcb_xkb_state_notify_event_t{ /*.response_type = */ s_xkbEventCode, /*.xkbType = */ XCB_XKB_STATE_NOTIFY, @@ -353,15 +390,17 @@ namespace AzFramework // event pointers are freed by the calling code, so we malloc new copies // here EXPECT_CALL(m_interface, xcb_poll_for_event(&m_connection)) - .WillOnce(ReturnMalloc(events[0])) + .WillOnce(ReturnMalloc(events[0])) // press a .WillOnce(Return(nullptr)) - .WillOnce(ReturnMalloc(events[1])) + .WillOnce(ReturnMalloc(events[1])) // release a .WillOnce(Return(nullptr)) - .WillOnce(ReturnMalloc(events[2])) - .WillOnce(ReturnMalloc(events[3])) + .WillOnce(ReturnMalloc(events[2])) // press shift + .WillOnce(ReturnMalloc(events[3])) // state notify shift is down + .WillOnce(ReturnMalloc(events[4])) // press a .WillOnce(Return(nullptr)) - .WillOnce(ReturnMalloc(events[4])) - .WillOnce(ReturnMalloc(events[5])) + .WillOnce(ReturnMalloc(events[5])) // release a + .WillOnce(ReturnMalloc(events[6])) // release shift + .WillOnce(ReturnMalloc(events[7])) // state notify shift is up .WillRepeatedly(Return(nullptr)) ; @@ -372,6 +411,9 @@ namespace AzFramework EXPECT_CALL(m_interface, xkb_state_key_get_utf8(m_matchesStateWithShift, s_keycodeForAKey, _, 2)) .Times(1); + EXPECT_CALL(m_interface, xkb_state_key_get_utf8(&m_xkbState, s_keycodeForShiftLKey, nullptr, 0)) + .Times(1); + InputTextNotificationListener textListener; EXPECT_CALL(textListener, OnInputTextEvent(StrEq("a"), _)).Times(1); EXPECT_CALL(textListener, OnInputTextEvent(StrEq("A"), _)).Times(1);