From 7fbfda037175fef7d63ed692f3636b26906b861e Mon Sep 17 00:00:00 2001 From: hultonha <82228511+hultonha@users.noreply.github.com> Date: Wed, 25 Aug 2021 09:28:41 +0100 Subject: [PATCH] Fix mouse capture behavior for Editor Viewport (#3417) * first pass of fixes for cursor capture and context menu Signed-off-by: hultonha * restore previous behavior of HandleMouseMoveEvent Signed-off-by: hultonha * tidy-up from previous cursor/input changes Signed-off-by: hultonha * add missing casts Signed-off-by: hultonha * small updates to support tests Signed-off-by: hultonha * additional tests and some tidy-up Signed-off-by: hultonha * small updates before publishing PR (comment/naming updates) Signed-off-by: hultonha * add missing parameter to MouseInteractionEvent constructor Signed-off-by: hultonha --- Code/Editor/EditorViewportSettings.cpp | 13 ++- Code/Editor/EditorViewportSettings.h | 3 + Code/Editor/EditorViewportWidget.cpp | 25 +++-- .../test_ModularViewportCameraController.cpp | 102 +++++++++++++++++- Code/Editor/ViewportManipulatorController.cpp | 20 ++-- Code/Framework/AzCore/AzCore/std/math.h | 2 + .../AzFramework/Viewport/CameraInput.cpp | 18 +++- .../AzFramework/Viewport/CameraInput.h | 1 + .../AzFramework/Viewport/CursorState.h | 19 +++- .../AzManipulatorTestFrameworkUtils.cpp | 2 +- .../Input/QtEventToAzInputManager.cpp | 57 ++++++---- .../Input/QtEventToAzInputManager.h | 9 +- .../UnitTest/AzToolsFrameworkTestHelpers.cpp | 2 +- .../Viewport/EditorContextMenu.cpp | 3 +- .../AzToolsFramework/Viewport/ViewportTypes.h | 4 +- 15 files changed, 231 insertions(+), 49 deletions(-) diff --git a/Code/Editor/EditorViewportSettings.cpp b/Code/Editor/EditorViewportSettings.cpp index 6e0ed86d2a..38831a1de4 100644 --- a/Code/Editor/EditorViewportSettings.cpp +++ b/Code/Editor/EditorViewportSettings.cpp @@ -33,6 +33,7 @@ namespace SandboxEditor constexpr AZStd::string_view CameraTranslateSmoothnessSetting = "/Amazon/Preferences/Editor/Camera/TranslateSmoothness"; constexpr AZStd::string_view CameraTranslateSmoothingSetting = "/Amazon/Preferences/Editor/Camera/TranslateSmoothing"; constexpr AZStd::string_view CameraRotateSmoothingSetting = "/Amazon/Preferences/Editor/Camera/RotateSmoothing"; + constexpr AZStd::string_view CameraCaptureCursorLookSetting = "/Amazon/Preferences/Editor/Camera/CaptureCursorLook"; constexpr AZStd::string_view CameraTranslateForwardIdSetting = "/Amazon/Preferences/Editor/Camera/CameraTranslateForwardId"; constexpr AZStd::string_view CameraTranslateBackwardIdSetting = "/Amazon/Preferences/Editor/Camera/CameraTranslateBackwardId"; constexpr AZStd::string_view CameraTranslateLeftIdSetting = "/Amazon/Preferences/Editor/Camera/CameraTranslateLeftId"; @@ -60,7 +61,7 @@ namespace SandboxEditor AZStd::remove_cvref_t GetRegistry(const AZStd::string_view setting, T&& defaultValue) { AZStd::remove_cvref_t value = AZStd::forward(defaultValue); - if (auto* registry = AZ::SettingsRegistry::Get()) + if (const auto* registry = AZ::SettingsRegistry::Get()) { registry->Get(value, setting); } @@ -281,6 +282,16 @@ namespace SandboxEditor SetRegistry(CameraTranslateSmoothingSetting, enabled); } + bool CameraCaptureCursorForLook() + { + return GetRegistry(CameraCaptureCursorLookSetting, true); + } + + void SetCameraCaptureCursorForLook(const bool capture) + { + SetRegistry(CameraCaptureCursorLookSetting, capture); + } + AzFramework::InputChannelId CameraTranslateForwardChannelId() { return AzFramework::InputChannelId( diff --git a/Code/Editor/EditorViewportSettings.h b/Code/Editor/EditorViewportSettings.h index 1aca51395f..83004d52af 100644 --- a/Code/Editor/EditorViewportSettings.h +++ b/Code/Editor/EditorViewportSettings.h @@ -86,6 +86,9 @@ namespace SandboxEditor SANDBOX_API bool CameraTranslateSmoothingEnabled(); SANDBOX_API void SetCameraTranslateSmoothingEnabled(bool enabled); + SANDBOX_API bool CameraCaptureCursorForLook(); + SANDBOX_API void SetCameraCaptureCursorForLook(bool capture); + SANDBOX_API AzFramework::InputChannelId CameraTranslateForwardChannelId(); SANDBOX_API void SetCameraTranslateForwardChannelId(AZStd::string_view cameraTranslateForwardId); diff --git a/Code/Editor/EditorViewportWidget.cpp b/Code/Editor/EditorViewportWidget.cpp index ec965cc51f..779c0093a0 100644 --- a/Code/Editor/EditorViewportWidget.cpp +++ b/Code/Editor/EditorViewportWidget.cpp @@ -104,7 +104,6 @@ AZ_CVAR( bool, ed_visibility_logTiming, false, nullptr, AZ::ConsoleFunctorFlags::Null, "Output the timing of the new IVisibilitySystem query"); -AZ_CVAR(bool, ed_showCursorCameraLook, true, nullptr, AZ::ConsoleFunctorFlags::Null, "Show the cursor when using free look with the new camera system"); EditorViewportWidget* EditorViewportWidget::m_pPrimaryViewport = nullptr; @@ -1079,13 +1078,19 @@ AZStd::shared_ptr CreateMod { const auto hideCursor = [viewportId] { - AzToolsFramework::ViewportInteraction::ViewportMouseCursorRequestBus::Event( - viewportId, &AzToolsFramework::ViewportInteraction::ViewportMouseCursorRequestBus::Events::BeginCursorCapture); + if (SandboxEditor::CameraCaptureCursorForLook()) + { + AzToolsFramework::ViewportInteraction::ViewportMouseCursorRequestBus::Event( + viewportId, &AzToolsFramework::ViewportInteraction::ViewportMouseCursorRequestBus::Events::BeginCursorCapture); + } }; const auto showCursor = [viewportId] { - AzToolsFramework::ViewportInteraction::ViewportMouseCursorRequestBus::Event( - viewportId, &AzToolsFramework::ViewportInteraction::ViewportMouseCursorRequestBus::Events::EndCursorCapture); + if (SandboxEditor::CameraCaptureCursorForLook()) + { + AzToolsFramework::ViewportInteraction::ViewportMouseCursorRequestBus::Event( + viewportId, &AzToolsFramework::ViewportInteraction::ViewportMouseCursorRequestBus::Events::EndCursorCapture); + } }; auto firstPersonRotateCamera = AZStd::make_shared(SandboxEditor::CameraFreeLookChannelId()); @@ -1094,12 +1099,10 @@ AZStd::shared_ptr CreateMod return SandboxEditor::CameraRotateSpeed(); }; - if (!ed_showCursorCameraLook) - { - // default behavior is to hide the cursor but this can be disabled (useful for remote desktop) - firstPersonRotateCamera->SetActivationBeganFn(hideCursor); - firstPersonRotateCamera->SetActivationEndedFn(showCursor); - } + // default behavior is to hide the cursor but this can be disabled (useful for remote desktop) + // note: See CaptureCursorLook in the Settings Registry + firstPersonRotateCamera->SetActivationBeganFn(hideCursor); + firstPersonRotateCamera->SetActivationEndedFn(showCursor); auto firstPersonPanCamera = AZStd::make_shared(SandboxEditor::CameraFreePanChannelId(), AzFramework::LookPan); diff --git a/Code/Editor/Lib/Tests/test_ModularViewportCameraController.cpp b/Code/Editor/Lib/Tests/test_ModularViewportCameraController.cpp index 28bc6fe3b3..7309110c4d 100644 --- a/Code/Editor/Lib/Tests/test_ModularViewportCameraController.cpp +++ b/Code/Editor/Lib/Tests/test_ModularViewportCameraController.cpp @@ -7,6 +7,7 @@ */ #include +#include #include #include #include @@ -95,10 +96,16 @@ namespace UnitTest m_controllerList->RegisterViewportContext(TestViewportId); m_inputChannelMapper = AZStd::make_unique(m_rootWidget.get(), TestViewportId); + + m_settingsRegistry = AZStd::make_unique(); + AZ::SettingsRegistry::Register(m_settingsRegistry.get()); } void TearDown() override { + AZ::SettingsRegistry::Unregister(m_settingsRegistry.get()); + m_settingsRegistry.reset(); + m_inputChannelMapper.reset(); m_controllerList->UnregisterViewportContext(TestViewportId); @@ -170,7 +177,7 @@ namespace UnitTest void RepeatDiagonalMouseMovements(const AZStd::function& deltaTimeFn) { // move to the center of the screen - auto start = QPoint(WidgetSize.width() / 2, WidgetSize.height() / 2); + const auto start = QPoint(WidgetSize.width() / 2, WidgetSize.height() / 2); MouseMove(m_rootWidget.get(), start, QPoint(0, 0)); m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(deltaTimeFn()), AZ::ScriptTimePoint() }); @@ -204,12 +211,15 @@ namespace UnitTest ::testing::NiceMock m_mockWindowRequests; ViewportMouseCursorRequestImpl m_viewportMouseCursorRequests; AtomToolsFramework::ModularCameraViewportContext* m_cameraViewportContextView = nullptr; + AZStd::unique_ptr m_settingsRegistry; }; const AzFramework::ViewportId ModularViewportCameraControllerFixture::TestViewportId = AzFramework::ViewportId(0); TEST_F(ModularViewportCameraControllerFixture, MouseMovementDoesNotAccumulateExcessiveDriftInModularViewportCameraWithVaryingDeltaTime) { + SandboxEditor::SetCameraCaptureCursorForLook(false); + // Given PrepareCollaborators(); @@ -242,6 +252,8 @@ namespace UnitTest ModularViewportCameraControllerDeltaTimeParamFixture, MouseMovementDoesNotAccumulateExcessiveDriftInModularViewportCameraWithFixedDeltaTime) { + SandboxEditor::SetCameraCaptureCursorForLook(false); + // Given PrepareCollaborators(); @@ -263,4 +275,92 @@ namespace UnitTest INSTANTIATE_TEST_CASE_P( All, ModularViewportCameraControllerDeltaTimeParamFixture, testing::Values(1.0f / 60.0f, 1.0f / 50.0f, 1.0f / 30.0f)); + + TEST_F(ModularViewportCameraControllerFixture, MouseMovementOrientatesCameraWhenCursorIsCaptured) + { + // Given + PrepareCollaborators(); + // ensure cursor is captured + SandboxEditor::SetCameraCaptureCursorForLook(true); + + const float deltaTime = 1.0f / 60.0f; + + // When + // move to the center of the screen + auto start = QPoint(WidgetSize.width() / 2, WidgetSize.height() / 2); + MouseMove(m_rootWidget.get(), start, QPoint(0, 0)); + m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(deltaTime), AZ::ScriptTimePoint() }); + + const auto mouseDelta = QPoint(5, 0); + + // initial movement to begin the camera behavior + MousePressAndMove(m_rootWidget.get(), start, mouseDelta, Qt::MouseButton::RightButton); + m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(deltaTime), AZ::ScriptTimePoint() }); + + // move the cursor right + for (int i = 0; i < 50; ++i) + { + MousePressAndMove(m_rootWidget.get(), start + mouseDelta, mouseDelta, Qt::MouseButton::RightButton); + m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(deltaTime), AZ::ScriptTimePoint() }); + } + + // move the cursor left (do an extra iteration moving left to account for the initial dead-zone) + for (int i = 0; i < 51; ++i) + { + MousePressAndMove(m_rootWidget.get(), start + mouseDelta, -mouseDelta, Qt::MouseButton::RightButton); + m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(deltaTime), AZ::ScriptTimePoint() }); + } + + QTest::mouseRelease(m_rootWidget.get(), Qt::MouseButton::RightButton, Qt::KeyboardModifier::NoModifier, start + mouseDelta); + m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(deltaTime), AZ::ScriptTimePoint() }); + + // Then + // retrieve the amount of yaw rotation + const AZ::Quaternion cameraRotation = m_cameraViewportContextView->GetCameraTransform().GetRotation(); + const auto eulerAngles = AzFramework::EulerAngles(AZ::Matrix3x3::CreateFromQuaternion(cameraRotation)); + + // camera should be back at the center (no yaw) + using ::testing::FloatNear; + EXPECT_THAT(eulerAngles.GetZ(), FloatNear(0.0f, 0.001f)); + + // Clean-up + HaltCollaborators(); + } + + TEST_F(ModularViewportCameraControllerFixture, CameraDoesNotContinueToRotateGivenNoInputWhenCaptured) + { + // Given + PrepareCollaborators(); + SandboxEditor::SetCameraCaptureCursorForLook(true); + + const float deltaTime = 1.0f / 60.0f; + + // When + // move to the center of the screen + auto start = QPoint(WidgetSize.width() / 2, WidgetSize.height() / 2); + MouseMove(m_rootWidget.get(), start, QPoint(0, 0)); + m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(deltaTime), AZ::ScriptTimePoint() }); + + // will move a small amount initially + const auto mouseDelta = QPoint(5, 0); + MousePressAndMove(m_rootWidget.get(), start, mouseDelta, Qt::MouseButton::RightButton); + + // ensure further updates to not continue to rotate + for (int i = 0; i < 50; ++i) + { + m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(deltaTime), AZ::ScriptTimePoint() }); + } + + // Then + // ensure the camera rotation is no longer the identity + const AZ::Quaternion cameraRotation = m_cameraViewportContextView->GetCameraTransform().GetRotation(); + const auto eulerAngles = AzFramework::EulerAngles(AZ::Matrix3x3::CreateFromQuaternion(cameraRotation)); + + // initial amount of rotation after first mouse move + using ::testing::FloatNear; + EXPECT_THAT(eulerAngles.GetZ(), FloatNear(-0.025f, 0.001f)); + + // Clean-up + HaltCollaborators(); + } } // namespace UnitTest diff --git a/Code/Editor/ViewportManipulatorController.cpp b/Code/Editor/ViewportManipulatorController.cpp index a67d733cf9..1766945541 100644 --- a/Code/Editor/ViewportManipulatorController.cpp +++ b/Code/Editor/ViewportManipulatorController.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include @@ -112,9 +113,9 @@ namespace SandboxEditor AzFramework::WindowRequestBus::EventResult( windowSize, event.m_windowHandle, &AzFramework::WindowRequestBus::Events::GetClientAreaSize); - auto screenPoint = AzFramework::ScreenPoint( - static_cast(position->m_normalizedPosition.GetX() * windowSize.m_width), - static_cast(position->m_normalizedPosition.GetY() * windowSize.m_height)); + const auto screenPoint = AzFramework::ScreenPoint( + aznumeric_cast(position->m_normalizedPosition.GetX() * windowSize.m_width), + aznumeric_cast(position->m_normalizedPosition.GetY() * windowSize.m_height)); m_mouseInteraction.m_mousePick.m_screenCoordinates = screenPoint; AZStd::optional ray; @@ -207,20 +208,27 @@ namespace SandboxEditor ? &InteractionBus::Events::InternalHandleMouseManipulatorInteraction : &InteractionBus::Events::InternalHandleMouseViewportInteraction; - const auto mouseInteractionEvent = [mouseInteraction, event = eventType.value(), wheelDelta] { + auto currentCursorState = AzFramework::SystemCursorState::Unknown; + AzFramework::InputSystemCursorRequestBus::EventResult( + currentCursorState, event.m_inputChannel.GetInputDevice().GetInputDeviceId(), + &AzFramework::InputSystemCursorRequestBus::Events::GetSystemCursorState); + + const auto mouseInteractionEvent = [mouseInteraction, event = eventType.value(), wheelDelta, + cursorCaptured = currentCursorState == AzFramework::SystemCursorState::ConstrainedAndHidden] + { switch (event) { case MouseEvent::Up: case MouseEvent::Down: case MouseEvent::Move: case MouseEvent::DoubleClick: - return MouseInteractionEvent(AZStd::move(mouseInteraction), event); + return MouseInteractionEvent(AZStd::move(mouseInteraction), event, cursorCaptured); case MouseEvent::Wheel: return MouseInteractionEvent(AZStd::move(mouseInteraction), wheelDelta); } AZ_Assert(false, "Unhandled MouseEvent"); - return MouseInteractionEvent(MouseInteraction{}, MouseEvent::Up); + return MouseInteractionEvent(MouseInteraction{}, MouseEvent::Up, false); }(); InteractionBus::EventResult( diff --git a/Code/Framework/AzCore/AzCore/std/math.h b/Code/Framework/AzCore/AzCore/std/math.h index 74685cb84e..03f12e6e08 100644 --- a/Code/Framework/AzCore/AzCore/std/math.h +++ b/Code/Framework/AzCore/AzCore/std/math.h @@ -22,6 +22,8 @@ namespace AZStd using std::exp2; using std::floor; using std::fmod; + using std::llround; + using std::lround; using std::pow; using std::round; using std::sin; diff --git a/Code/Framework/AzFramework/AzFramework/Viewport/CameraInput.cpp b/Code/Framework/AzFramework/AzFramework/Viewport/CameraInput.cpp index 72984f8111..fac1e45920 100644 --- a/Code/Framework/AzFramework/AzFramework/Viewport/CameraInput.cpp +++ b/Code/Framework/AzFramework/AzFramework/Viewport/CameraInput.cpp @@ -144,6 +144,7 @@ namespace AzFramework if (const auto& cursor = AZStd::get_if(&event)) { m_cursorState.SetCurrentPosition(cursor->m_position); + m_cursorState.SetCaptured(cursor->m_captured); } else if (const auto& horizontalMotion = AZStd::get_if(&event)) { @@ -790,17 +791,24 @@ namespace AzFramework const auto* position = inputChannel.GetCustomData(); AZ_Assert(position, "Expected PositionData2D but found nullptr"); - return CursorEvent{ ScreenPoint( - static_cast(position->m_normalizedPosition.GetX() * windowSize.m_width), - static_cast(position->m_normalizedPosition.GetY() * windowSize.m_height)) }; + auto currentCursorState = AzFramework::SystemCursorState::Unknown; + AzFramework::InputSystemCursorRequestBus::EventResult( + currentCursorState, inputDeviceId, &AzFramework::InputSystemCursorRequestBus::Events::GetSystemCursorState); + + const auto x = position->m_normalizedPosition.GetX() * aznumeric_cast(windowSize.m_width); + const auto y = position->m_normalizedPosition.GetY() * aznumeric_cast(windowSize.m_height); + return CursorEvent{ ScreenPoint(aznumeric_cast(AZStd::lround(x)), aznumeric_cast(AZStd::lround(y))), + currentCursorState == AzFramework::SystemCursorState::ConstrainedAndHidden }; } else if (inputChannelId == InputDeviceMouse::Movement::X) { - return HorizontalMotionEvent{ aznumeric_cast(inputChannel.GetValue()) }; + const auto x = inputChannel.GetValue(); + return HorizontalMotionEvent{ aznumeric_cast(AZStd::lround(x)) }; } else if (inputChannelId == InputDeviceMouse::Movement::Y) { - return VerticalMotionEvent{ aznumeric_cast(inputChannel.GetValue()) }; + const auto y = inputChannel.GetValue(); + return VerticalMotionEvent{ aznumeric_cast(AZStd::lround(y)) }; } else if (inputChannelId == InputDeviceMouse::Movement::Z) { diff --git a/Code/Framework/AzFramework/AzFramework/Viewport/CameraInput.h b/Code/Framework/AzFramework/AzFramework/Viewport/CameraInput.h index 0b7bbbc30d..ec383fa8ef 100644 --- a/Code/Framework/AzFramework/AzFramework/Viewport/CameraInput.h +++ b/Code/Framework/AzFramework/AzFramework/Viewport/CameraInput.h @@ -88,6 +88,7 @@ namespace AzFramework struct CursorEvent { ScreenPoint m_position; + bool m_captured = false; }; struct ScrollEvent diff --git a/Code/Framework/AzFramework/AzFramework/Viewport/CursorState.h b/Code/Framework/AzFramework/AzFramework/Viewport/CursorState.h index 704383d615..f6474a242e 100644 --- a/Code/Framework/AzFramework/AzFramework/Viewport/CursorState.h +++ b/Code/Framework/AzFramework/AzFramework/Viewport/CursorState.h @@ -21,6 +21,8 @@ namespace AzFramework [[nodiscard]] ScreenVector CursorDelta() const; //! Call this in a 'handle event' call to update the most recent cursor position. void SetCurrentPosition(const ScreenPoint& currentPosition); + //! Set whether the cursor is currently being constrained (and hidden). + void SetCaptured(bool captured); //! Call this in an 'update' call to copy the current cursor position to the last //! cursor position. void Update(); @@ -28,8 +30,14 @@ namespace AzFramework private: AZStd::optional m_lastCursorPosition; AZStd::optional m_currentCursorPosition; + bool m_captured = false; }; + inline void CursorState::SetCaptured(const bool captured) + { + m_captured = captured; + } + inline void CursorState::SetCurrentPosition(const ScreenPoint& currentPosition) { m_currentCursorPosition = currentPosition; @@ -44,9 +52,16 @@ namespace AzFramework inline void CursorState::Update() { - if (m_currentCursorPosition.has_value()) + if (!m_captured) + { + if (m_currentCursorPosition.has_value()) + { + m_lastCursorPosition = m_currentCursorPosition; + } + } + else { - m_lastCursorPosition = m_currentCursorPosition; + m_currentCursorPosition = m_lastCursorPosition; } } } // namespace AzFramework diff --git a/Code/Framework/AzManipulatorTestFramework/Source/AzManipulatorTestFrameworkUtils.cpp b/Code/Framework/AzManipulatorTestFramework/Source/AzManipulatorTestFrameworkUtils.cpp index 25aed336de..1b68a3d0cd 100644 --- a/Code/Framework/AzManipulatorTestFramework/Source/AzManipulatorTestFrameworkUtils.cpp +++ b/Code/Framework/AzManipulatorTestFramework/Source/AzManipulatorTestFrameworkUtils.cpp @@ -122,7 +122,7 @@ namespace AzManipulatorTestFramework MouseInteractionEvent CreateMouseInteractionEvent(const MouseInteraction& mouseInteraction, MouseEvent event) { - return MouseInteractionEvent(mouseInteraction, event); + return MouseInteractionEvent(mouseInteraction, event, /*captured=*/false); } void DispatchMouseInteractionEvent(const MouseInteractionEvent& event) diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputManager.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputManager.cpp index d39ade5527..5fcf7e11d3 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputManager.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputManager.cpp @@ -158,6 +158,16 @@ namespace AzToolsFramework SetImplementation(nullptr); } + void QtEventToAzInputMapper::EditorQtMouseDevice::SetSystemCursorState(const AzFramework::SystemCursorState systemCursorState) + { + m_systemCursorState = systemCursorState; + } + + AzFramework::SystemCursorState QtEventToAzInputMapper::EditorQtMouseDevice::GetSystemCursorState() const + { + return m_systemCursorState; + } + QtEventToAzInputMapper::QtEventToAzInputMapper(QWidget* sourceWidget, int syntheticDeviceId) : QObject(sourceWidget) , m_sourceWidget(sourceWidget) @@ -210,12 +220,15 @@ namespace AzToolsFramework if (m_capturingCursor != enabled) { m_capturingCursor = enabled; + if (m_capturingCursor) { + m_mouseDevice->SetSystemCursorState(AzFramework::SystemCursorState::ConstrainedAndHidden); qApp->setOverrideCursor(Qt::BlankCursor); } else { + m_mouseDevice->SetSystemCursorState(AzFramework::SystemCursorState::UnconstrainedAndVisible); qApp->restoreOverrideCursor(); } } @@ -238,10 +251,22 @@ namespace AzToolsFramework return false; } - // If our focus changes, go ahead and reset all input devices. if (eventType == QEvent::FocusIn || eventType == QEvent::FocusOut) { + // If our focus changes, go ahead and reset all input devices. HandleFocusChange(event); + + // If we focus in on the source widget and the mouse is contained in its + // bounds, refresh the cached cursor position to ensure it is up to date (this + // ensures cursor positions are refreshed correctly with context menu focus changes) + if (eventType == QEvent::FocusIn) + { + const auto widgetCursorPosition = m_sourceWidget->mapFromGlobal(QCursor::pos()); + if (m_sourceWidget->geometry().contains(widgetCursorPosition)) + { + HandleMouseMoveEvent(widgetCursorPosition); + } + } } // Map key events to input channels. // ShortcutOverride is used in lieu of KeyPress for high priority input channels like Alt @@ -249,7 +274,7 @@ namespace AzToolsFramework else if ( eventType == QEvent::Type::KeyPress || eventType == QEvent::Type::KeyRelease || eventType == QEvent::Type::ShortcutOverride) { - QKeyEvent* keyEvent = static_cast(event); + auto keyEvent = static_cast(event); HandleKeyEvent(keyEvent); } // Map mouse events to input channels. @@ -257,20 +282,20 @@ namespace AzToolsFramework eventType == QEvent::Type::MouseButtonPress || eventType == QEvent::Type::MouseButtonRelease || eventType == QEvent::Type::MouseButtonDblClick) { - QMouseEvent* mouseEvent = static_cast(event); + auto mouseEvent = static_cast(event); HandleMouseButtonEvent(mouseEvent); } // Map mouse movement to the movement input channels. // This includes SystemCursorPosition alongside Movement::X and Movement::Y. else if (eventType == QEvent::Type::MouseMove) { - QMouseEvent* mouseEvent = static_cast(event); - HandleMouseMoveEvent(mouseEvent); + auto mouseEvent = static_cast(event); + HandleMouseMoveEvent(mouseEvent->pos()); } // Map wheel events to the mouse Z movement channel. else if (eventType == QEvent::Type::Wheel) { - QWheelEvent* wheelEvent = static_cast(event); + auto wheelEvent = static_cast(event); HandleWheelEvent(wheelEvent); } @@ -345,9 +370,8 @@ namespace AzToolsFramework return QPoint{ denormalizedX, denormalizedY }; } - void QtEventToAzInputMapper::HandleMouseMoveEvent(QMouseEvent* mouseEvent) + void QtEventToAzInputMapper::HandleMouseMoveEvent(const QPoint& cursorPosition) { - const QPoint cursorPosition = mouseEvent->pos(); const QPoint cursorDelta = cursorPosition - m_previousCursorPosition; m_mouseDevice->m_cursorPositionData2D->m_normalizedPosition = WidgetPositionToNormalizedPosition(cursorPosition); @@ -357,17 +381,14 @@ namespace AzToolsFramework if (m_capturingCursor) { - // Reset our cursor position to the previous point. - const QPoint targetScreenPosition = m_sourceWidget->mapToGlobal(m_previousCursorPosition); - AzQtComponents::SetCursorPos(targetScreenPosition); - - // Even though we just set the cursor position, there are edge cases such as remote desktop that will leave - // the cursor position unchanged. For safety, we re-cache our last cursor position for delta generation. - const QPoint actualWidgetPosition = m_sourceWidget->mapFromGlobal(QCursor::pos()); - m_mouseDevice->m_cursorPositionData2D->m_normalizedPosition = WidgetPositionToNormalizedPosition(actualWidgetPosition); + // Reset our cursor position to the previous point + const QPoint screenCursorPosition = m_sourceWidget->mapToGlobal(m_previousCursorPosition); + AzQtComponents::SetCursorPos(screenCursorPosition); + } + else + { + m_previousCursorPosition = cursorPosition; } - - m_previousCursorPosition = cursorPosition; } void QtEventToAzInputMapper::HandleKeyEvent(QKeyEvent* keyEvent) diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputManager.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputManager.h index 6e73bf4f9c..aaf3fb6295 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputManager.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputManager.h @@ -105,7 +105,14 @@ namespace AzToolsFramework public: EditorQtMouseDevice(AzFramework::InputDeviceId id); + // AzFramework::InputDeviceMouse overrides ... + void SetSystemCursorState(AzFramework::SystemCursorState systemCursorState) override; + AzFramework::SystemCursorState GetSystemCursorState() const override; + friend class QtEventToAzInputMapper; + + private: + AzFramework::SystemCursorState m_systemCursorState = AzFramework::SystemCursorState::UnconstrainedAndVisible; }; // Emits InputChannelUpdated if channel has transitioned in state (i.e. has gone from active to inactive or vice versa). @@ -122,7 +129,7 @@ namespace AzToolsFramework // Handle mouse click events. void HandleMouseButtonEvent(QMouseEvent* mouseEvent); // Handle mouse move events. - void HandleMouseMoveEvent(QMouseEvent* mouseEvent); + void HandleMouseMoveEvent(const QPoint& cursorPosition); // Handles key press / release events (or ShortcutOverride events for keys listed in m_highPriorityKeys). void HandleKeyEvent(QKeyEvent* keyEvent); // Handles mouse wheel events. diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UnitTest/AzToolsFrameworkTestHelpers.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/UnitTest/AzToolsFrameworkTestHelpers.cpp index 6608e87784..8d30359562 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UnitTest/AzToolsFrameworkTestHelpers.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UnitTest/AzToolsFrameworkTestHelpers.cpp @@ -115,7 +115,7 @@ namespace UnitTest handled, AzToolsFramework::GetEntityContextId(), &EditorInteractionSystemViewportSelectionRequestBus::Events::InternalHandleMouseViewportInteraction, AzToolsFramework::ViewportInteraction::MouseInteractionEvent( - mouseInteraction, AzToolsFramework::ViewportInteraction::MouseEvent::Down)); + mouseInteraction, AzToolsFramework::ViewportInteraction::MouseEvent::Down, /*captured=*/false)); return handled; } } diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Viewport/EditorContextMenu.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Viewport/EditorContextMenu.cpp index ff13bce531..394aa3fe5c 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Viewport/EditorContextMenu.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Viewport/EditorContextMenu.cpp @@ -41,7 +41,8 @@ namespace AzToolsFramework ViewportInteraction::QPointFromScreenPoint(mouseInteraction.m_mouseInteraction.m_mousePick.m_screenCoordinates); // if the mouse hasn't moved, open the pop-up menu - if ((currentScreenCoords - contextMenu.m_clickPoint).manhattanLength() < ed_contextMenuDisplayThreshold) + if ((currentScreenCoords - contextMenu.m_clickPoint).manhattanLength() < ed_contextMenuDisplayThreshold && + !mouseInteraction.m_captured) { QWidget* parent = nullptr; ViewportInteraction::MainEditorViewportInteractionRequestBus::EventResult( diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Viewport/ViewportTypes.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Viewport/ViewportTypes.h index 1cfb0dbe74..bc1d277609 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Viewport/ViewportTypes.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Viewport/ViewportTypes.h @@ -212,9 +212,10 @@ namespace AzToolsFramework static void Reflect(AZ::SerializeContext& context); //! Constructor to create a default MouseInteractionEvent - MouseInteractionEvent(MouseInteraction mouseInteraction, const MouseEvent mouseEvent) + MouseInteractionEvent(MouseInteraction mouseInteraction, const MouseEvent mouseEvent, const bool captured) : m_mouseInteraction(std::move(mouseInteraction)) , m_mouseEvent(mouseEvent) + , m_captured(captured) { } @@ -228,6 +229,7 @@ namespace AzToolsFramework MouseInteraction m_mouseInteraction; //!< Mouse state. MouseEvent m_mouseEvent; //!< Mouse event. + bool m_captured = false; //!< Is the mouse cursor being captured during the event. //! Special friend function to return the mouse wheel delta (scroll amount) //! if the event was of type MouseEvent::Wheel.