Merge pull request #4512 from aws-lumberyard-dev/burelc/xcbInputTextEvents

Generate text input events during XCB key press handling
This commit is contained in:
Nicholas Lawson
2021-10-06 14:50:04 -07:00
committed by GitHub
9 changed files with 586 additions and 75 deletions
@@ -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<xcb_xkb_use_extension_reply_t> 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<xcb_generic_error_t> 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;
}
@@ -70,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()
@@ -93,30 +144,45 @@ 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<xcb_key_press_event_t*>(event);
const auto* keyPress = reinterpret_cast<xcb_key_press_event_t*>(event);
{
auto text = TextFromKeycode(m_xkbState.get(), keyPress->detail);
if (!text.empty())
{
QueueRawTextEvent(AZStd::move(text));
}
}
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<xcb_key_release_event_t*>(event);
const auto* keyRelease = reinterpret_cast<xcb_key_release_event_t*>(event);
const InputChannelId* key = InputChannelFromKeyEvent(keyRelease->detail);
if (key)
{
QueueRawKeyEvent(*key, false);
}
break;
}
else if (responseType == m_xkbEventCode)
{
const auto* xkbEvent = reinterpret_cast<XcbXkbGenericEventT*>(event);
switch (xkbEvent->xkbType)
{
case XCB_XKB_STATE_NOTIFY:
{
const auto* stateNotifyEvent = reinterpret_cast<xcb_xkb_state_notify_event_t*>(event);
UpdateState(stateNotifyEvent);
break;
}
}
}
}
@@ -268,4 +334,34 @@ namespace AzFramework
default: return nullptr;
}
}
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)
{
xkb_state_update_mask(m_xkbState.get(), state->baseMods, state->latchedMods, state->lockedMods, state->baseGroup, state->latchedGroup, state->lockedGroup);
}
}
} // namespace AzFramework
@@ -13,6 +13,8 @@
#include <xcb/xcb.h>
#include <xkbcommon/xkbcommon.h>
struct xcb_xkb_state_notify_event_t;
namespace AzFramework
{
class XcbInputDeviceKeyboard
@@ -37,10 +39,16 @@ 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<xkb_context, xkb_context_unref> m_xkbContext;
XcbUniquePtr<xkb_keymap, xkb_keymap_unref> m_xkbKeymap;
XcbUniquePtr<xkb_state, xkb_state_unref> m_xkbState;
int m_coreDeviceId{-1};
uint8_t m_xkbEventCode{0};
bool m_initialized{false};
bool m_hasTextEntryStarted{false};
};
} // namespace AzFramework
@@ -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 <gmock/gmock.h>
#include <AzCore/std/string/string.h>
inline testing::PolymorphicMatcher<testing::internal::StrEqualityMatcher<AZStd::string>> StrEq(const AZStd::string& str)
{
return ::testing::MakePolymorphicMatcher(testing::internal::StrEqualityMatcher<AZStd::string>(str, true, true));
}
@@ -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,58 @@ 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);
}
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);
}
}
@@ -17,6 +17,7 @@
#include <xcb/xkb.h>
#undef explicit
#include <xkbcommon/xkbcommon.h>
#include <xkbcommon/xkbcommon-x11.h>
#include "Printers.h"
@@ -35,6 +36,7 @@ struct xkb_keymap
struct xkb_state
{
xkb_mod_mask_t m_modifiers{};
};
class MockXcbInterface
@@ -48,7 +50,7 @@ public:
MockXcbInterface(MockXcbInterface&&) = delete;
MockXcbInterface& operator=(const MockXcbInterface&) = delete;
MockXcbInterface& operator=(MockXcbInterface&&) = delete;
~MockXcbInterface()
virtual ~MockXcbInterface()
{
self = nullptr;
}
@@ -59,22 +61,27 @@ 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(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;
@@ -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
@@ -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 <gtest/gtest.h>
#include <xcb/xcb.h>
#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<MockXcbInterface> m_interface;
xcb_connection_t m_connection{};
};
} // namespace AzFramework
@@ -6,6 +6,7 @@
*
*/
#include <gmock/gmock-actions.h>
#include <gtest/gtest.h>
#include <gmock/gmock.h>
@@ -13,8 +14,11 @@
#include <AzFramework/XcbApplication.h>
#include <AzFramework/XcbInputDeviceKeyboard.h>
#include <AzFramework/Input/Buses/Notifications/InputTextNotificationBus.h>
#include "MockXcbInterface.h"
#include "Matchers.h"
#include "Actions.h"
#include "XcbBaseTestFixture.h"
template<typename T>
xcb_generic_event_t MakeEvent(T event)
@@ -24,26 +28,126 @@ 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 = 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))))
.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))
;
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)
));
ON_CALL(m_interface, xkb_state_key_get_utf8(&m_xkbState, s_keycodeForShiftLKey, nullptr, 0))
.WillByDefault(Return(0));
}
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<xkb_state*> m_matchesStateWithoutShift = testing::AllOf(&m_xkbState, testing::Field(&xkb_state::m_modifiers, 0));
const testing::Matcher<xkb_state*> 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)
{
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 +163,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 +179,20 @@ 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<xcb_xkb_use_extension_reply_t>(
/* .response_type =*/static_cast<uint8_t>(XCB_XKB_USE_EXTENSION),
/* .supported =*/ static_cast<uint8_t>(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<xcb_generic_event_t>(events[0]))
.WillOnce(Return(nullptr))
.WillOnce(ReturnMalloc<xcb_generic_event_t>(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);
Application application;
application.Start({}, {});
@@ -142,4 +215,219 @@ 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
}),
// 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,
/*.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_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,
/*.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<xcb_generic_event_t>(events[0])) // press a
.WillOnce(Return(nullptr))
.WillOnce(ReturnMalloc<xcb_generic_event_t>(events[1])) // release a
.WillOnce(Return(nullptr))
.WillOnce(ReturnMalloc<xcb_generic_event_t>(events[2])) // press shift
.WillOnce(ReturnMalloc<xcb_generic_event_t>(events[3])) // state notify shift is down
.WillOnce(ReturnMalloc<xcb_generic_event_t>(events[4])) // press a
.WillOnce(Return(nullptr))
.WillOnce(ReturnMalloc<xcb_generic_event_t>(events[5])) // release a
.WillOnce(ReturnMalloc<xcb_generic_event_t>(events[6])) // release shift
.WillOnce(ReturnMalloc<xcb_generic_event_t>(events[7])) // state notify shift is up
.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);
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);
Application application;
application.Start({}, {});
for (int i = 0; i < 4; ++i)
{
application.PumpSystemEventLoopUntilEmpty();
application.TickSystem();
application.Tick();
}
application.Stop();
}
} // namespace AzFramework
@@ -9,9 +9,12 @@
set(FILES
Actions.h
Main.cpp
Matchers.h
MockXcbInterface.cpp
MockXcbInterface.h
Printers.cpp
Printers.h
XcbBaseTestFixture.cpp
XcbBaseTestFixture.h
XcbInputDeviceKeyboardTests.cpp
)