diff --git a/Code/Editor/Platform/Linux/Editor/Core/QtEditorApplication_linux.cpp b/Code/Editor/Platform/Linux/Editor/Core/QtEditorApplication_linux.cpp index ad5e57479b..8ba152a9f9 100644 --- a/Code/Editor/Platform/Linux/Editor/Core/QtEditorApplication_linux.cpp +++ b/Code/Editor/Platform/Linux/Editor/Core/QtEditorApplication_linux.cpp @@ -10,6 +10,8 @@ #ifdef PAL_TRAIT_LINUX_WINDOW_MANAGER_XCB #include +#include +#include #endif namespace Editor @@ -23,16 +25,34 @@ namespace Editor return nullptr; } + xcb_connection_t* EditorQtApplicationXcb::GetXcbConnectionFromQt() + { + QPlatformNativeInterface* native = platformNativeInterface(); + AZ_Warning("EditorQtApplicationXcb", native, "Unable to retrieve the native platform interface"); + if (!native) + { + return nullptr; + } + return reinterpret_cast(native->nativeResourceForIntegration(QByteArray("connection"))); + } + + void EditorQtApplicationXcb::OnStartPlayInEditor() + { + auto* interface = AzFramework::XcbConnectionManagerInterface::Get(); + interface->SetEnableXInput(GetXcbConnectionFromQt(), true); + } + + void EditorQtApplicationXcb::OnStopPlayInEditor() + { + auto* interface = AzFramework::XcbConnectionManagerInterface::Get(); + interface->SetEnableXInput(GetXcbConnectionFromQt(), false); + } + bool EditorQtApplicationXcb::nativeEventFilter([[maybe_unused]] const QByteArray& eventType, void* message, long*) { if (GetIEditor()->IsInGameMode()) { #ifdef PAL_TRAIT_LINUX_WINDOW_MANAGER_XCB - // We need to handle RAW Input events in a separate loop. This is a workaround to enable XInput2 RAW Inputs using Editor mode. - // TODO To have this call here might be not be perfect. - AzFramework::XcbEventHandlerBus::Broadcast(&AzFramework::XcbEventHandler::PollSpecialEvents); - - // Now handle the rest of the events. AzFramework::XcbEventHandlerBus::Broadcast( &AzFramework::XcbEventHandler::HandleXcbEvent, static_cast(message)); #endif diff --git a/Code/Editor/Platform/Linux/Editor/Core/QtEditorApplication_linux.h b/Code/Editor/Platform/Linux/Editor/Core/QtEditorApplication_linux.h index 8c145c3aa7..109ae1742b 100644 --- a/Code/Editor/Platform/Linux/Editor/Core/QtEditorApplication_linux.h +++ b/Code/Editor/Platform/Linux/Editor/Core/QtEditorApplication_linux.h @@ -6,19 +6,35 @@ * */ +#if !defined(Q_MOC_RUN) #include +#include +#endif + +using xcb_connection_t = struct xcb_connection_t; namespace Editor { - class EditorQtApplicationXcb : public EditorQtApplication + class EditorQtApplicationXcb + : public EditorQtApplication + , public AzToolsFramework::EditorEntityContextNotificationBus::Handler { Q_OBJECT public: EditorQtApplicationXcb(int& argc, char** argv) : EditorQtApplication(argc, argv) { + // Connect bus to listen for OnStart/StopPlayInEditor events + AzToolsFramework::EditorEntityContextNotificationBus::Handler::BusConnect(); } + xcb_connection_t* GetXcbConnectionFromQt(); + + /////////////////////////////////////////////////////////////////////// + // AzToolsFramework::EditorEntityContextNotificationBus overrides + void OnStartPlayInEditor() override; + void OnStopPlayInEditor() override; + // QAbstractNativeEventFilter: bool nativeEventFilter(const QByteArray& eventType, void* message, long* result) override; }; diff --git a/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbApplication.cpp b/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbApplication.cpp index f57f4a89ac..780e1e72fe 100644 --- a/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbApplication.cpp +++ b/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbApplication.cpp @@ -10,6 +10,8 @@ #include #include +#include + namespace AzFramework { //////////////////////////////////////////////////////////////////////////////////////////////// @@ -34,6 +36,31 @@ namespace AzFramework return m_xcbConnection.get(); } + void SetEnableXInput(xcb_connection_t* connection, bool enable) override + { + struct Mask + { + xcb_input_event_mask_t head; + xcb_input_xi_event_mask_t mask; + }; + const Mask mask { + /*.head=*/{ + /*.device_id=*/XCB_INPUT_DEVICE_ALL_MASTER, + /*.mask_len=*/1 + }, + /*.mask=*/ enable ? + (xcb_input_xi_event_mask_t)(XCB_INPUT_XI_EVENT_MASK_RAW_MOTION | XCB_INPUT_XI_EVENT_MASK_RAW_BUTTON_PRESS | XCB_INPUT_XI_EVENT_MASK_RAW_BUTTON_RELEASE) : + (xcb_input_xi_event_mask_t)XCB_NONE + }; + + const xcb_setup_t* xcbSetup = xcb_get_setup(connection); + const xcb_screen_t* xcbScreen = xcb_setup_roots_iterator(xcbSetup).data; + + xcb_input_xi_select_events(connection, xcbScreen->root, 1, &mask.head); + + xcb_flush(connection); + } + private: XcbUniquePtr m_xcbConnection = nullptr; }; diff --git a/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbConnectionManager.h b/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbConnectionManager.h index daa5bf35af..ca7ce06e6c 100644 --- a/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbConnectionManager.h +++ b/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbConnectionManager.h @@ -24,6 +24,9 @@ namespace AzFramework virtual ~XcbConnectionManager() = default; virtual xcb_connection_t* GetXcbConnection() const = 0; + + //! Enables/Disables XInput Raw Input events. + virtual void SetEnableXInput(xcb_connection_t* connection, bool enable) = 0; }; class XcbConnectionManagerBusTraits diff --git a/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbEventHandler.h b/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbEventHandler.h index 251342093a..f32e45ed99 100644 --- a/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbEventHandler.h +++ b/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbEventHandler.h @@ -23,9 +23,6 @@ namespace AzFramework virtual ~XcbEventHandler() = default; virtual void HandleXcbEvent(xcb_generic_event_t* event) = 0; - - // ATTN This is used as a workaround for RAW Input events when using the Editor. - virtual void PollSpecialEvents(){}; }; class XcbEventHandlerBusTraits : public AZ::EBusTraits diff --git a/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbInputDeviceMouse.cpp b/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbInputDeviceMouse.cpp index 56f21e6533..c3b7a97ccf 100644 --- a/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbInputDeviceMouse.cpp +++ b/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbInputDeviceMouse.cpp @@ -13,21 +13,68 @@ namespace AzFramework { - xcb_window_t GetSystemCursorFocusWindow() + xcb_window_t GetSystemCursorFocusWindow(xcb_connection_t* connection) { void* systemCursorFocusWindow = nullptr; AzFramework::InputSystemCursorConstraintRequestBus::BroadcastResult( systemCursorFocusWindow, &AzFramework::InputSystemCursorConstraintRequests::GetSystemCursorConstraintWindow); - if (!systemCursorFocusWindow) + if (systemCursorFocusWindow) { - return XCB_NONE; + return static_cast(reinterpret_cast(systemCursorFocusWindow)); } - // TODO Clang compile error because cast .... loses information. On GNU/Linux HWND is void* and on 64-bit - // machines its obviously 64 bit but we receive the window id from m_renderOverlay.winId() which is xcb_window_t 32-bit. + // EWMH-compliant window managers set the "_NET_ACTIVE_WINDOW" property + // of the X server's root window to the currently active window. This + // retrieves value of that property. - return static_cast(reinterpret_cast(systemCursorFocusWindow)); + // Get the atom for the _NET_ACTIVE_WINDOW property + constexpr int propertyNameLength = 18; + xcb_generic_error_t* error = nullptr; + XcbStdFreePtr activeWindowAtom {xcb_intern_atom_reply( + connection, + xcb_intern_atom(connection, /*only_if_exists=*/ 1, propertyNameLength, "_NET_ACTIVE_WINDOW"), + &error + )}; + if (!activeWindowAtom || error) + { + if (error) + { + AZ_Warning("XcbInput", false, "Retrieving _NET_ACTIVE_WINDOW atom failed : Error code %d", error->error_code); + free(error); + } + return XCB_WINDOW_NONE; + } + + // Get the root window + const xcb_window_t rootWId = xcb_setup_roots_iterator(xcb_get_setup(connection)).data->root; + + // Fetch the value of the root window's _NET_ACTIVE_WINDOW property + XcbStdFreePtr property {xcb_get_property_reply( + connection, + xcb_get_property( + /*c=*/connection, + /*_delete=*/ 0, + /*window=*/rootWId, + /*property=*/activeWindowAtom->atom, + /*type=*/XCB_ATOM_WINDOW, + /*long_offset=*/0, + /*long_length=*/1 + ), + &error + )}; + + if (!property || error) + { + if (error) + { + AZ_Warning("XcbInput", false, "Retrieving _NET_ACTIVE_WINDOW atom failed : Error code %d", error->error_code); + free(error); + } + return XCB_WINDOW_NONE; + } + + return *static_cast(xcb_get_property_value(property.get())); } xcb_connection_t* XcbInputDeviceMouse::s_xcbConnection = nullptr; @@ -39,8 +86,7 @@ namespace AzFramework : InputDeviceMouse::Implementation(inputDevice) , m_systemCursorState(SystemCursorState::Unknown) , m_systemCursorPositionNormalized(0.5f, 0.5f) - , m_prevConstraintWindow(XCB_NONE) - , m_focusWindow(XCB_NONE) + , m_focusWindow(XCB_WINDOW_NONE) , m_cursorShown(true) { XcbEventHandlerBus::Handler::BusConnect(); @@ -57,14 +103,14 @@ namespace AzFramework InputDeviceMouse::Implementation* XcbInputDeviceMouse::Create(InputDeviceMouse& inputDevice) { - auto* interface = AzFramework::XcbConnectionManagerInterface::Get(); + const auto* interface = AzFramework::XcbConnectionManagerInterface::Get(); if (!interface) { AZ_Warning("XcbInput", false, "XCB interface not available"); return nullptr; } - s_xcbConnection = AzFramework::XcbConnectionManagerInterface::Get()->GetXcbConnection(); + s_xcbConnection = interface->GetXcbConnection(); if (!s_xcbConnection) { AZ_Warning("XcbInput", false, "XCB connection not available"); @@ -126,7 +172,7 @@ namespace AzFramework // Get window information. const XcbStdFreePtr xcbGeometryReply{ xcb_get_geometry_reply( - s_xcbConnection, xcb_get_geometry(s_xcbConnection, window), NULL) }; + s_xcbConnection, xcb_get_geometry(s_xcbConnection, window), nullptr) }; if (!xcbGeometryReply) { @@ -137,7 +183,7 @@ namespace AzFramework xcb_translate_coordinates(s_xcbConnection, window, s_xcbScreen->root, 0, 0); const XcbStdFreePtr xkbTranslateCoordReply{ xcb_translate_coordinates_reply( - s_xcbConnection, translate_coord, NULL) }; + s_xcbConnection, translate_coord, nullptr) }; if (!xkbTranslateCoordReply) { @@ -173,11 +219,11 @@ namespace AzFramework for (const auto& barrier : m_activeBarriers) { xcb_void_cookie_t cookie = xcb_xfixes_create_pointer_barrier_checked( - s_xcbConnection, barrier.id, window, barrier.x0, barrier.y0, barrier.x1, barrier.y1, barrier.direction, 0, NULL); - const XcbStdFreePtr xkbError{ xcb_request_check(s_xcbConnection, cookie) }; + s_xcbConnection, barrier.id, window, barrier.x0, barrier.y0, barrier.x1, barrier.y1, barrier.direction, 0, nullptr); + const XcbStdFreePtr xcbError{ xcb_request_check(s_xcbConnection, cookie) }; AZ_Warning( - "XcbInput", !xkbError, "XFixes, failed to create barrier %d at (%d %d %d %d)", barrier.id, barrier.x0, barrier.y0, + "XcbInput", !xcbError, "XFixes, failed to create barrier %d at (%d %d %d %d)", barrier.id, barrier.x0, barrier.y0, barrier.x1, barrier.y1); } } @@ -207,7 +253,7 @@ namespace AzFramework const xcb_xfixes_query_version_cookie_t query_cookie = xcb_xfixes_query_version(s_xcbConnection, 5, 0); - xcb_generic_error_t* error = NULL; + xcb_generic_error_t* error = nullptr; const XcbStdFreePtr xkbQueryRequestReply{ xcb_xfixes_query_version_reply( s_xcbConnection, query_cookie, &error) }; @@ -244,7 +290,7 @@ namespace AzFramework const xcb_input_xi_query_version_cookie_t query_version_cookie = xcb_input_xi_query_version(s_xcbConnection, 2, 2); - xcb_generic_error_t* error = NULL; + xcb_generic_error_t* error = nullptr; const XcbStdFreePtr xkbQueryRequestReply{ xcb_input_xi_query_version_reply( s_xcbConnection, query_version_cookie, &error) }; @@ -268,40 +314,13 @@ namespace AzFramework return m_xInputInitialized; } - void XcbInputDeviceMouse::SetEnableXInput(bool enable) - { - struct - { - xcb_input_event_mask_t head; - int mask; - } mask; - - mask.head.deviceid = XCB_INPUT_DEVICE_ALL; - mask.head.mask_len = 1; - - if (enable) - { - mask.mask = XCB_INPUT_XI_EVENT_MASK_RAW_MOTION | XCB_INPUT_XI_EVENT_MASK_RAW_BUTTON_PRESS | - XCB_INPUT_XI_EVENT_MASK_RAW_BUTTON_RELEASE | XCB_INPUT_XI_EVENT_MASK_MOTION | XCB_INPUT_XI_EVENT_MASK_BUTTON_PRESS | - XCB_INPUT_XI_EVENT_MASK_BUTTON_RELEASE; - } - else - { - mask.mask = XCB_NONE; - } - - xcb_input_xi_select_events(s_xcbConnection, s_xcbScreen->root, 1, &mask.head); - - xcb_flush(s_xcbConnection); - } - void XcbInputDeviceMouse::SetSystemCursorState(SystemCursorState systemCursorState) { if (systemCursorState != m_systemCursorState) { m_systemCursorState = systemCursorState; - m_focusWindow = GetSystemCursorFocusWindow(); + m_focusWindow = GetSystemCursorFocusWindow(s_xcbConnection); HandleCursorState(m_focusWindow, systemCursorState); } @@ -309,52 +328,10 @@ namespace AzFramework void XcbInputDeviceMouse::HandleCursorState(xcb_window_t window, SystemCursorState systemCursorState) { - bool confined = false, cursorShown = true; - switch (systemCursorState) - { - case SystemCursorState::ConstrainedAndHidden: - { - //!< Constrained to the application's main window and hidden - confined = true; - cursorShown = false; - } - break; - case SystemCursorState::ConstrainedAndVisible: - { - //!< Constrained to the application's main window and visible - confined = true; - } - break; - case SystemCursorState::UnconstrainedAndHidden: - { - //!< Free to move outside the main window but hidden while inside - cursorShown = false; - } - break; - case SystemCursorState::UnconstrainedAndVisible: - { - //!< Free to move outside the application's main window and visible - } - case SystemCursorState::Unknown: - default: - break; - } - - // ATTN GetSystemCursorFocusWindow when getting out of the play in editor will return XCB_NONE - // We need however the window id to reset the cursor. - if (XCB_NONE == window && (confined || cursorShown)) - { - // Reuse the previous window to reset states. - window = m_prevConstraintWindow; - m_prevConstraintWindow = XCB_NONE; - } - else - { - // Remember the window we used to modify cursor and barrier states. - m_prevConstraintWindow = window; - } - - SetEnableXInput(!cursorShown); + const bool confined = (systemCursorState == SystemCursorState::ConstrainedAndHidden) || + (systemCursorState == SystemCursorState::ConstrainedAndVisible); + const bool cursorShown = (systemCursorState == SystemCursorState::ConstrainedAndVisible) || + (systemCursorState == SystemCursorState::UnconstrainedAndVisible); CreateBarriers(window, confined); ShowCursor(window, cursorShown); @@ -368,26 +345,26 @@ namespace AzFramework void XcbInputDeviceMouse::SetSystemCursorPositionNormalizedInternal(xcb_window_t window, AZ::Vector2 positionNormalized) { // TODO Basically not done at all. Added only the basic functions needed. - const XcbStdFreePtr xkbGeometryReply{ xcb_get_geometry_reply( - s_xcbConnection, xcb_get_geometry(s_xcbConnection, window), NULL) }; + const XcbStdFreePtr xcbGeometryReply{ xcb_get_geometry_reply( + s_xcbConnection, xcb_get_geometry(s_xcbConnection, window), nullptr) }; - if (!xkbGeometryReply) + if (!xcbGeometryReply) { return; } - const int16_t x = static_cast(positionNormalized.GetX() * xkbGeometryReply->width); - const int16_t y = static_cast(positionNormalized.GetY() * xkbGeometryReply->height); + const int16_t x = static_cast(positionNormalized.GetX() * xcbGeometryReply->width); + const int16_t y = static_cast(positionNormalized.GetY() * xcbGeometryReply->height); - xcb_warp_pointer(s_xcbConnection, XCB_NONE, window, 0, 0, 0, 0, x, y); + xcb_warp_pointer(s_xcbConnection, XCB_WINDOW_NONE, window, 0, 0, 0, 0, x, y); xcb_flush(s_xcbConnection); } void XcbInputDeviceMouse::SetSystemCursorPositionNormalized(AZ::Vector2 positionNormalized) { - const xcb_window_t window = GetSystemCursorFocusWindow(); - if (XCB_NONE == window) + const xcb_window_t window = GetSystemCursorFocusWindow(s_xcbConnection); + if (XCB_WINDOW_NONE == window) { return; } @@ -401,7 +378,7 @@ namespace AzFramework const xcb_query_pointer_cookie_t pointer = xcb_query_pointer(s_xcbConnection, window); - const XcbStdFreePtr xkbQueryPointerReply{ xcb_query_pointer_reply(s_xcbConnection, pointer, NULL) }; + const XcbStdFreePtr xkbQueryPointerReply{ xcb_query_pointer_reply(s_xcbConnection, pointer, nullptr) }; if (!xkbQueryPointerReply) { @@ -409,7 +386,7 @@ namespace AzFramework } const XcbStdFreePtr xkbGeometryReply{ xcb_get_geometry_reply( - s_xcbConnection, xcb_get_geometry(s_xcbConnection, window), NULL) }; + s_xcbConnection, xcb_get_geometry(s_xcbConnection, window), nullptr) }; if (!xkbGeometryReply) { @@ -429,8 +406,8 @@ namespace AzFramework AZ::Vector2 XcbInputDeviceMouse::GetSystemCursorPositionNormalized() const { - const xcb_window_t window = GetSystemCursorFocusWindow(); - if (XCB_NONE == window) + const xcb_window_t window = GetSystemCursorFocusWindow(s_xcbConnection); + if (XCB_WINDOW_NONE == window) { return AZ::Vector2::CreateZero(); } @@ -455,11 +432,11 @@ namespace AzFramework cookie = xcb_xfixes_hide_cursor_checked(s_xcbConnection, window); } - const XcbStdFreePtr xkbError{ xcb_request_check(s_xcbConnection, cookie) }; + const XcbStdFreePtr xcbError{ xcb_request_check(s_xcbConnection, cookie) }; - if (xkbError) + if (xcbError) { - AZ_Warning("XcbInput", false, "ShowCursor failed: %d", xkbError->error_code); + AZ_Warning("XcbInput", false, "ShowCursor failed: %d", xcbError->error_code); return; } @@ -500,14 +477,6 @@ namespace AzFramework } } - void XcbInputDeviceMouse::HandlePointerMotionEvents(const xcb_generic_event_t* event) - { - const xcb_input_motion_event_t* mouseMotionEvent = reinterpret_cast(event); - - m_systemCursorPosition[0] = mouseMotionEvent->event_x; - m_systemCursorPosition[1] = mouseMotionEvent->event_y; - } - void XcbInputDeviceMouse::HandleRawInputEvents(const xcb_ge_generic_event_t* event) { const xcb_ge_generic_event_t* genericEvent = reinterpret_cast(event); @@ -552,78 +521,20 @@ namespace AzFramework } } - void XcbInputDeviceMouse::PollSpecialEvents() - { - while (xcb_generic_event_t* genericEvent = xcb_poll_for_queued_event(s_xcbConnection)) - { - // TODO Is the following correct? If we are showing the cursor, don't poll RAW Input events. - switch (genericEvent->response_type & ~0x80) - { - case XCB_GE_GENERIC: - { - const xcb_ge_generic_event_t* geGenericEvent = reinterpret_cast(genericEvent); - - // Only handle raw inputs if we have focus. - // Handle Raw Input events first. - if ((geGenericEvent->event_type == XCB_INPUT_RAW_BUTTON_PRESS) || - (geGenericEvent->event_type == XCB_INPUT_RAW_BUTTON_RELEASE) || - (geGenericEvent->event_type == XCB_INPUT_RAW_MOTION)) - { - HandleRawInputEvents(geGenericEvent); - - free(genericEvent); - } - } - break; - } - } - } - void XcbInputDeviceMouse::HandleXcbEvent(xcb_generic_event_t* event) { switch (event->response_type & ~0x80) { - // QT5 is using by default XInput which means we do need to check for XCB_GE_GENERIC event to parse all mouse related events. + // XInput raw events are sent from the server as a XCB_GE_GENERIC + // event. A XCB_GE_GENERIC event is typecast to a + // xcb_ge_generic_event_t, which is distinct from a + // xcb_generic_event_t, and exists so that X11 extensions can extend + // the event emission beyond the size that a normal X11 event could + // contain. case XCB_GE_GENERIC: { const xcb_ge_generic_event_t* genericEvent = reinterpret_cast(event); - - // Handling RAW Inputs here works in GameMode but not in Editor mode because QT is - // not handling RAW input events and passing to. - if (!m_cursorShown) - { - // Handle Raw Input events first. - if ((genericEvent->event_type == XCB_INPUT_RAW_BUTTON_PRESS) || - (genericEvent->event_type == XCB_INPUT_RAW_BUTTON_RELEASE) || (genericEvent->event_type == XCB_INPUT_RAW_MOTION)) - { - HandleRawInputEvents(genericEvent); - } - } - else - { - switch (genericEvent->event_type) - { - case XCB_INPUT_BUTTON_PRESS: - { - const xcb_input_button_press_event_t* mouseButtonEvent = - reinterpret_cast(genericEvent); - HandleButtonPressEvents(mouseButtonEvent->detail, true); - } - break; - case XCB_INPUT_BUTTON_RELEASE: - { - const xcb_input_button_release_event_t* mouseButtonEvent = - reinterpret_cast(genericEvent); - HandleButtonPressEvents(mouseButtonEvent->detail, false); - } - break; - case XCB_INPUT_MOTION: - { - HandlePointerMotionEvents(event); - } - break; - } - } + HandleRawInputEvents(genericEvent); } break; case XCB_FOCUS_IN: @@ -634,6 +545,9 @@ namespace AzFramework m_focusWindow = focusInEvent->event; HandleCursorState(m_focusWindow, m_systemCursorState); } + + auto* interface = AzFramework::XcbConnectionManagerInterface::Get(); + interface->SetEnableXInput(interface->GetXcbConnection(), true); } break; case XCB_FOCUS_OUT: @@ -644,7 +558,10 @@ namespace AzFramework ProcessRawEventQueues(); ResetInputChannelStates(); - m_focusWindow = XCB_NONE; + m_focusWindow = XCB_WINDOW_NONE; + + auto* interface = AzFramework::XcbConnectionManagerInterface::Get(); + interface->SetEnableXInput(interface->GetXcbConnection(), false); } break; } diff --git a/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbInputDeviceMouse.h b/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbInputDeviceMouse.h index 106d204ca9..a69a8a9ec5 100644 --- a/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbInputDeviceMouse.h +++ b/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbInputDeviceMouse.h @@ -65,9 +65,6 @@ namespace AzFramework //! \ref AzFramework::InputDeviceMouse::Implementation::TickInputDevice void TickInputDevice() override; - //! This method is called by the Editor to accommodate some events with the Editor. Never called in Game mode. - void PollSpecialEvents() override; - //! Handle X11 events. void HandleXcbEvent(xcb_generic_event_t* event) override; @@ -77,9 +74,6 @@ namespace AzFramework //! Initialize XInput extension. Used for raw input during confinement and showing/hiding the cursor. static bool InitializeXInput(); - //! Enables/Disables XInput Raw Input events. - void SetEnableXInput(bool enable); - //! Create barriers. void CreateBarriers(xcb_window_t window, bool create); @@ -98,9 +92,6 @@ namespace AzFramework //! Handle button press/release events. void HandleButtonPressEvents(uint32_t detail, bool pressed); - //! Handle motion notify events. - void HandlePointerMotionEvents(const xcb_generic_event_t* event); - //! Will set cursor states and confinement modes. void HandleCursorState(xcb_window_t window, SystemCursorState systemCursorState); @@ -160,7 +151,6 @@ namespace AzFramework AZ::Vector2 m_cursorHiddenPosition; AZ::Vector2 m_systemCursorPositionNormalized; - uint32_t m_systemCursorPosition[MAX_XI_RAW_AXIS]; static xcb_connection_t* s_xcbConnection; static xcb_screen_t* s_xcbScreen; @@ -171,9 +161,6 @@ namespace AzFramework //! Will be true if the xinput2 extension could be initialized. static bool m_xInputInitialized; - //! The window that had focus - xcb_window_t m_prevConstraintWindow; - //! The current window that has focus xcb_window_t m_focusWindow; diff --git a/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/Actions.h b/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/Actions.h index 1650ff4f8d..e86904efa3 100644 --- a/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/Actions.h +++ b/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/Actions.h @@ -11,6 +11,13 @@ #include #include +ACTION_TEMPLATE(ReturnMalloc, + HAS_1_TEMPLATE_PARAMS(typename, T), + AND_0_VALUE_PARAMS()) { + T* value = static_cast(malloc(sizeof(T))); + *value = T{}; + return value; +} ACTION_TEMPLATE(ReturnMalloc, HAS_1_TEMPLATE_PARAMS(typename, T), AND_1_VALUE_PARAMS(p0)) { @@ -25,3 +32,38 @@ ACTION_TEMPLATE(ReturnMalloc, *value = T{ p0, p1 }; return value; } +ACTION_TEMPLATE(ReturnMalloc, + HAS_1_TEMPLATE_PARAMS(typename, T), + AND_3_VALUE_PARAMS(p0, p1, p2)) { + T* value = static_cast(malloc(sizeof(T))); + *value = T{ p0, p1, p2 }; + return value; +} +ACTION_TEMPLATE(ReturnMalloc, + HAS_1_TEMPLATE_PARAMS(typename, T), + AND_4_VALUE_PARAMS(p0, p1, p2, p3)) { + T* value = static_cast(malloc(sizeof(T))); + *value = T{ p0, p1, p2, p3 }; + return value; +} +ACTION_TEMPLATE(ReturnMalloc, + HAS_1_TEMPLATE_PARAMS(typename, T), + AND_5_VALUE_PARAMS(p0, p1, p2, p3, p4)) { + T* value = static_cast(malloc(sizeof(T))); + *value = T{ p0, p1, p2, p3, p4 }; + return value; +} +ACTION_TEMPLATE(ReturnMalloc, + HAS_1_TEMPLATE_PARAMS(typename, T), + AND_6_VALUE_PARAMS(p0, p1, p2, p3, p4, p5)) { + T* value = static_cast(malloc(sizeof(T))); + *value = T{ p0, p1, p2, p3, p4, p5 }; + return value; +} +ACTION_TEMPLATE(ReturnMalloc, + HAS_1_TEMPLATE_PARAMS(typename, T), + AND_7_VALUE_PARAMS(p0, p1, p2, p3, p4, p5, p6)) { + T* value = static_cast(malloc(sizeof(T))); + *value = T{ p0, p1, p2, p3, p4, p5, p6 }; + return value; +} diff --git a/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/MockXcbInterface.cpp b/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/MockXcbInterface.cpp index b15809a4c6..a19642e388 100644 --- a/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/MockXcbInterface.cpp +++ b/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/MockXcbInterface.cpp @@ -32,6 +32,82 @@ xcb_generic_error_t* xcb_request_check(xcb_connection_t* c, xcb_void_cookie_t co { return MockXcbInterface::Instance()->xcb_request_check(c, cookie); } +const xcb_setup_t* xcb_get_setup(xcb_connection_t *c) +{ + return MockXcbInterface::Instance()->xcb_get_setup(c); +} +xcb_screen_iterator_t xcb_setup_roots_iterator(const xcb_setup_t* R) +{ + return MockXcbInterface::Instance()->xcb_setup_roots_iterator(R); +} +const xcb_query_extension_reply_t* xcb_get_extension_data(xcb_connection_t* c, xcb_extension_t* ext) +{ + return MockXcbInterface::Instance()->xcb_get_extension_data(c, ext); +} +int xcb_flush(xcb_connection_t *c) +{ + return MockXcbInterface::Instance()->xcb_flush(c); +} +xcb_query_pointer_cookie_t xcb_query_pointer(xcb_connection_t* c, xcb_window_t window) +{ + return MockXcbInterface::Instance()->xcb_query_pointer(c, window); +} +xcb_query_pointer_reply_t* xcb_query_pointer_reply(xcb_connection_t* c, xcb_query_pointer_cookie_t cookie, xcb_generic_error_t** e) +{ + return MockXcbInterface::Instance()->xcb_query_pointer_reply(c, cookie, e); +} +xcb_get_geometry_cookie_t xcb_get_geometry(xcb_connection_t* c, xcb_drawable_t drawable) +{ + return MockXcbInterface::Instance()->xcb_get_geometry(c, drawable); +} +xcb_get_geometry_reply_t* xcb_get_geometry_reply(xcb_connection_t* c, xcb_get_geometry_cookie_t cookie, xcb_generic_error_t** e) +{ + return MockXcbInterface::Instance()->xcb_get_geometry_reply(c, cookie, e); +} +xcb_void_cookie_t xcb_warp_pointer( + xcb_connection_t* c, + xcb_window_t src_window, + xcb_window_t dst_window, + int16_t src_x, + int16_t src_y, + uint16_t src_width, + uint16_t src_height, + int16_t dst_x, + int16_t dst_y) +{ + return MockXcbInterface::Instance()->xcb_warp_pointer(c, src_window, dst_window, src_x, src_y, src_width, src_height, dst_x, dst_y); +} +xcb_intern_atom_cookie_t xcb_intern_atom(xcb_connection_t* c, uint8_t only_if_exists, uint16_t name_len, const char* name) +{ + return MockXcbInterface::Instance()->xcb_intern_atom(c, only_if_exists, name_len, name); +} +xcb_intern_atom_reply_t* xcb_intern_atom_reply(xcb_connection_t* c, xcb_intern_atom_cookie_t cookie, xcb_generic_error_t** e) +{ + return MockXcbInterface::Instance()->xcb_intern_atom_reply(c, cookie, e); +} +xcb_get_property_cookie_t xcb_get_property( + xcb_connection_t* c, + uint8_t _delete, + xcb_window_t window, + xcb_atom_t property, + xcb_atom_t type, + uint32_t long_offset, + uint32_t long_length) +{ + return MockXcbInterface::Instance()->xcb_get_property(c, _delete, window, property, type, long_offset, long_length); +} +xcb_get_property_reply_t* xcb_get_property_reply(xcb_connection_t* c, xcb_get_property_cookie_t cookie, xcb_generic_error_t** e) +{ + return MockXcbInterface::Instance()->xcb_get_property_reply(c, cookie, e); +} +void* xcb_get_property_value(const xcb_get_property_reply_t* R) +{ + return MockXcbInterface::Instance()->xcb_get_property_value(R); +} +uint32_t xcb_generate_id(xcb_connection_t *c) +{ + return MockXcbInterface::Instance()->xcb_generate_id(c); +} // ---------------------------------------------------------------------------- // xcb-xkb @@ -116,4 +192,76 @@ xkb_state_component xkb_state_update_mask( state, depressed_mods, latched_mods, locked_mods, depressed_layout, latched_layout, locked_layout); } +// ---------------------------------------------------------------------------- +// xcb-xfixes +xcb_xfixes_query_version_cookie_t xcb_xfixes_query_version( + xcb_connection_t* c, uint32_t client_major_version, uint32_t client_minor_version) +{ + return MockXcbInterface::Instance()->xcb_xfixes_query_version(c, client_major_version, client_minor_version); +} +xcb_xfixes_query_version_reply_t* xcb_xfixes_query_version_reply( + xcb_connection_t* c, xcb_xfixes_query_version_cookie_t cookie, xcb_generic_error_t** e) +{ + return MockXcbInterface::Instance()->xcb_xfixes_query_version_reply(c, cookie, e); +} +xcb_void_cookie_t xcb_xfixes_show_cursor_checked(xcb_connection_t* c, xcb_window_t window) +{ + return MockXcbInterface::Instance()->xcb_xfixes_show_cursor_checked(c, window); +} +xcb_void_cookie_t xcb_xfixes_hide_cursor_checked(xcb_connection_t* c, xcb_window_t window) +{ + return MockXcbInterface::Instance()->xcb_xfixes_hide_cursor_checked(c, window); +} +xcb_void_cookie_t xcb_xfixes_delete_pointer_barrier_checked(xcb_connection_t* c, xcb_xfixes_barrier_t barrier) +{ + return MockXcbInterface::Instance()->xcb_xfixes_delete_pointer_barrier_checked(c, barrier); +} +xcb_translate_coordinates_cookie_t xcb_translate_coordinates(xcb_connection_t* c, xcb_window_t src_window, xcb_window_t dst_window, int16_t src_x, int16_t src_y) +{ + return MockXcbInterface::Instance()->xcb_translate_coordinates(c, src_window, dst_window, src_x, src_y); +} +xcb_translate_coordinates_reply_t* xcb_translate_coordinates_reply(xcb_connection_t* c, xcb_translate_coordinates_cookie_t cookie, xcb_generic_error_t** e) +{ + return MockXcbInterface::Instance()->xcb_translate_coordinates_reply(c, cookie, e); +} +xcb_void_cookie_t xcb_xfixes_create_pointer_barrier_checked( + xcb_connection_t* c, + xcb_xfixes_barrier_t barrier, + xcb_window_t window, + uint16_t x1, + uint16_t y1, + uint16_t x2, + uint16_t y2, + uint32_t directions, + uint16_t num_devices, + const uint16_t* devices) +{ + return MockXcbInterface::Instance()->xcb_xfixes_create_pointer_barrier_checked(c, barrier, window, x1, y1, x2, y2, directions, num_devices, devices); +} + +// ---------------------------------------------------------------------------- +// xcb-xinput +xcb_input_xi_query_version_cookie_t xcb_input_xi_query_version(xcb_connection_t* c, uint16_t major_version, uint16_t minor_version) +{ + return MockXcbInterface::Instance()->xcb_input_xi_query_version(c, major_version, minor_version); +} +xcb_input_xi_query_version_reply_t* xcb_input_xi_query_version_reply( + xcb_connection_t* c, xcb_input_xi_query_version_cookie_t cookie, xcb_generic_error_t** e) +{ + return MockXcbInterface::Instance()->xcb_input_xi_query_version_reply(c, cookie, e); +} +xcb_void_cookie_t xcb_input_xi_select_events( + xcb_connection_t* c, xcb_window_t window, uint16_t num_mask, const xcb_input_event_mask_t* masks) +{ + return MockXcbInterface::Instance()->xcb_input_xi_select_events(c, window, num_mask, masks); +} +int xcb_input_raw_button_press_axisvalues_length (const xcb_input_raw_button_press_event_t *R) +{ + return MockXcbInterface::Instance()->xcb_input_raw_button_press_axisvalues_length(R); +} +xcb_input_fp3232_t* xcb_input_raw_button_press_axisvalues_raw(const xcb_input_raw_button_press_event_t* R) +{ + return MockXcbInterface::Instance()->xcb_input_raw_button_press_axisvalues_raw(R); +} + } diff --git a/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/MockXcbInterface.h b/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/MockXcbInterface.h index b57751344e..c554993110 100644 --- a/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/MockXcbInterface.h +++ b/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/MockXcbInterface.h @@ -18,6 +18,8 @@ #undef explicit #include #include +#include +#include #include "Printers.h" @@ -62,6 +64,37 @@ public: 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)); + MOCK_CONST_METHOD1(xcb_get_setup, const xcb_setup_t*(xcb_connection_t *c)); + MOCK_CONST_METHOD1(xcb_setup_roots_iterator, xcb_screen_iterator_t(const xcb_setup_t* R)); + MOCK_CONST_METHOD2(xcb_get_extension_data, const xcb_query_extension_reply_t*(xcb_connection_t* c, xcb_extension_t* ext)); + MOCK_CONST_METHOD1(xcb_flush, int(xcb_connection_t *c)); + MOCK_CONST_METHOD2(xcb_query_pointer, xcb_query_pointer_cookie_t(xcb_connection_t* c, xcb_window_t window)); + MOCK_CONST_METHOD3(xcb_query_pointer_reply, xcb_query_pointer_reply_t*(xcb_connection_t* c, xcb_query_pointer_cookie_t cookie, xcb_generic_error_t** e)); + MOCK_CONST_METHOD2(xcb_get_geometry, xcb_get_geometry_cookie_t(xcb_connection_t* c, xcb_drawable_t drawable)); + MOCK_CONST_METHOD3(xcb_get_geometry_reply, xcb_get_geometry_reply_t*(xcb_connection_t* c, xcb_get_geometry_cookie_t cookie, xcb_generic_error_t** e)); + MOCK_CONST_METHOD9(xcb_warp_pointer, xcb_void_cookie_t( + xcb_connection_t* c, + xcb_window_t src_window, + xcb_window_t dst_window, + int16_t src_x, + int16_t src_y, + uint16_t src_width, + uint16_t src_height, + int16_t dst_x, + int16_t dst_y)); + MOCK_CONST_METHOD4(xcb_intern_atom, xcb_intern_atom_cookie_t(xcb_connection_t* c, uint8_t only_if_exists, uint16_t name_len, const char* name)); + MOCK_CONST_METHOD3(xcb_intern_atom_reply, xcb_intern_atom_reply_t*(xcb_connection_t* c, xcb_intern_atom_cookie_t cookie, xcb_generic_error_t** e)); + MOCK_CONST_METHOD7(xcb_get_property, xcb_get_property_cookie_t( + xcb_connection_t* c, + uint8_t _delete, + xcb_window_t window, + xcb_atom_t property, + xcb_atom_t type, + uint32_t long_offset, + uint32_t long_length)); + MOCK_CONST_METHOD3(xcb_get_property_reply, xcb_get_property_reply_t*(xcb_connection_t* c, xcb_get_property_cookie_t cookie, xcb_generic_error_t** e)); + MOCK_CONST_METHOD1(xcb_get_property_value, void*(const xcb_get_property_reply_t* R)); + MOCK_CONST_METHOD1(xcb_generate_id, uint32_t(xcb_connection_t *c)); // 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)); @@ -83,6 +116,33 @@ public: 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)); + // xcb-xfixes + MOCK_CONST_METHOD3(xcb_xfixes_query_version, xcb_xfixes_query_version_cookie_t(xcb_connection_t* c, uint32_t client_major_version, uint32_t client_minor_version)); + MOCK_CONST_METHOD3(xcb_xfixes_query_version_reply, xcb_xfixes_query_version_reply_t*(xcb_connection_t* c, xcb_xfixes_query_version_cookie_t cookie, xcb_generic_error_t** e)); + MOCK_CONST_METHOD2(xcb_xfixes_show_cursor_checked, xcb_void_cookie_t(xcb_connection_t* c, xcb_window_t window)); + MOCK_CONST_METHOD2(xcb_xfixes_hide_cursor_checked, xcb_void_cookie_t(xcb_connection_t* c, xcb_window_t window)); + MOCK_CONST_METHOD2(xcb_xfixes_delete_pointer_barrier_checked, xcb_void_cookie_t(xcb_connection_t* c, xcb_xfixes_barrier_t barrier)); + MOCK_CONST_METHOD5(xcb_translate_coordinates, xcb_translate_coordinates_cookie_t(xcb_connection_t* c, xcb_window_t src_window, xcb_window_t dst_window, int16_t src_x, int16_t src_y)); + MOCK_CONST_METHOD3(xcb_translate_coordinates_reply, xcb_translate_coordinates_reply_t*(xcb_connection_t* c, xcb_translate_coordinates_cookie_t cookie, xcb_generic_error_t** e)); + MOCK_CONST_METHOD10(xcb_xfixes_create_pointer_barrier_checked, xcb_void_cookie_t( + xcb_connection_t* c, + xcb_xfixes_barrier_t barrier, + xcb_window_t window, + uint16_t x1, + uint16_t y1, + uint16_t x2, + uint16_t y2, + uint32_t directions, + uint16_t num_devices, + const uint16_t* devices)); + + // xcb-xinput + MOCK_CONST_METHOD3(xcb_input_xi_query_version, xcb_input_xi_query_version_cookie_t(xcb_connection_t* c, uint16_t major_version, uint16_t minor_version)); + MOCK_CONST_METHOD3(xcb_input_xi_query_version_reply, xcb_input_xi_query_version_reply_t*(xcb_connection_t* c, xcb_input_xi_query_version_cookie_t cookie, xcb_generic_error_t** e)); + MOCK_CONST_METHOD4(xcb_input_xi_select_events, xcb_void_cookie_t(xcb_connection_t* c, xcb_window_t window, uint16_t num_mask, const xcb_input_event_mask_t* masks)); + MOCK_CONST_METHOD1(xcb_input_raw_button_press_axisvalues_length, int(const xcb_input_raw_button_press_event_t* R)); + MOCK_CONST_METHOD1(xcb_input_raw_button_press_axisvalues_raw, xcb_input_fp3232_t*(const xcb_input_raw_button_press_event_t* R)); + private: static inline MockXcbInterface* self = nullptr; }; diff --git a/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/XcbBaseTestFixture.h b/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/XcbBaseTestFixture.h index 8e9b008fc1..2a63a5f158 100644 --- a/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/XcbBaseTestFixture.h +++ b/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/XcbBaseTestFixture.h @@ -22,6 +22,12 @@ namespace AzFramework public: void SetUp() override; + template + static xcb_generic_event_t MakeEvent(T event) + { + return *reinterpret_cast(&event); + } + protected: testing::NiceMock m_interface; xcb_connection_t m_connection{}; diff --git a/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/XcbInputDeviceKeyboardTests.cpp b/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/XcbInputDeviceKeyboardTests.cpp index 76d00eda50..7209875235 100644 --- a/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/XcbInputDeviceKeyboardTests.cpp +++ b/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/XcbInputDeviceKeyboardTests.cpp @@ -21,12 +21,6 @@ #include "XcbBaseTestFixture.h" #include "XcbTestApplication.h" -template -xcb_generic_event_t MakeEvent(T event) -{ - return *reinterpret_cast(&event); -} - namespace AzFramework { // Sets up default behavior for mock keyboard responses to xcb methods diff --git a/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/XcbInputDeviceMouseTests.cpp b/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/XcbInputDeviceMouseTests.cpp new file mode 100644 index 0000000000..05784462d7 --- /dev/null +++ b/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/XcbInputDeviceMouseTests.cpp @@ -0,0 +1,545 @@ +/* + * 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 +#include + +#include + +#include +#include + +#include "XcbBaseTestFixture.h" +#include "XcbTestApplication.h" +#include "Matchers.h" +#include "Actions.h" + +namespace AzFramework +{ + // Sets up default behavior for mock keyboard responses to xcb methods + class XcbInputDeviceMouseTests + : public XcbBaseTestFixture + { + public: + void SetUp() override + { + using testing::Eq; + using testing::Field; + using testing::Return; + using testing::StrEq; + using testing::_; + + XcbBaseTestFixture::SetUp(); + + ON_CALL(m_interface, xcb_get_setup(&m_connection)) + .WillByDefault(Return(&s_xcbSetup)); + ON_CALL(m_interface, xcb_setup_roots_iterator(&s_xcbSetup)) + .WillByDefault(Return(xcb_screen_iterator_t{&s_xcbScreen})); + + ON_CALL(m_interface, xcb_get_extension_data(&m_connection, &xcb_xfixes_id)) + .WillByDefault(Return(&s_xfixesExtensionReply)); + ON_CALL(m_interface, xcb_xfixes_query_version_reply(&m_connection, _, _)) + .WillByDefault(ReturnMalloc( + /*response_type=*/(uint8_t)XCB_XFIXES_QUERY_VERSION, + /*pad0=*/(uint8_t)0, + /*sequence=*/(uint16_t)1, + /*length=*/0u, + /*major_version=*/5u, + /*minor_version=*/0u + )); + + ON_CALL(m_interface, xcb_get_extension_data(&m_connection, &xcb_input_id)) + .WillByDefault(Return(&s_xfixesExtensionReply)); + ON_CALL(m_interface, xcb_input_xi_query_version_reply(&m_connection, _, _)) + .WillByDefault(ReturnMalloc( + /*response_type=*/(uint8_t)XCB_INPUT_XI_QUERY_VERSION, + /*pad0=*/(uint8_t)0, + /*sequence=*/(uint16_t)1, + /*length=*/0u, + /*major_version=*/(uint16_t)2, + /*minor_version=*/(uint16_t)2 + )); + + // Set the default focus window + EXPECT_CALL(m_interface, xcb_intern_atom(&m_connection, 1, 18, StrEq("_NET_ACTIVE_WINDOW"))) + .WillRepeatedly(Return(xcb_intern_atom_cookie_t{/*.sequence=*/ 1})); + ON_CALL(m_interface, xcb_intern_atom_reply(&m_connection, Field(&xcb_intern_atom_cookie_t::sequence, Eq(1)), _)) + .WillByDefault(ReturnMalloc( + /*response_type=*/(uint8_t)XCB_INTERN_ATOM, + /*pad0=*/(uint8_t)0, + /*sequence=*/(uint16_t)1, + /*length=*/0u, + /*xcb_atom_t=*/s_netActiveWindowAtom + )); + ON_CALL(m_interface, xcb_get_property(&m_connection, 0, s_rootWindow, s_netActiveWindowAtom, XCB_ATOM_WINDOW, 0, 1)) + .WillByDefault(Return(xcb_get_property_cookie_t{/*.sequence=*/ s_getActiveWindowPropertySequence})); + ON_CALL(m_interface, xcb_get_property_reply(&m_connection, Field(&xcb_get_property_cookie_t::sequence, Eq(s_getActiveWindowPropertySequence)), _)) + .WillByDefault(ReturnMalloc( + /*response_type=*/(uint8_t)XCB_GET_PROPERTY, + /*format=*/(uint8_t)0, + /*sequence=*/(uint16_t)s_getActiveWindowPropertySequence, + /*length=*/0u, + /*type=*/XCB_ATOM_WINDOW, + /*bytes_after=*/0u, + /*value_len=*/1u + )); + ON_CALL(m_interface, xcb_get_property_value(Field(&xcb_get_property_reply_t::sequence, Eq(s_getActiveWindowPropertySequence)))) + .WillByDefault(Return(const_cast(&s_nullWindow))); + + ON_CALL(m_interface, xcb_get_geometry(&m_connection, _)) + .WillByDefault(Return(xcb_get_geometry_cookie_t{/*.sequence=*/1})); + ON_CALL(m_interface, xcb_get_geometry_reply(&m_connection, Field(&xcb_get_geometry_cookie_t::sequence, Eq(1)), _)) + .WillByDefault(ReturnMalloc(s_defaultWindowGeometry)); + } + + void PumpApplication() + { + m_application.PumpSystemEventLoopUntilEmpty(); + m_application.TickSystem(); + m_application.Tick(); + } + + protected: + static constexpr inline uint8_t s_xinputMajorOpcode = 131; + static constexpr inline xcb_window_t s_rootWindow = 1; + static constexpr inline xcb_window_t s_nullWindow = XCB_WINDOW_NONE; + static constexpr inline xcb_input_device_id_t s_virtualCorePointerId = 2; + static constexpr inline xcb_input_device_id_t s_physicalPointerDeviceId = 3; + static constexpr inline uint16_t s_screenWidthInPixels = 3840; + static constexpr inline uint16_t s_screenHeightInPixels = 2160; + static constexpr inline uint16_t s_getActiveWindowPropertySequence = 2160; + static constexpr inline xcb_atom_t s_netActiveWindowAtom = 1; + static constexpr inline xcb_setup_t s_xcbSetup{ + /*.status=*/1, + /*.pad0=*/0, + /*.protocol_major_version=*/11, + /*.protocol_minor_version=*/0, + }; + static inline xcb_screen_t s_xcbScreen{ + /*.root=*/s_rootWindow, + /*.default_colormap=*/32, + /*.white_pixel=*/16777215, + /*.black_pixel=*/0, + /*.current_input_masks=*/0, + /*.width_in_pixels=*/s_screenWidthInPixels, + /*.height_in_pixels=*/s_screenHeightInPixels, + /*.width_in_millimeters=*/602, + /*.height_in_millimeters=*/341, + }; + static constexpr inline xcb_query_extension_reply_t s_xfixesExtensionReply{ + /*.response_type=*/XCB_QUERY_EXTENSION, + /*.pad0=*/0, + /*.sequence=*/1, + /*.length=*/0, + /*.present=*/1, + }; + static constexpr inline xcb_query_extension_reply_t s_xinputExtensionReply{ + /*.response_type=*/XCB_QUERY_EXTENSION, + /*.pad0=*/0, + /*.sequence=*/1, + /*.length=*/0, + /*.present=*/1, + /*.major_opcode=*/s_xinputMajorOpcode, + }; + static constexpr inline xcb_get_geometry_reply_t s_defaultWindowGeometry{ + /*.response_type=*/XCB_GET_GEOMETRY, + /*.depth=*/0, + /*.sequence=*/1, + /*.length=*/0, + /*.root=*/s_rootWindow, + /*.x=*/100, + /*.y=*/100, + /*.width=*/100, + /*.height=*/100, + /*.border_width=*/3, + /*.pad0[2]=*/{}, + }; + XcbTestApplication m_application{ + /*enabledGamepadsCount=*/0, + /*keyboardEnabled=*/false, + /*motionEnabled=*/false, + /*mouseEnabled=*/true, + /*touchEnabled=*/false, + /*virtualKeyboardEnabled=*/false + }; + }; + + struct MouseButtonTestData + { + xcb_button_index_t m_button; + }; + + class XcbInputDeviceMouseButtonTests + : public XcbInputDeviceMouseTests + , public testing::WithParamInterface + { + public: + static InputChannelId GetInputChannelIdForButton(const xcb_button_index_t button) + { + switch (button) + { + case XCB_BUTTON_INDEX_1: + return InputDeviceMouse::Button::Left; + case XCB_BUTTON_INDEX_2: + return InputDeviceMouse::Button::Right; + case XCB_BUTTON_INDEX_3: + return InputDeviceMouse::Button::Middle; + } + return InputChannelId{}; + } + + AZStd::array GetIdleChannelIdsForButton(const xcb_button_index_t button) + { + switch (button) + { + case XCB_BUTTON_INDEX_1: + return { InputDeviceMouse::Button::Right, InputDeviceMouse::Button::Middle, InputDeviceMouse::Button::Other1, InputDeviceMouse::Button::Other2 }; + case XCB_BUTTON_INDEX_2: + return { InputDeviceMouse::Button::Left, InputDeviceMouse::Button::Middle, InputDeviceMouse::Button::Other1, InputDeviceMouse::Button::Other2 }; + case XCB_BUTTON_INDEX_3: + return { InputDeviceMouse::Button::Left, InputDeviceMouse::Button::Right, InputDeviceMouse::Button::Other1, InputDeviceMouse::Button::Other2 }; + case XCB_BUTTON_INDEX_4: + return { InputDeviceMouse::Button::Left, InputDeviceMouse::Button::Right, InputDeviceMouse::Button::Middle, InputDeviceMouse::Button::Other2 }; + case XCB_BUTTON_INDEX_5: + return { InputDeviceMouse::Button::Left, InputDeviceMouse::Button::Right, InputDeviceMouse::Button::Middle, InputDeviceMouse::Button::Other1 }; + } + return AZStd::array(); + } + }; + + TEST_P(XcbInputDeviceMouseButtonTests, ButtonInputChannelsUpdateStateFromXcbEvents) + { + using testing::Each; + using testing::Eq; + using testing::NotNull; + using testing::Property; + using testing::Return; + + // 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 these actions + // malloc new copies + // + // The xcb mouse does not react to the `XCB_BUTTON_PRESS` / + // `XCB_BUTTON_RELEASE` events, but it will still receive those events + // from the X server. + EXPECT_CALL(m_interface, xcb_poll_for_event(&m_connection)) + .WillOnce(ReturnMalloc(MakeEvent(xcb_input_raw_button_press_event_t{ + /*response_type=*/XCB_GE_GENERIC, + /*extension=*/s_xinputMajorOpcode, + /*sequence=*/4, + /*length=*/2, + /*event_type=*/XCB_INPUT_RAW_BUTTON_PRESS, + /*deviceid=*/s_virtualCorePointerId, + /*time=*/3984920, + /*detail=*/GetParam().m_button, + /*sourceid=*/s_physicalPointerDeviceId, + /*valuators_len=*/2, + /*flags=*/0, + /*pad0[4]=*/{}, + /*full_sequence=*/4 + }))) + .WillOnce(Return(nullptr)) + .WillOnce(ReturnMalloc(MakeEvent(xcb_button_press_event_t{ + /*response_type=*/XCB_BUTTON_PRESS, + /*detail=*/static_cast(GetParam().m_button), + /*sequence=*/4, + /*time=*/3984920, + /*root=*/s_rootWindow, + /*event=*/119537664, + /*child=*/0, + /*root_x=*/55, + /*root_y=*/1099, + /*event_x=*/55, + /*event_y=*/55, + /*state=*/0, + /*same_screen=*/1 + }))) + .WillOnce(Return(nullptr)) + .WillOnce(ReturnMalloc(MakeEvent(xcb_input_raw_button_release_event_t{ + /*response_type=*/XCB_GE_GENERIC, + /*extension=*/s_xinputMajorOpcode, + /*sequence=*/4, + /*length=*/2, + /*event_type=*/XCB_INPUT_RAW_BUTTON_RELEASE, + /*deviceid=*/s_virtualCorePointerId, + /*time=*/3984964, + /*detail=*/GetParam().m_button, + /*sourceid=*/s_physicalPointerDeviceId, + /*valuators_len=*/2, + /*flags=*/0, + /*pad0[4]=*/{}, + /*full_sequence=*/4 + }))) + .WillOnce(Return(nullptr)) + .WillOnce(ReturnMalloc(MakeEvent(xcb_button_release_event_t{ + /*response_type=*/XCB_BUTTON_RELEASE, + /*detail=*/static_cast(GetParam().m_button), + /*sequence=*/4, + /*time=*/3984964, + /*root=*/s_rootWindow, + /*event=*/119537664, + /*child=*/0, + /*root_x=*/55, + /*root_y=*/1099, + /*event_x=*/55, + /*event_y=*/55, + /*state=*/XCB_KEY_BUT_MASK_BUTTON_1, + /*same_screen=*/1 + }))) + .WillOnce(Return(nullptr)) + ; + + m_application.Start(); + InputSystemCursorRequestBus::Event( + InputDeviceMouse::Id, + &InputSystemCursorRequests::SetSystemCursorState, + SystemCursorState::ConstrainedAndHidden); + + const InputChannel* activeButtonChannel = InputChannelRequests::FindInputChannel(GetInputChannelIdForButton(GetParam().m_button)); + const auto inactiveButtonChannels = [this]() + { + const auto inactiveButtonChannelIds = GetIdleChannelIdsForButton(GetParam().m_button); + AZStd::array channels{}; + AZStd::transform(begin(inactiveButtonChannelIds), end(inactiveButtonChannelIds), begin(channels), [](const InputChannelId& id) + { + return InputChannelRequests::FindInputChannel(id); + }); + return channels; + }(); + + ASSERT_TRUE(activeButtonChannel); + ASSERT_THAT(inactiveButtonChannels, Each(NotNull())); + + EXPECT_THAT(activeButtonChannel->GetState(), Eq(InputChannel::State::Idle)); + EXPECT_THAT(inactiveButtonChannels, Each(Property(&InputChannel::GetState, Eq(InputChannel::State::Idle)))); + + PumpApplication(); + + EXPECT_THAT(activeButtonChannel->GetState(), Eq(InputChannel::State::Began)); + EXPECT_THAT(inactiveButtonChannels, Each(Property(&InputChannel::GetState, Eq(InputChannel::State::Idle)))); + + PumpApplication(); + + EXPECT_THAT(activeButtonChannel->GetState(), Eq(InputChannel::State::Updated)); + EXPECT_THAT(inactiveButtonChannels, Each(Property(&InputChannel::GetState, Eq(InputChannel::State::Idle)))); + + PumpApplication(); + + EXPECT_THAT(activeButtonChannel->GetState(), Eq(InputChannel::State::Ended)); + EXPECT_THAT(inactiveButtonChannels, Each(Property(&InputChannel::GetState, Eq(InputChannel::State::Idle)))); + + PumpApplication(); + + EXPECT_THAT(activeButtonChannel->GetState(), Eq(InputChannel::State::Idle)); + EXPECT_THAT(inactiveButtonChannels, Each(Property(&InputChannel::GetState, Eq(InputChannel::State::Idle)))); + } + + INSTANTIATE_TEST_CASE_P( + AllButtons, + XcbInputDeviceMouseButtonTests, + testing::Values( + MouseButtonTestData{ XCB_BUTTON_INDEX_1 }, + MouseButtonTestData{ XCB_BUTTON_INDEX_2 }, + MouseButtonTestData{ XCB_BUTTON_INDEX_3 } + // XCB_BUTTON_INDEX_4 and XCB_BUTTON_INDEX_5 map to positive and + // negative scroll wheel events, which are handled as motion events + ) + ); + + TEST_F(XcbInputDeviceMouseTests, MovementInputChannelsUpdateStateFromXcbEvents) + { + using testing::Each; + using testing::Eq; + using testing::FloatEq; + using testing::NotNull; + using testing::Property; + using testing::Return; + + // 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 these actions + // malloc new copies + // + // The xcb mouse does not react to the `XCB_MOTION_NOTIFY` event, but + // it will still receive it from the X server. + EXPECT_CALL(m_interface, xcb_poll_for_event(&m_connection)) + .WillOnce(ReturnMalloc(MakeEvent(xcb_input_raw_motion_event_t{ + /*response_type=*/XCB_GE_GENERIC, + /*extension=*/s_xinputMajorOpcode, + /*sequence=*/5, + /*length=*/10, + /*event_type=*/XCB_INPUT_RAW_MOTION, + /*deviceid=*/s_virtualCorePointerId, + /*time=*/0, // use the time value to identify each event + /*detail=*/XCB_MOTION_NORMAL, + /*sourceid=*/s_physicalPointerDeviceId, + /*valuators_len=*/2, // number of axes that have values for this event + /*flags=*/0, + /*pad0[4]=*/{}, + /*full_sequence=*/5, + }))) + .WillOnce(Return(nullptr)) + .WillOnce(ReturnMalloc(MakeEvent(xcb_motion_notify_event_t{ + /*response_type=*/XCB_MOTION_NOTIFY, + /*detail=*/XCB_MOTION_NORMAL, + /*sequence=*/5, + /*time=*/1, // use the time value to identify each event + /*root=*/s_rootWindow, + /*event=*/127926272, + /*child=*/0, + /*root_x=*/95, + /*root_y=*/1079, + /*event_x=*/95, + /*event_y=*/20, + /*state=*/0, + /*same_screen=*/1, + }))) + .WillOnce(Return(nullptr)) + ; + + AZStd::array axisValues + { + xcb_input_fp3232_t{ /*.integral=*/ 1, /*.fraction=*/0 }, // x motion + xcb_input_fp3232_t{ /*.integral=*/ 2, /*.fraction=*/0 } // y motion + }; + + EXPECT_CALL(m_interface, xcb_input_raw_button_press_axisvalues_length(testing::Field(&xcb_input_raw_button_press_event_t::time, 0))) + .WillRepeatedly(testing::Return(2)); // x and y axis + EXPECT_CALL(m_interface, xcb_input_raw_button_press_axisvalues_raw(testing::Field(&xcb_input_raw_button_press_event_t::time, 0))) + .WillRepeatedly(testing::Return(axisValues.data())); // x and y axis + + m_application.Start(); + InputSystemCursorRequestBus::Event( + InputDeviceMouse::Id, + &InputSystemCursorRequests::SetSystemCursorState, + SystemCursorState::ConstrainedAndHidden); + + const InputChannel* xMotionChannel = InputChannelRequests::FindInputChannel(InputDeviceMouse::Movement::X); + const InputChannel* yMotionChannel = InputChannelRequests::FindInputChannel(InputDeviceMouse::Movement::Y); + ASSERT_TRUE(xMotionChannel); + ASSERT_TRUE(yMotionChannel); + + EXPECT_THAT(xMotionChannel->GetState(), Eq(InputChannel::State::Idle)); + EXPECT_THAT(yMotionChannel->GetState(), Eq(InputChannel::State::Idle)); + EXPECT_THAT(xMotionChannel->GetValue(), FloatEq(0.0f)); + EXPECT_THAT(yMotionChannel->GetValue(), FloatEq(0.0f)); + + PumpApplication(); + + EXPECT_THAT(xMotionChannel->GetState(), Eq(InputChannel::State::Began)); + EXPECT_THAT(yMotionChannel->GetState(), Eq(InputChannel::State::Began)); + EXPECT_THAT(xMotionChannel->GetValue(), FloatEq(1.0f)); + EXPECT_THAT(yMotionChannel->GetValue(), FloatEq(2.0f)); + + PumpApplication(); + + EXPECT_THAT(xMotionChannel->GetState(), Eq(InputChannel::State::Ended)); + EXPECT_THAT(yMotionChannel->GetState(), Eq(InputChannel::State::Ended)); + EXPECT_THAT(xMotionChannel->GetValue(), FloatEq(0.0f)); + EXPECT_THAT(yMotionChannel->GetValue(), FloatEq(0.0f)); + } + + struct GetCursorPositionParam + { + int16_t m_x; + int16_t m_y; + }; + + class XcbGetSystemCursorPositionTests + : public XcbInputDeviceMouseTests + , public testing::WithParamInterface + { + }; + + TEST_P(XcbGetSystemCursorPositionTests, GetSystemCursorPositionNormalizedReturnsCorrectValue) + { + using testing::Eq; + using testing::Field; + using testing::Return; + using testing::_; + + xcb_window_t focusWindow = 42; + const xcb_query_pointer_reply_t queryPointerReply{ + /*.response_type=*/XCB_QUERY_POINTER, + /*.same_screen=*/1, + /*.sequence=*/0, + /*.length=*/1, + /*.root=*/s_rootWindow, + /*.child=*/focusWindow, + /*.root_x=*/static_cast(GetParam().m_x + s_defaultWindowGeometry.x), + /*.root_y=*/static_cast(GetParam().m_y + s_defaultWindowGeometry.y), + /*.win_x=*/GetParam().m_x, + /*.win_y=*/GetParam().m_y, + /*.mask=*/{}, + /*.pad0[2]=*/{}, + }; + + // Querying the root window's pointer gives its absolute value + const xcb_query_pointer_reply_t rootWindowQueryPointerReply{ + /*.response_type=*/XCB_QUERY_POINTER, + /*.same_screen=*/1, + /*.sequence=*/0, + /*.length=*/1, + /*.root=*/s_rootWindow, + /*.child=*/s_rootWindow, + /*.root_x=*/static_cast(GetParam().m_x + s_defaultWindowGeometry.x), + /*.root_y=*/static_cast(GetParam().m_y + s_defaultWindowGeometry.y), + /*.win_x=*/static_cast(GetParam().m_x + s_defaultWindowGeometry.x), + /*.win_y=*/static_cast(GetParam().m_y + s_defaultWindowGeometry.y), + /*.mask=*/{}, + /*.pad0[2]=*/{}, + }; + + EXPECT_CALL(m_interface, xcb_get_property_value(Field(&xcb_get_property_reply_t::sequence, Eq(s_getActiveWindowPropertySequence)))) + .WillRepeatedly(Return(&focusWindow)); + + EXPECT_CALL(m_interface, xcb_query_pointer(&m_connection, focusWindow)) + .WillRepeatedly(Return(xcb_query_pointer_cookie_t{/*.sequence=*/1})); + EXPECT_CALL(m_interface, xcb_query_pointer_reply(&m_connection, Field(&xcb_query_pointer_cookie_t::sequence, 1), _)) + .WillRepeatedly(ReturnMalloc(queryPointerReply)); + + EXPECT_CALL(m_interface, xcb_query_pointer(&m_connection, s_rootWindow)) + .WillRepeatedly(Return(xcb_query_pointer_cookie_t{/*.sequence=*/2})); + EXPECT_CALL(m_interface, xcb_query_pointer_reply(&m_connection, Field(&xcb_query_pointer_cookie_t::sequence, 2), _)) + .WillRepeatedly(ReturnMalloc(rootWindowQueryPointerReply)); + + m_application.Start(); + InputSystemCursorRequestBus::Event( + InputDeviceMouse::Id, + &InputSystemCursorRequests::SetSystemCursorState, + SystemCursorState::ConstrainedAndHidden); + + AZ::Vector2 systemCursorPositionNormalized = AZ::Vector2::CreateZero(); + InputSystemCursorRequestBus::EventResult( + systemCursorPositionNormalized, + InputDeviceMouse::Id, + &InputSystemCursorRequests::GetSystemCursorPositionNormalized); + + EXPECT_THAT(systemCursorPositionNormalized, ::testing::AllOf( + testing::Property(&AZ::Vector2::GetX, testing::FloatEq(static_cast(GetParam().m_x) / s_defaultWindowGeometry.width)), + testing::Property(&AZ::Vector2::GetY, testing::FloatEq(static_cast(GetParam().m_y) / s_defaultWindowGeometry.height)) + )); + } + + INSTANTIATE_TEST_CASE_P( + AllPointerPositions, + XcbGetSystemCursorPositionTests, + testing::Values( + // Default mocked window geometry sets width and height to 100, all + // parameter values should be within [0, 100) + GetCursorPositionParam{ 50, 50 }, + GetCursorPositionParam{ 25, 25 }, + GetCursorPositionParam{ 0, 100 } + ) + ); +} // 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 147fd2bfe1..7da00fa18c 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 @@ -17,5 +17,6 @@ set(FILES XcbBaseTestFixture.cpp XcbBaseTestFixture.h XcbInputDeviceKeyboardTests.cpp + XcbInputDeviceMouseTests.cpp XcbTestApplication.h )