diff --git a/Code/Editor/Core/QtEditorApplication_linux.cpp b/Code/Editor/Core/QtEditorApplication_linux.cpp index 2fd4ef629e..5491134170 100644 --- a/Code/Editor/Core/QtEditorApplication_linux.cpp +++ b/Code/Editor/Core/QtEditorApplication_linux.cpp @@ -19,10 +19,16 @@ namespace Editor if (GetIEditor()->IsInGameMode()) { #ifdef PAL_TRAIT_LINUX_WINDOW_MANAGER_XCB - AzFramework::XcbEventHandlerBus::Broadcast(&AzFramework::XcbEventHandler::HandleXcbEvent, static_cast(message)); + // 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 return true; } return false; } -} +} // namespace Editor diff --git a/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbEventHandler.h b/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbEventHandler.h index c6307755a7..251342093a 100644 --- a/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbEventHandler.h +++ b/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbEventHandler.h @@ -23,10 +23,12 @@ 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 + class XcbEventHandlerBusTraits : public AZ::EBusTraits { public: ////////////////////////////////////////////////////////////////////////// diff --git a/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbInputDeviceMouse.cpp b/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbInputDeviceMouse.cpp new file mode 100644 index 0000000000..56f21e6533 --- /dev/null +++ b/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbInputDeviceMouse.cpp @@ -0,0 +1,652 @@ +/* + * 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 + +namespace AzFramework +{ + xcb_window_t GetSystemCursorFocusWindow() + { + void* systemCursorFocusWindow = nullptr; + AzFramework::InputSystemCursorConstraintRequestBus::BroadcastResult( + systemCursorFocusWindow, &AzFramework::InputSystemCursorConstraintRequests::GetSystemCursorConstraintWindow); + + if (!systemCursorFocusWindow) + { + return XCB_NONE; + } + + // 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. + + return static_cast(reinterpret_cast(systemCursorFocusWindow)); + } + + xcb_connection_t* XcbInputDeviceMouse::s_xcbConnection = nullptr; + xcb_screen_t* XcbInputDeviceMouse::s_xcbScreen = nullptr; + bool XcbInputDeviceMouse::m_xfixesInitialized = false; + bool XcbInputDeviceMouse::m_xInputInitialized = false; + + XcbInputDeviceMouse::XcbInputDeviceMouse(InputDeviceMouse& inputDevice) + : InputDeviceMouse::Implementation(inputDevice) + , m_systemCursorState(SystemCursorState::Unknown) + , m_systemCursorPositionNormalized(0.5f, 0.5f) + , m_prevConstraintWindow(XCB_NONE) + , m_focusWindow(XCB_NONE) + , m_cursorShown(true) + { + XcbEventHandlerBus::Handler::BusConnect(); + + SetSystemCursorState(SystemCursorState::Unknown); + } + + XcbInputDeviceMouse::~XcbInputDeviceMouse() + { + XcbEventHandlerBus::Handler::BusDisconnect(); + + SetSystemCursorState(SystemCursorState::Unknown); + } + + InputDeviceMouse::Implementation* XcbInputDeviceMouse::Create(InputDeviceMouse& inputDevice) + { + auto* interface = AzFramework::XcbConnectionManagerInterface::Get(); + if (!interface) + { + AZ_Warning("XcbInput", false, "XCB interface not available"); + return nullptr; + } + + s_xcbConnection = AzFramework::XcbConnectionManagerInterface::Get()->GetXcbConnection(); + if (!s_xcbConnection) + { + AZ_Warning("XcbInput", false, "XCB connection not available"); + return nullptr; + } + + const xcb_setup_t* xcbSetup = xcb_get_setup(s_xcbConnection); + s_xcbScreen = xcb_setup_roots_iterator(xcbSetup).data; + if (!s_xcbScreen) + { + AZ_Warning("XcbInput", false, "XCB screen not available"); + return nullptr; + } + + // Initialize XFixes extension which we use to create pointer barriers. + if (!InitializeXFixes()) + { + AZ_Warning("XcbInput", false, "XCB XFixes initialization failed"); + return nullptr; + } + + // Initialize XInput extension which is used to get RAW Input events. + if (!InitializeXInput()) + { + AZ_Warning("XcbInput", false, "XCB XInput initialization failed"); + return nullptr; + } + + return aznew XcbInputDeviceMouse(inputDevice); + } + + bool XcbInputDeviceMouse::IsConnected() const + { + return true; + } + + void XcbInputDeviceMouse::CreateBarriers(xcb_window_t window, bool create) + { + // Don't create any barriers if we are debugging. This will cause artifacts but better then + // a confined cursor during debugging. + if (AZ::Debug::Trace::IsDebuggerPresent()) + { + AZ_Warning("XcbInput", false, "Debugger running. Barriers will not be created."); + return; + } + + if (create) + { + // Destroy barriers if they are active already. + if (!m_activeBarriers.empty()) + { + for (const auto& barrier : m_activeBarriers) + { + xcb_xfixes_delete_pointer_barrier_checked(s_xcbConnection, barrier.id); + } + + m_activeBarriers.clear(); + } + + // Get window information. + const XcbStdFreePtr xcbGeometryReply{ xcb_get_geometry_reply( + s_xcbConnection, xcb_get_geometry(s_xcbConnection, window), NULL) }; + + if (!xcbGeometryReply) + { + return; + } + + const xcb_translate_coordinates_cookie_t translate_coord = + xcb_translate_coordinates(s_xcbConnection, window, s_xcbScreen->root, 0, 0); + + const XcbStdFreePtr xkbTranslateCoordReply{ xcb_translate_coordinates_reply( + s_xcbConnection, translate_coord, NULL) }; + + if (!xkbTranslateCoordReply) + { + return; + } + + const int16_t x0 = xkbTranslateCoordReply->dst_x < 0 ? 0 : xkbTranslateCoordReply->dst_x; + const int16_t y0 = xkbTranslateCoordReply->dst_y < 0 ? 0 : xkbTranslateCoordReply->dst_y; + const int16_t x1 = xkbTranslateCoordReply->dst_x + xcbGeometryReply->width; + const int16_t y1 = xkbTranslateCoordReply->dst_y + xcbGeometryReply->height; + + // ATTN For whatever reason, when making an exact rectangle the pointer will escape the top right corner in some cases. Adding + // an offset to the lines so that they cross each other prevents that. + const int16_t offset = 30; + + // Create the left barrier info. + m_activeBarriers.push_back({ xcb_generate_id(s_xcbConnection), XCB_XFIXES_BARRIER_DIRECTIONS_POSITIVE_X, x0, Clamp(y0 - offset), + x0, Clamp(y1 + offset) }); + + // Create the right barrier info. + m_activeBarriers.push_back({ xcb_generate_id(s_xcbConnection), XCB_XFIXES_BARRIER_DIRECTIONS_NEGATIVE_X, x1, Clamp(y0 - offset), + x1, Clamp(y1 + offset) }); + + // Create the top barrier info. + m_activeBarriers.push_back({ xcb_generate_id(s_xcbConnection), XCB_XFIXES_BARRIER_DIRECTIONS_POSITIVE_Y, Clamp(x0 - offset), y0, + Clamp(x1 + offset), y0 }); + + // Create the bottom barrier info. + m_activeBarriers.push_back({ xcb_generate_id(s_xcbConnection), XCB_XFIXES_BARRIER_DIRECTIONS_NEGATIVE_Y, Clamp(x0 - offset), y1, + Clamp(x1 + offset), y1 }); + + // Create the xfixes barriers. + 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) }; + + AZ_Warning( + "XcbInput", !xkbError, "XFixes, failed to create barrier %d at (%d %d %d %d)", barrier.id, barrier.x0, barrier.y0, + barrier.x1, barrier.y1); + } + } + else + { + for (const auto& barrier : m_activeBarriers) + { + xcb_xfixes_delete_pointer_barrier_checked(s_xcbConnection, barrier.id); + } + + m_activeBarriers.clear(); + } + + xcb_flush(s_xcbConnection); + } + + bool XcbInputDeviceMouse::InitializeXFixes() + { + m_xfixesInitialized = false; + + // We don't have to free query_extension_reply according to xcb documentation. + const xcb_query_extension_reply_t* query_extension_reply = xcb_get_extension_data(s_xcbConnection, &xcb_xfixes_id); + if (!query_extension_reply || !query_extension_reply->present) + { + return m_xfixesInitialized; + } + + const xcb_xfixes_query_version_cookie_t query_cookie = xcb_xfixes_query_version(s_xcbConnection, 5, 0); + + xcb_generic_error_t* error = NULL; + const XcbStdFreePtr xkbQueryRequestReply{ xcb_xfixes_query_version_reply( + s_xcbConnection, query_cookie, &error) }; + + if (!xkbQueryRequestReply || error) + { + if (error) + { + AZ_Warning("XcbInput", false, "Retrieving XFixes version failed : Error code %d", error->error_code); + free(error); + } + return m_xfixesInitialized; + } + else if (xkbQueryRequestReply->major_version < 5) + { + AZ_Warning("XcbInput", false, "XFixes version fails the minimum version check (%d<5)", xkbQueryRequestReply->major_version); + return m_xfixesInitialized; + } + + m_xfixesInitialized = true; + + return m_xfixesInitialized; + } + + bool XcbInputDeviceMouse::InitializeXInput() + { + m_xInputInitialized = false; + + // We don't have to free query_extension_reply according to xcb documentation. + const xcb_query_extension_reply_t* query_extension_reply = xcb_get_extension_data(s_xcbConnection, &xcb_input_id); + if (!query_extension_reply || !query_extension_reply->present) + { + return m_xInputInitialized; + } + + 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; + const XcbStdFreePtr xkbQueryRequestReply{ xcb_input_xi_query_version_reply( + s_xcbConnection, query_version_cookie, &error) }; + + if (!xkbQueryRequestReply || error) + { + if (error) + { + AZ_Warning("XcbInput", false, "Retrieving XInput version failed : Error code %d", error->error_code); + free(error); + } + return m_xInputInitialized; + } + else if (xkbQueryRequestReply->major_version < 2) + { + AZ_Warning("XcbInput", false, "XInput version fails the minimum version check (%d<5)", xkbQueryRequestReply->major_version); + return m_xInputInitialized; + } + + m_xInputInitialized = true; + + 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(); + + HandleCursorState(m_focusWindow, systemCursorState); + } + } + + 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); + + CreateBarriers(window, confined); + ShowCursor(window, cursorShown); + } + + SystemCursorState XcbInputDeviceMouse::GetSystemCursorState() const + { + return m_systemCursorState; + } + + 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) }; + + if (!xkbGeometryReply) + { + return; + } + + const int16_t x = static_cast(positionNormalized.GetX() * xkbGeometryReply->width); + const int16_t y = static_cast(positionNormalized.GetY() * xkbGeometryReply->height); + + xcb_warp_pointer(s_xcbConnection, XCB_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) + { + return; + } + + SetSystemCursorPositionNormalizedInternal(window, positionNormalized); + } + + AZ::Vector2 XcbInputDeviceMouse::GetSystemCursorPositionNormalizedInternal(xcb_window_t window) const + { + AZ::Vector2 position = AZ::Vector2::CreateZero(); + + const xcb_query_pointer_cookie_t pointer = xcb_query_pointer(s_xcbConnection, window); + + const XcbStdFreePtr xkbQueryPointerReply{ xcb_query_pointer_reply(s_xcbConnection, pointer, NULL) }; + + if (!xkbQueryPointerReply) + { + return position; + } + + const XcbStdFreePtr xkbGeometryReply{ xcb_get_geometry_reply( + s_xcbConnection, xcb_get_geometry(s_xcbConnection, window), NULL) }; + + if (!xkbGeometryReply) + { + return position; + } + + AZ_Assert(xkbGeometryReply->width != 0, "xkbGeometry response width must be non-zero. (%d)", xkbGeometryReply->width); + const float normalizedCursorPostionX = static_cast(xkbQueryPointerReply->win_x) / xkbGeometryReply->width; + + AZ_Assert(xkbGeometryReply->height != 0, "xkbGeometry response height must be non-zero. (%d)", xkbGeometryReply->height); + const float normalizedCursorPostionY = static_cast(xkbQueryPointerReply->win_y) / xkbGeometryReply->height; + + position = AZ::Vector2(normalizedCursorPostionX, normalizedCursorPostionY); + + return position; + } + + AZ::Vector2 XcbInputDeviceMouse::GetSystemCursorPositionNormalized() const + { + const xcb_window_t window = GetSystemCursorFocusWindow(); + if (XCB_NONE == window) + { + return AZ::Vector2::CreateZero(); + } + + return GetSystemCursorPositionNormalizedInternal(window); + } + + void XcbInputDeviceMouse::TickInputDevice() + { + ProcessRawEventQueues(); + } + + void XcbInputDeviceMouse::ShowCursor(xcb_window_t window, bool show) + { + xcb_void_cookie_t cookie; + if (show) + { + cookie = xcb_xfixes_show_cursor_checked(s_xcbConnection, window); + } + else + { + cookie = xcb_xfixes_hide_cursor_checked(s_xcbConnection, window); + } + + const XcbStdFreePtr xkbError{ xcb_request_check(s_xcbConnection, cookie) }; + + if (xkbError) + { + AZ_Warning("XcbInput", false, "ShowCursor failed: %d", xkbError->error_code); + + return; + } + + // ATTN In the following part we will when cursor gets hidden store the position of the cursor in screen space + // not window space. We use that to re-position when showing the cursor again. Is this the correct + // behavior? + + const bool cursorWasHidden = !m_cursorShown; + m_cursorShown = show; + if (!m_cursorShown) + { + m_cursorHiddenPosition = GetSystemCursorPositionNormalizedInternal(s_xcbScreen->root); + + SetSystemCursorPositionNormalized(AZ::Vector2(0.5f, 0.5f)); + } + else if (cursorWasHidden) + { + SetSystemCursorPositionNormalizedInternal(s_xcbScreen->root, m_cursorHiddenPosition); + } + + xcb_flush(s_xcbConnection); + } + + void XcbInputDeviceMouse::HandleButtonPressEvents(uint32_t detail, bool pressed) + { + bool isWheel; + float wheelDirection; + const auto* button = InputChannelFromMouseEvent(detail, isWheel, wheelDirection); + if (button) + { + QueueRawButtonEvent(*button, pressed); + } + if (isWheel) + { + float axisValue = MAX_XI_WHEEL_SENSITIVITY * wheelDirection; + QueueRawMovementEvent(InputDeviceMouse::Movement::Z, axisValue); + } + } + + 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); + switch (genericEvent->event_type) + { + case XCB_INPUT_RAW_BUTTON_PRESS: + { + const xcb_input_raw_button_press_event_t* mouseButtonEvent = + reinterpret_cast(event); + HandleButtonPressEvents(mouseButtonEvent->detail, true); + } + break; + case XCB_INPUT_RAW_BUTTON_RELEASE: + { + const xcb_input_raw_button_release_event_t* mouseButtonEvent = + reinterpret_cast(event); + HandleButtonPressEvents(mouseButtonEvent->detail, false); + } + break; + case XCB_INPUT_RAW_MOTION: + { + const xcb_input_raw_motion_event_t* mouseMotionEvent = reinterpret_cast(event); + + int axisLen = xcb_input_raw_button_press_axisvalues_length(mouseMotionEvent); + const xcb_input_fp3232_t* axisvalues = xcb_input_raw_button_press_axisvalues_raw(mouseMotionEvent); + for (int i = 0; i < axisLen; ++i) + { + const float axisValue = fp3232ToFloat(axisvalues[i]); + + switch (i) + { + case 0: + QueueRawMovementEvent(InputDeviceMouse::Movement::X, axisValue); + break; + case 1: + QueueRawMovementEvent(InputDeviceMouse::Movement::Y, axisValue); + break; + } + } + } + break; + } + } + + 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. + 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; + } + } + } + break; + case XCB_FOCUS_IN: + { + const xcb_focus_in_event_t* focusInEvent = reinterpret_cast(event); + if (m_focusWindow != focusInEvent->event) + { + m_focusWindow = focusInEvent->event; + HandleCursorState(m_focusWindow, m_systemCursorState); + } + } + break; + case XCB_FOCUS_OUT: + { + const xcb_focus_out_event_t* focusOutEvent = reinterpret_cast(event); + HandleCursorState(focusOutEvent->event, SystemCursorState::UnconstrainedAndVisible); + + ProcessRawEventQueues(); + ResetInputChannelStates(); + + m_focusWindow = XCB_NONE; + } + break; + } + } +} // namespace AzFramework diff --git a/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbInputDeviceMouse.h b/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbInputDeviceMouse.h new file mode 100644 index 0000000000..106d204ca9 --- /dev/null +++ b/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbInputDeviceMouse.h @@ -0,0 +1,193 @@ +/* + * 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 + +// The maximum number of raw input axis this mouse device supports. +constexpr uint32_t MAX_XI_RAW_AXIS = 2; + +// The sensitivity of the wheel. +constexpr float MAX_XI_WHEEL_SENSITIVITY = 140.0f; + +namespace AzFramework +{ + class XcbInputDeviceMouse + : public InputDeviceMouse::Implementation + , public XcbEventHandlerBus::Handler + { + public: + AZ_CLASS_ALLOCATOR(XcbInputDeviceMouse, AZ::SystemAllocator, 0); + + //////////////////////////////////////////////////////////////////////////////////////////// + //! Constructor + //! \param[in] inputDevice Reference to the input device being implemented + XcbInputDeviceMouse(InputDeviceMouse& inputDevice); + + //////////////////////////////////////////////////////////////////////////////////////////// + //! Destructor + virtual ~XcbInputDeviceMouse(); + + static XcbInputDeviceMouse::Implementation* Create(InputDeviceMouse& inputDevice); + + protected: + //////////////////////////////////////////////////////////////////////////////////////////// + //! \ref AzFramework::InputDeviceMouse::Implementation::IsConnected + bool IsConnected() const override; + + //////////////////////////////////////////////////////////////////////////////////////////// + //! \ref AzFramework::InputDeviceMouse::Implementation::SetSystemCursorState + void SetSystemCursorState(SystemCursorState systemCursorState) override; + + //////////////////////////////////////////////////////////////////////////////////////////// + //! \ref AzFramework::InputDeviceMouse::Implementation::GetSystemCursorState + SystemCursorState GetSystemCursorState() const override; + + //////////////////////////////////////////////////////////////////////////////////////////// + //! \ref AzFramework::InputDeviceMouse::Implementation::SetSystemCursorPositionNormalized + void SetSystemCursorPositionNormalized(AZ::Vector2 positionNormalized) override; + + //////////////////////////////////////////////////////////////////////////////////////////// + //! \ref AzFramework::InputDeviceMouse::Implementation::GetSystemCursorPositionNormalized + AZ::Vector2 GetSystemCursorPositionNormalized() const override; + + //////////////////////////////////////////////////////////////////////////////////////////// + //! \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; + + //! Initialize XFixes extension. Used for barriers. + static bool InitializeXFixes(); + + //! 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); + + //! Helper function. + void SystemCursorStateToLogic(SystemCursorState systemCursorState, bool& confined, bool& cursorShown); + + //! Shows/Hides the cursor. + void ShowCursor(xcb_window_t window, bool show); + + //! Get the normalized cursor position. The coordinates returned are relative to the specified window. + AZ::Vector2 GetSystemCursorPositionNormalizedInternal(xcb_window_t window) const; + + //! Set the normalized cursor position. The normalized position will be relative to the specified window. + void SetSystemCursorPositionNormalizedInternal(xcb_window_t window, AZ::Vector2 positionNormalized); + + //! 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); + + //! Will handle all raw input events. + void HandleRawInputEvents(const xcb_ge_generic_event_t* event); + + //! Convert XInput fp1616 to float. + inline float fp1616ToFloat(xcb_input_fp1616_t value) const + { + return static_cast((value >> 16) + (value & 0xffff) / 0xffff); + } + + //! Convert XInput fp3232 to float. + inline float fp3232ToFloat(xcb_input_fp3232_t value) const + { + return static_cast(value.integral) + static_cast(value.frac / (float)(1ull << 32)); + } + + const InputChannelId* InputChannelFromMouseEvent(xcb_button_t button, bool& isWheel, float& direction) const + { + isWheel = false; + direction = 1.0f; + 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; + case XCB_BUTTON_INDEX_4: + isWheel = true; + direction = 1.0f; + break; + case XCB_BUTTON_INDEX_5: + isWheel = true; + direction = -1.0f; + break; + default: + break; + } + + return nullptr; + } + + // Barriers work only with positive values. We clamp here to zero. + inline int16_t Clamp(int16_t value) const + { + return value < 0 ? 0 : value; + } + + private: + //! The current system cursor state + SystemCursorState m_systemCursorState; + + //! The cursor position before it got hidden. + 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; + + //! Will be true if the xfixes extension could be initialized. + static bool m_xfixesInitialized; + + //! 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; + + //! Will be true if the cursor is shown else false. + bool m_cursorShown; + + struct XFixesBarrierProperty + { + xcb_xfixes_barrier_t id; + uint32_t direction; + int16_t x0, y0, x1, y1; + }; + + //! Array that holds barrier information used to confine the cursor. + std::vector m_activeBarriers; + }; +} // namespace AzFramework diff --git a/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbNativeWindow.cpp b/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbNativeWindow.cpp index 43f7e7140f..af5b3af3d8 100644 --- a/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbNativeWindow.cpp +++ b/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbNativeWindow.cpp @@ -8,25 +8,31 @@ #include #include -#include #include #include +#include #include namespace AzFramework { [[maybe_unused]] const char XcbErrorWindow[] = "XcbNativeWindow"; - static constexpr uint8_t s_XcbFormatDataSize = 32; // Format indicator for xcb for client messages - static constexpr uint16_t s_DefaultXcbWindowBorderWidth = 4; // The default border with in pixels if a border was specified - static constexpr uint8_t s_XcbResponseTypeMask = 0x7f; // Mask to extract the specific event type from an xcb event + static constexpr uint8_t s_XcbFormatDataSize = 32; // Format indicator for xcb for client messages + static constexpr uint16_t s_DefaultXcbWindowBorderWidth = 4; // The default border with in pixels if a border was specified + static constexpr uint8_t s_XcbResponseTypeMask = 0x7f; // Mask to extract the specific event type from an xcb event + +#define _NET_WM_STATE_REMOVE 0l +#define _NET_WM_STATE_ADD 1l +#define _NET_WM_STATE_TOGGLE 2l //////////////////////////////////////////////////////////////////////////////////////////////// - XcbNativeWindow::XcbNativeWindow() + XcbNativeWindow::XcbNativeWindow() : NativeWindow::Implementation() + , m_xcbConnection(nullptr) + , m_xcbRootScreen(nullptr) + , m_xcbWindow(XCB_NONE) { - if (auto xcbConnectionManager = AzFramework::XcbConnectionManagerInterface::Get(); - xcbConnectionManager != nullptr) + if (auto xcbConnectionManager = AzFramework::XcbConnectionManagerInterface::Get(); xcbConnectionManager != nullptr) { m_xcbConnection = xcbConnectionManager->GetXcbConnection(); } @@ -34,89 +40,184 @@ namespace AzFramework } //////////////////////////////////////////////////////////////////////////////////////////////// - XcbNativeWindow::~XcbNativeWindow() = default; + XcbNativeWindow::~XcbNativeWindow() + { + if (XCB_NONE != m_xcbWindow) + { + xcb_destroy_window(m_xcbConnection, m_xcbWindow); + } + } //////////////////////////////////////////////////////////////////////////////////////////////// - void XcbNativeWindow::InitWindow(const AZStd::string& title, - const WindowGeometry& geometry, - const WindowStyleMasks& styleMasks) + void XcbNativeWindow::InitWindow(const AZStd::string& title, const WindowGeometry& geometry, const WindowStyleMasks& styleMasks) { - // Get the parent window + // Get the parent window const xcb_setup_t* xcbSetup = xcb_get_setup(m_xcbConnection); - xcb_screen_t* xcbRootScreen = xcb_setup_roots_iterator(xcbSetup).data; - xcb_window_t xcbParentWindow = xcbRootScreen->root; + m_xcbRootScreen = xcb_setup_roots_iterator(xcbSetup).data; + xcb_window_t xcbParentWindow = m_xcbRootScreen->root; // Create an XCB window from the connection m_xcbWindow = xcb_generate_id(m_xcbConnection); uint16_t borderWidth = 0; const uint32_t mask = styleMasks.m_platformAgnosticStyleMask; - if ((mask & WindowStyleMasks::WINDOW_STYLE_BORDERED) || - (mask & WindowStyleMasks::WINDOW_STYLE_RESIZEABLE)) + if ((mask & WindowStyleMasks::WINDOW_STYLE_BORDERED) || (mask & WindowStyleMasks::WINDOW_STYLE_RESIZEABLE)) { borderWidth = s_DefaultXcbWindowBorderWidth; } uint32_t eventMask = XCB_CW_BACK_PIXEL | XCB_CW_EVENT_MASK; - - const uint32_t interestedEvents = - XCB_EVENT_MASK_STRUCTURE_NOTIFY - | XCB_EVENT_MASK_BUTTON_PRESS - | XCB_EVENT_MASK_BUTTON_RELEASE - | XCB_EVENT_MASK_KEY_PRESS - | XCB_EVENT_MASK_KEY_RELEASE - | XCB_EVENT_MASK_POINTER_MOTION - ; - uint32_t valueList[] = { xcbRootScreen->black_pixel, - interestedEvents }; + + const uint32_t interestedEvents = XCB_EVENT_MASK_STRUCTURE_NOTIFY | XCB_EVENT_MASK_KEY_PRESS | XCB_EVENT_MASK_KEY_RELEASE | + XCB_EVENT_MASK_FOCUS_CHANGE | XCB_EVENT_MASK_PROPERTY_CHANGE; + uint32_t valueList[] = { m_xcbRootScreen->black_pixel, interestedEvents }; xcb_void_cookie_t xcbCheckResult; - xcbCheckResult = xcb_create_window_checked(m_xcbConnection, - XCB_COPY_FROM_PARENT, - m_xcbWindow, - xcbParentWindow, - aznumeric_cast(geometry.m_posX), - aznumeric_cast(geometry.m_posY), - aznumeric_cast(geometry.m_width), - aznumeric_cast(geometry.m_height), - borderWidth, - XCB_WINDOW_CLASS_INPUT_OUTPUT, - xcbRootScreen->root_visual, - eventMask, - valueList); + xcbCheckResult = xcb_create_window_checked( + m_xcbConnection, XCB_COPY_FROM_PARENT, m_xcbWindow, xcbParentWindow, aznumeric_cast(geometry.m_posX), + aznumeric_cast(geometry.m_posY), aznumeric_cast(geometry.m_width), aznumeric_cast(geometry.m_height), + borderWidth, XCB_WINDOW_CLASS_INPUT_OUTPUT, m_xcbRootScreen->root_visual, eventMask, valueList); AZ_Assert(ValidateXcbResult(xcbCheckResult), "Failed to create xcb window."); SetWindowTitle(title); - // Setup the window close event - const static char* wmProtocolString = "WM_PROTOCOLS"; - - xcb_intern_atom_cookie_t cookieProtocol = xcb_intern_atom(m_xcbConnection, 1, strlen(wmProtocolString), wmProtocolString); - xcb_intern_atom_reply_t* replyProtocol = xcb_intern_atom_reply(m_xcbConnection, cookieProtocol, nullptr); - AZ_Error(XcbErrorWindow, replyProtocol != nullptr, "Unable to query xcb '%s' atom", wmProtocolString); - m_xcbAtomProtocols = replyProtocol->atom; - - const static char* wmDeleteWindowString = "WM_DELETE_WINDOW"; - xcb_intern_atom_cookie_t cookieDeleteWindow = xcb_intern_atom(m_xcbConnection, 0, strlen(wmDeleteWindowString), wmDeleteWindowString); - xcb_intern_atom_reply_t* replyDeleteWindow = xcb_intern_atom_reply(m_xcbConnection, cookieDeleteWindow, nullptr); - AZ_Error(XcbErrorWindow, replyDeleteWindow != nullptr, "Unable to query xcb '%s' atom", wmDeleteWindowString); - m_xcbAtomDeleteWindow = replyDeleteWindow->atom; - - xcbCheckResult = xcb_change_property_checked(m_xcbConnection, - XCB_PROP_MODE_REPLACE, - m_xcbWindow, - m_xcbAtomProtocols, - XCB_ATOM_ATOM, - s_XcbFormatDataSize, - 1, - &m_xcbAtomDeleteWindow); - - AZ_Assert(ValidateXcbResult(xcbCheckResult), "Failed to change the xcb atom property for WM_CLOSE event"); - + m_posX = geometry.m_posX; + m_posY = geometry.m_posY; m_width = geometry.m_width; m_height = geometry.m_height; + + InitializeAtoms(); + + xcb_client_message_event_t event; + event.response_type = XCB_CLIENT_MESSAGE; + event.type = _NET_REQUEST_FRAME_EXTENTS; + event.window = m_xcbWindow; + event.format = 32; + event.sequence = 0; + event.data.data32[0] = 0l; + event.data.data32[1] = 0l; + event.data.data32[2] = 0l; + event.data.data32[3] = 0l; + event.data.data32[4] = 0l; + xcbCheckResult = xcb_send_event( + m_xcbConnection, 1, m_xcbRootScreen->root, XCB_EVENT_MASK_STRUCTURE_NOTIFY | XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT, + (const char*)&event); + AZ_Assert(ValidateXcbResult(xcbCheckResult), "Failed to set _NET_REQUEST_FRAME_EXTENTS"); + + // The WM will be able to kill the application if it gets unresponsive. + int32_t pid = getpid(); + xcb_change_property(m_xcbConnection, XCB_PROP_MODE_REPLACE, m_xcbWindow, _NET_WM_PID, XCB_ATOM_CARDINAL, 32, 1, &pid); + + xcb_flush(m_xcbConnection); + } + + xcb_atom_t XcbNativeWindow::GetAtom(const char* atomName) + { + xcb_intern_atom_cookie_t intern_atom_cookie = xcb_intern_atom(m_xcbConnection, 0, strlen(atomName), atomName); + XcbStdFreePtr xkbinternAtom{ xcb_intern_atom_reply(m_xcbConnection, intern_atom_cookie, NULL) }; + + if (!xkbinternAtom) + { + AZ_Error(XcbErrorWindow, xkbinternAtom != nullptr, "Unable to query xcb '%s' atom", atomName); + return XCB_NONE; + } + + return xkbinternAtom->atom; + } + + int XcbNativeWindow::SetAtom(xcb_window_t window, xcb_atom_t atom, xcb_atom_t type, size_t len, void* data) + { + xcb_void_cookie_t cookie = xcb_change_property_checked(m_xcbConnection, XCB_PROP_MODE_REPLACE, window, atom, type, 32, len, data); + XcbStdFreePtr xkbError{ xcb_request_check(m_xcbConnection, cookie) }; + + if (!xkbError) + { + return 0; + } + + return xkbError->error_code; + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + + void XcbNativeWindow::InitializeAtoms() + { + AZStd::vector Atoms; + + _NET_ACTIVE_WINDOW = GetAtom("_NET_ACTIVE_WINDOW"); + _NET_WM_BYPASS_COMPOSITOR = GetAtom("_NET_WM_BYPASS_COMPOSITOR"); + + // --------------------------------------------------------------------- + // Handle all WM Protocols atoms. + // + + WM_PROTOCOLS = GetAtom("WM_PROTOCOLS"); + + // This atom is used to close a window. Emitted when user clicks the close button. + WM_DELETE_WINDOW = GetAtom("WM_DELETE_WINDOW"); + + Atoms.push_back(WM_DELETE_WINDOW); + + xcb_change_property( + m_xcbConnection, XCB_PROP_MODE_REPLACE, m_xcbWindow, WM_PROTOCOLS, XCB_ATOM_ATOM, 32, Atoms.size(), Atoms.data()); + + xcb_flush(m_xcbConnection); + + // --------------------------------------------------------------------- + // Handle all WM State atoms. + // + + _NET_WM_STATE = GetAtom("_NET_WM_STATE"); + _NET_WM_STATE_FULLSCREEN = GetAtom("_NET_WM_STATE_FULLSCREEN"); + _NET_WM_STATE_MAXIMIZED_VERT = GetAtom("_NET_WM_STATE_MAXIMIZED_VERT"); + _NET_WM_STATE_MAXIMIZED_HORZ = GetAtom("_NET_WM_STATE_MAXIMIZED_HORZ"); + _NET_MOVERESIZE_WINDOW = GetAtom("_NET_MOVERESIZE_WINDOW"); + _NET_REQUEST_FRAME_EXTENTS = GetAtom("_NET_REQUEST_FRAME_EXTENTS"); + _NET_FRAME_EXTENTS = GetAtom("_NET_FRAME_EXTENTS"); + _NET_WM_PID = GetAtom("_NET_WM_PID"); + } + + void XcbNativeWindow::GetWMStates() + { + xcb_get_property_cookie_t cookie = xcb_get_property(m_xcbConnection, 0, m_xcbWindow, _NET_WM_STATE, XCB_ATOM_ATOM, 0, 1024); + + xcb_generic_error_t* error = nullptr; + XcbStdFreePtr xkbGetPropertyReply{ xcb_get_property_reply(m_xcbConnection, cookie, &error) }; + + if (!xkbGetPropertyReply || error || !((xkbGetPropertyReply->format == 32) && (xkbGetPropertyReply->type == XCB_ATOM_ATOM))) + { + AZ_Warning("ApplicationLinux", false, "Acquiring _NET_WM_STATE information from the WM failed."); + + if (error) + { + AZ_TracePrintf("Error", "Error code %d", error->error_code); + free(error); + } + return; + } + + m_fullscreenState = false; + m_horizontalyMaximized = false; + m_verticallyMaximized = false; + + const xcb_atom_t* states = static_cast(xcb_get_property_value(xkbGetPropertyReply.get())); + for (int i = 0; i < xkbGetPropertyReply->length; i++) + { + if (states[i] == _NET_WM_STATE_FULLSCREEN) + { + m_fullscreenState = true; + } + else if (states[i] == _NET_WM_STATE_MAXIMIZED_HORZ) + { + m_horizontalyMaximized = true; + } + else if (states[i] == _NET_WM_STATE_MAXIMIZED_VERT) + { + m_verticallyMaximized = true; + } + } } //////////////////////////////////////////////////////////////////////////////////////////////// @@ -146,7 +247,7 @@ namespace AzFramework xcb_flush(m_xcbConnection); } XcbEventHandlerBus::Handler::BusDisconnect(); - } + } //////////////////////////////////////////////////////////////////////////////////////////////// NativeWindowHandle XcbNativeWindow::GetWindowHandle() const @@ -158,14 +259,9 @@ namespace AzFramework void XcbNativeWindow::SetWindowTitle(const AZStd::string& title) { xcb_void_cookie_t xcbCheckResult; - xcbCheckResult = xcb_change_property(m_xcbConnection, - XCB_PROP_MODE_REPLACE, - m_xcbWindow, - XCB_ATOM_WM_NAME, - XCB_ATOM_STRING, - 8, - static_cast(title.size()), - title.c_str()); + xcbCheckResult = xcb_change_property( + m_xcbConnection, XCB_PROP_MODE_REPLACE, m_xcbWindow, XCB_ATOM_WM_NAME, XCB_ATOM_STRING, 8, static_cast(title.size()), + title.c_str()); AZ_Assert(ValidateXcbResult(xcbCheckResult), "Failed to set window title."); } @@ -175,7 +271,7 @@ namespace AzFramework const uint32_t values[] = { clientAreaSize.m_width, clientAreaSize.m_height }; xcb_configure_window(m_xcbConnection, m_xcbWindow, XCB_CONFIG_WINDOW_WIDTH | XCB_CONFIG_WINDOW_HEIGHT, values); - + m_width = clientAreaSize.m_width; m_height = clientAreaSize.m_height; } @@ -185,16 +281,77 @@ namespace AzFramework { // [GFX TODO][GHI - 2678] // Using 60 for now until proper support is added + return 60; } + bool XcbNativeWindow::GetFullScreenState() const + { + return m_fullscreenState; + } + + void XcbNativeWindow::SetFullScreenState(bool fullScreenState) + { + // TODO This is a pretty basic full-screen implementation using WM's _NET_WM_STATE_FULLSCREEN state. + // Do we have to provide also the old way? + + GetWMStates(); + + xcb_client_message_event_t event; + event.response_type = XCB_CLIENT_MESSAGE; + event.type = _NET_WM_STATE; + event.window = m_xcbWindow; + event.format = 32; + event.sequence = 0; + event.data.data32[0] = fullScreenState ? _NET_WM_STATE_ADD : _NET_WM_STATE_REMOVE; + event.data.data32[1] = _NET_WM_STATE_FULLSCREEN; + event.data.data32[2] = 0; + event.data.data32[3] = 1; + event.data.data32[4] = 0; + xcb_void_cookie_t xcbCheckResult = xcb_send_event( + m_xcbConnection, 1, m_xcbRootScreen->root, XCB_EVENT_MASK_STRUCTURE_NOTIFY | XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT, + (const char*)&event); + AZ_Assert(ValidateXcbResult(xcbCheckResult), "Failed to set _NET_WM_STATE_FULLSCREEN"); + + // Also try to disable/enable the compositor if possible. Might help in some cases. + const long _NET_WM_BYPASS_COMPOSITOR_HINT_ON = m_fullscreenState ? 1 : 0; + SetAtom(m_xcbWindow, _NET_WM_BYPASS_COMPOSITOR, XCB_ATOM_CARDINAL, 32, (char*)&_NET_WM_BYPASS_COMPOSITOR_HINT_ON); + + if (!fullScreenState) + { + if (m_horizontalyMaximized || m_verticallyMaximized) + { + printf("Remove maximized state.\n"); + xcb_client_message_event_t event; + event.response_type = XCB_CLIENT_MESSAGE; + event.type = _NET_WM_STATE; + event.window = m_xcbWindow; + event.format = 32; + event.sequence = 0; + event.data.data32[0] = _NET_WM_STATE_MAXIMIZED_VERT; + event.data.data32[1] = _NET_WM_STATE_MAXIMIZED_HORZ; + event.data.data32[2] = 0; + event.data.data32[3] = 0; + event.data.data32[4] = 0; + xcb_void_cookie_t xcbCheckResult = xcb_send_event( + m_xcbConnection, 1, m_xcbRootScreen->root, XCB_EVENT_MASK_STRUCTURE_NOTIFY | XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT, + (const char*)&event); + AZ_Assert( + ValidateXcbResult(xcbCheckResult), "Failed to remove _NET_WM_STATE_MAXIMIZED_VERT | _NET_WM_STATE_MAXIMIZED_HORZ"); + } + } + + xcb_flush(m_xcbConnection); + m_fullscreenState = fullScreenState; + } + //////////////////////////////////////////////////////////////////////////////////////////////// bool XcbNativeWindow::ValidateXcbResult(xcb_void_cookie_t cookie) { bool result = true; if (xcb_generic_error_t* error = xcb_request_check(m_xcbConnection, cookie)) { - AZ_TracePrintf("Error","Error code %d", error->error_code); + AZ_TracePrintf("Error", "Error code %d", error->error_code); result = false; } return result; @@ -205,20 +362,20 @@ namespace AzFramework { switch (event->response_type & s_XcbResponseTypeMask) { - case XCB_CONFIGURE_NOTIFY: + case XCB_CONFIGURE_NOTIFY: { xcb_configure_notify_event_t* cne = reinterpret_cast(event); - WindowSizeChanged(aznumeric_cast(cne->width), - aznumeric_cast(cne->height)); - + if ((cne->width != m_width) || (cne->height != m_height)) + { + WindowSizeChanged(aznumeric_cast(cne->width), aznumeric_cast(cne->height)); + } break; } - case XCB_CLIENT_MESSAGE: + case XCB_CLIENT_MESSAGE: { xcb_client_message_event_t* cme = reinterpret_cast(event); - if ((cme->type == m_xcbAtomProtocols) && - (cme->format == s_XcbFormatDataSize) && - (cme->data.data32[0] == m_xcbAtomDeleteWindow)) + + if ((cme->type == WM_PROTOCOLS) && (cme->format == s_XcbFormatDataSize) && (cme->data.data32[0] == WM_DELETE_WINDOW)) { Deactivate(); @@ -239,7 +396,8 @@ namespace AzFramework if (m_activated) { - WindowNotificationBus::Event(reinterpret_cast(m_xcbWindow), &WindowNotificationBus::Events::OnWindowResized, width, height); + WindowNotificationBus::Event( + reinterpret_cast(m_xcbWindow), &WindowNotificationBus::Events::OnWindowResized, width, height); } } } diff --git a/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbNativeWindow.h b/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbNativeWindow.h index 36737e24e8..38462dcb08 100644 --- a/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbNativeWindow.h +++ b/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbNativeWindow.h @@ -27,15 +27,16 @@ namespace AzFramework //////////////////////////////////////////////////////////////////////////////////////////// // NativeWindow::Implementation - void InitWindow(const AZStd::string& title, - const WindowGeometry& geometry, - const WindowStyleMasks& styleMasks) override; + void InitWindow(const AZStd::string& title, const WindowGeometry& geometry, const WindowStyleMasks& styleMasks) override; void Activate() override; void Deactivate() override; NativeWindowHandle GetWindowHandle() const override; void SetWindowTitle(const AZStd::string& title) override; void ResizeClientArea(WindowSize clientAreaSize) override; - uint32_t GetDisplayRefreshRate() const override; + uint32_t GetDisplayRefreshRate() const override; + + bool GetFullScreenState() const override; + void SetFullScreenState(bool fullScreenState) override; //////////////////////////////////////////////////////////////////////////////////////////// // XcbEventHandlerBus::Handler @@ -44,10 +45,46 @@ namespace AzFramework private: bool ValidateXcbResult(xcb_void_cookie_t cookie); void WindowSizeChanged(const uint32_t width, const uint32_t height); + int SetAtom(xcb_window_t window, xcb_atom_t atom, xcb_atom_t type, size_t len, void* data); - xcb_connection_t* m_xcbConnection = nullptr; - xcb_window_t m_xcbWindow = 0; - xcb_atom_t m_xcbAtomProtocols; - xcb_atom_t m_xcbAtomDeleteWindow; + // Initialize one atom. + xcb_atom_t GetAtom(const char* atomName); + + // Initialize all used atoms. + void InitializeAtoms(); + void GetWMStates(); + + xcb_connection_t* m_xcbConnection = nullptr; + xcb_screen_t* m_xcbRootScreen = nullptr; + xcb_window_t m_xcbWindow = 0; + int32_t m_posX; + int32_t m_posY; + bool m_fullscreenState = false; + bool m_horizontalyMaximized = false; + bool m_verticallyMaximized = false; + + // Use exact atom names for easy readability and usage. + xcb_atom_t WM_PROTOCOLS; + xcb_atom_t WM_DELETE_WINDOW; + // This atom is used to activate a window. + xcb_atom_t _NET_ACTIVE_WINDOW; + // This atom is use to bypass a compositor. Used during fullscreen mode. + xcb_atom_t _NET_WM_BYPASS_COMPOSITOR; + // This atom is used to change the state of a window using the WM. + xcb_atom_t _NET_WM_STATE; + // This atom is used to enable/disable fullscreen mode of a window. + xcb_atom_t _NET_WM_STATE_FULLSCREEN; + // This atom is used to extend the window to max vertically. + xcb_atom_t _NET_WM_STATE_MAXIMIZED_VERT; + // This atom is used to extend the window to max horizontally. + xcb_atom_t _NET_WM_STATE_MAXIMIZED_HORZ; + // This atom is used to position and resize a window. + xcb_atom_t _NET_MOVERESIZE_WINDOW; + // This atom is used to request the extent of the window. + xcb_atom_t _NET_REQUEST_FRAME_EXTENTS; + // This atom is used to identify the reply event for _NET_REQUEST_FRAME_EXTENTS + xcb_atom_t _NET_FRAME_EXTENTS; + // This atom is used to allow WM to kill app if not responsive anymore + xcb_atom_t _NET_WM_PID; }; } // namespace AzFramework diff --git a/Code/Framework/AzFramework/Platform/Common/Xcb/azframework_xcb_files.cmake b/Code/Framework/AzFramework/Platform/Common/Xcb/azframework_xcb_files.cmake index 68de710fa1..48afccc0d0 100644 --- a/Code/Framework/AzFramework/Platform/Common/Xcb/azframework_xcb_files.cmake +++ b/Code/Framework/AzFramework/Platform/Common/Xcb/azframework_xcb_files.cmake @@ -12,6 +12,8 @@ set(FILES AzFramework/XcbConnectionManager.h AzFramework/XcbInputDeviceKeyboard.cpp AzFramework/XcbInputDeviceKeyboard.h + AzFramework/XcbInputDeviceMouse.cpp + AzFramework/XcbInputDeviceMouse.h AzFramework/XcbInterface.h AzFramework/XcbNativeWindow.cpp AzFramework/XcbNativeWindow.h diff --git a/Code/Framework/AzFramework/Platform/Linux/AzFramework/Input/Devices/Mouse/InputDeviceMouse_Linux.cpp b/Code/Framework/AzFramework/Platform/Linux/AzFramework/Input/Devices/Mouse/InputDeviceMouse_Linux.cpp new file mode 100644 index 0000000000..e2c3c2d575 --- /dev/null +++ b/Code/Framework/AzFramework/Platform/Linux/AzFramework/Input/Devices/Mouse/InputDeviceMouse_Linux.cpp @@ -0,0 +1,27 @@ +/* + * 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 + * + */ + +#if PAL_TRAIT_LINUX_WINDOW_MANAGER_XCB +#include +#endif + +namespace AzFramework +{ + InputDeviceMouse::Implementation* InputDeviceMouse::Implementation::Create(InputDeviceMouse& inputDevice) + { +#if PAL_TRAIT_LINUX_WINDOW_MANAGER_XCB + return XcbInputDeviceMouse::Create(inputDevice); +#elif PAL_TRAIT_LINUX_WINDOW_MANAGER_WAYLAND +#error "Linux Window Manager Wayland not supported." + return nullptr; +#else +#error "Linux Window Manager not recognized." + return nullptr; +#endif // PAL_TRAIT_LINUX_WINDOW_MANAGER_XCB + } +} // namespace AzFramework diff --git a/Code/Framework/AzFramework/Platform/Linux/platform_linux.cmake b/Code/Framework/AzFramework/Platform/Linux/platform_linux.cmake index b08c3b310d..66211c6085 100644 --- a/Code/Framework/AzFramework/Platform/Linux/platform_linux.cmake +++ b/Code/Framework/AzFramework/Platform/Linux/platform_linux.cmake @@ -22,8 +22,10 @@ if (${PAL_TRAIT_LINUX_WINDOW_MANAGER} STREQUAL "xcb") PRIVATE 3rdParty::X11::xcb 3rdParty::X11::xcb_xkb + 3rdParty::X11::xcb_xfixes 3rdParty::X11::xkbcommon 3rdParty::X11::xkbcommon_X11 + xcb-xinput ) elseif(PAL_TRAIT_LINUX_WINDOW_MANAGER STREQUAL "wayland") diff --git a/Code/Framework/AzFramework/Platform/Linux/platform_linux_files.cmake b/Code/Framework/AzFramework/Platform/Linux/platform_linux_files.cmake index 4f06aa5c57..206563fdda 100644 --- a/Code/Framework/AzFramework/Platform/Linux/platform_linux_files.cmake +++ b/Code/Framework/AzFramework/Platform/Linux/platform_linux_files.cmake @@ -22,8 +22,8 @@ set(FILES AzFramework/Windowing/NativeWindow_Linux.cpp ../Common/Unimplemented/AzFramework/Input/Devices/Gamepad/InputDeviceGamepad_Unimplemented.cpp AzFramework/Input/Devices/Keyboard/InputDeviceKeyboard_Linux.cpp + AzFramework/Input/Devices/Mouse/InputDeviceMouse_Linux.cpp ../Common/Unimplemented/AzFramework/Input/Devices/Motion/InputDeviceMotion_Unimplemented.cpp - ../Common/Unimplemented/AzFramework/Input/Devices/Mouse/InputDeviceMouse_Unimplemented.cpp ../Common/Unimplemented/AzFramework/Input/Devices/Touch/InputDeviceTouch_Unimplemented.cpp AzFramework/Input/User/LocalUserId_Platform.h ../Common/Default/AzFramework/Input/User/LocalUserId_Default.h