Merge pull request #5155 from pollend/feat/add-cursor-wrap-mode-viewport
feat: add cursor wrapped mode
This commit is contained in:
@@ -47,12 +47,12 @@ namespace UnitTest
|
||||
|
||||
void ViewportMouseCursorRequestImpl::BeginCursorCapture()
|
||||
{
|
||||
m_inputChannelMapper->SetCursorCaptureEnabled(true);
|
||||
m_inputChannelMapper->SetCursorMode(AzToolsFramework::CursorInputMode::CursorModeCaptured);
|
||||
}
|
||||
|
||||
void ViewportMouseCursorRequestImpl::EndCursorCapture()
|
||||
{
|
||||
m_inputChannelMapper->SetCursorCaptureEnabled(false);
|
||||
m_inputChannelMapper->SetCursorMode(AzToolsFramework::CursorInputMode::CursorModeNone);
|
||||
}
|
||||
|
||||
bool ViewportMouseCursorRequestImpl::IsMouseOver() const
|
||||
|
||||
@@ -241,25 +241,34 @@ namespace AzToolsFramework
|
||||
}
|
||||
}
|
||||
|
||||
void QtEventToAzInputMapper::SetCursorCaptureEnabled(bool enabled)
|
||||
void QtEventToAzInputMapper::SetCursorMode(AzToolsFramework::CursorInputMode mode)
|
||||
{
|
||||
if (m_capturingCursor != enabled)
|
||||
if (mode != m_cursorMode)
|
||||
{
|
||||
m_capturingCursor = enabled;
|
||||
|
||||
if (m_capturingCursor)
|
||||
m_cursorMode = mode;
|
||||
switch (m_cursorMode)
|
||||
{
|
||||
m_mouseDevice->SetSystemCursorState(AzFramework::SystemCursorState::ConstrainedAndHidden);
|
||||
case CursorInputMode::CursorModeCaptured:
|
||||
qApp->setOverrideCursor(Qt::BlankCursor);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_mouseDevice->SetSystemCursorState(AzFramework::SystemCursorState::UnconstrainedAndVisible);
|
||||
m_mouseDevice->SetSystemCursorState(AzFramework::SystemCursorState::ConstrainedAndHidden);
|
||||
break;
|
||||
case CursorInputMode::CursorModeWrapped:
|
||||
qApp->restoreOverrideCursor();
|
||||
m_mouseDevice->SetSystemCursorState(AzFramework::SystemCursorState::UnconstrainedAndVisible);
|
||||
break;
|
||||
case CursorInputMode::CursorModeNone:
|
||||
qApp->restoreOverrideCursor();
|
||||
m_mouseDevice->SetSystemCursorState(AzFramework::SystemCursorState::UnconstrainedAndVisible);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void QtEventToAzInputMapper::SetCursorCaptureEnabled(bool enabled)
|
||||
{
|
||||
SetCursorMode(enabled ? CursorInputMode::CursorModeCaptured : CursorInputMode::CursorModeNone);
|
||||
}
|
||||
|
||||
bool QtEventToAzInputMapper::eventFilter(QObject* object, QEvent* event)
|
||||
{
|
||||
// Abort if processing isn't enabled.
|
||||
@@ -436,25 +445,80 @@ namespace AzToolsFramework
|
||||
return QPoint{ denormalizedX, denormalizedY };
|
||||
}
|
||||
|
||||
void WrapCursorX(const QRect& rect, QPoint& point)
|
||||
{
|
||||
if (point.x() < rect.left())
|
||||
{
|
||||
point.setX(rect.right() - 1);
|
||||
}
|
||||
else if (point.x() > rect.right())
|
||||
{
|
||||
point.setX(rect.left() + 1);
|
||||
}
|
||||
}
|
||||
|
||||
void WrapCursorY(const QRect& rect, QPoint& point)
|
||||
{
|
||||
if (point.y() < rect.top())
|
||||
{
|
||||
point.setY(rect.bottom() - 1);
|
||||
}
|
||||
else if (point.y() > rect.bottom())
|
||||
{
|
||||
point.setY(rect.top() + 1);
|
||||
}
|
||||
}
|
||||
|
||||
void QtEventToAzInputMapper::HandleMouseMoveEvent(const QPoint& globalCursorPosition)
|
||||
{
|
||||
const QPoint cursorDelta = globalCursorPosition - m_previousGlobalCursorPosition;
|
||||
QScreen* screen = m_sourceWidget->screen();
|
||||
const QRect widgetRect(m_sourceWidget->mapToGlobal(QPoint(0, 0)), m_sourceWidget->size());
|
||||
|
||||
m_mouseDevice->m_cursorPositionData2D->m_normalizedPosition =
|
||||
WidgetPositionToNormalizedPosition(m_sourceWidget->mapFromGlobal(globalCursorPosition));
|
||||
m_mouseDevice->m_cursorPositionData2D->m_normalizedPositionDelta = WidgetPositionToNormalizedPosition(cursorDelta);
|
||||
|
||||
ProcessPendingMouseEvents(cursorDelta);
|
||||
|
||||
if (m_capturingCursor)
|
||||
switch (m_cursorMode)
|
||||
{
|
||||
// Reset our cursor position to the previous point
|
||||
case CursorInputMode::CursorModeCaptured:
|
||||
AzQtComponents::SetCursorPos(m_previousGlobalCursorPosition);
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
case CursorInputMode::CursorModeWrappedX:
|
||||
case CursorInputMode::CursorModeWrappedY:
|
||||
case CursorInputMode::CursorModeWrapped:
|
||||
{
|
||||
QPoint screenPos(globalCursorPosition);
|
||||
switch (m_cursorMode)
|
||||
{
|
||||
case CursorInputMode::CursorModeWrappedX:
|
||||
WrapCursorX(widgetRect, screenPos);
|
||||
break;
|
||||
case CursorInputMode::CursorModeWrappedY:
|
||||
WrapCursorY(widgetRect, screenPos);
|
||||
break;
|
||||
case CursorInputMode::CursorModeWrapped:
|
||||
WrapCursorX(widgetRect, screenPos);
|
||||
WrapCursorY(widgetRect, screenPos);
|
||||
break;
|
||||
default:
|
||||
// this should never happen
|
||||
AZ_Assert(false, "Invalid Curosr Mode: %i.", m_cursorMode);
|
||||
break;
|
||||
}
|
||||
QCursor::setPos(screen, screenPos);
|
||||
const QPoint screenDelta = globalCursorPosition - screenPos;
|
||||
m_previousGlobalCursorPosition = globalCursorPosition - screenDelta;
|
||||
}
|
||||
break;
|
||||
case CursorInputMode::CursorModeNone:
|
||||
m_previousGlobalCursorPosition = globalCursorPosition;
|
||||
break;
|
||||
default:
|
||||
AZ_Assert(false, "Invalid Curosr Mode: %i.", m_cursorMode);
|
||||
break;
|
||||
}
|
||||
ProcessPendingMouseEvents(cursorDelta);
|
||||
}
|
||||
|
||||
void QtEventToAzInputMapper::HandleKeyEvent(QKeyEvent* keyEvent)
|
||||
|
||||
@@ -32,6 +32,17 @@ class QWheelEvent;
|
||||
|
||||
namespace AzToolsFramework
|
||||
{
|
||||
enum class CursorInputMode {
|
||||
CursorModeNone,
|
||||
CursorModeCaptured, //!< Sets whether or not the cursor should be constrained to the source widget and invisible.
|
||||
//!< Internally, this will reset the cursor position after each move event to ensure movement
|
||||
//!< events don't allow the cursor to escape. This can be used for typical camera controls
|
||||
//!< like a dolly or rotation, where mouse movement is important but cursor location is not.
|
||||
CursorModeWrapped, //!< Flags whether the curser is going to wrap around the soruce widget.
|
||||
CursorModeWrappedX, //!< Flags whether the curser is going to wrap around the soruce widget only on the left and right side.
|
||||
CursorModeWrappedY //!< Flags whether the curser is going to wrap around the soruce widget only on the top and bottom side.
|
||||
};
|
||||
|
||||
//! Maps events from the Qt input system to synthetic InputChannels in AzFramework
|
||||
//! that can be used by AzFramework::ViewportControllers.
|
||||
class QtEventToAzInputMapper final
|
||||
@@ -56,8 +67,12 @@ namespace AzToolsFramework
|
||||
//! Internally, this will reset the cursor position after each move event to ensure movement
|
||||
//! events don't allow the cursor to escape. This can be used for typical camera controls
|
||||
//! like a dolly or rotation, where mouse movement is important but cursor location is not.
|
||||
//! @deprecated Use #SetCursorMode()
|
||||
void SetCursorCaptureEnabled(bool enabled);
|
||||
|
||||
//! Set the cursor mode.
|
||||
void SetCursorMode(AzToolsFramework::CursorInputMode mode);
|
||||
|
||||
void SetOverrideCursor(ViewportInteraction::CursorStyleOverride cursorStyleOverride);
|
||||
void ClearOverrideCursor();
|
||||
|
||||
@@ -176,8 +191,8 @@ namespace AzToolsFramework
|
||||
QWidget* m_sourceWidget;
|
||||
// Flags whether or not Qt events should currently be processed.
|
||||
bool m_enabled = true;
|
||||
// Flags whether or not the cursor is being constrained to the source widget (for invisible mouse movement).
|
||||
bool m_capturingCursor = false;
|
||||
// Controls the cursor behavior.
|
||||
AzToolsFramework::CursorInputMode m_cursorMode = AzToolsFramework::CursorInputMode::CursorModeNone;
|
||||
// Flags whether the cursor has been overridden.
|
||||
bool m_overrideCursor = false;
|
||||
|
||||
|
||||
@@ -44,6 +44,10 @@ namespace UnitTest
|
||||
QObject::connect(m_inputChannelMapper.get(), &AzToolsFramework::QtEventToAzInputMapper::InputChannelUpdated, m_rootWidget.get(),
|
||||
[this]([[maybe_unused]] const AzFramework::InputChannel* inputChannel, QEvent* event)
|
||||
{
|
||||
if(event == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
const QEvent::Type eventType = event->type();
|
||||
|
||||
if (eventType == QEvent::Type::MouseButtonPress ||
|
||||
@@ -96,6 +100,11 @@ namespace UnitTest
|
||||
m_azChannelEvents.push_back(AzEventInfo(inputChannel));
|
||||
hasBeenConsumed = m_captureAzEvents;
|
||||
}
|
||||
else if (inputChannelId == AzFramework::InputDeviceMouse::SystemCursorPosition)
|
||||
{
|
||||
m_azCursorPositions.push_back(*inputChannel.GetCustomData<AzFramework::InputChannel::PositionData2D>());
|
||||
hasBeenConsumed = m_captureAzEvents;
|
||||
}
|
||||
}
|
||||
else if (AzFramework::InputDeviceKeyboard::IsKeyboardDevice(inputDeviceId))
|
||||
{
|
||||
@@ -161,6 +170,7 @@ namespace UnitTest
|
||||
AZStd::vector<QtEventInfo> m_signalEvents;
|
||||
AZStd::vector<AzEventInfo> m_azChannelEvents;
|
||||
AZStd::vector<AZStd::string> m_azTextEvents;
|
||||
AZStd::vector<AzFramework::InputChannel::PositionData2D> m_azCursorPositions;
|
||||
|
||||
bool m_captureAzEvents{ false };
|
||||
bool m_captureTextEvents{ false };
|
||||
@@ -512,4 +522,175 @@ namespace UnitTest
|
||||
return info.param.m_az.GetName();
|
||||
}
|
||||
);
|
||||
|
||||
struct MouseMoveParam
|
||||
{
|
||||
AzToolsFramework::CursorInputMode mode;
|
||||
int iterations;
|
||||
QPoint startPos;
|
||||
QPoint deltaPos;
|
||||
QPoint expectedPos;
|
||||
const char* name;
|
||||
};
|
||||
|
||||
class MoveMoveWrapParamQtEventToAzInputMapperFixture
|
||||
: public QtEventToAzInputMapperFixture
|
||||
, public ::testing::WithParamInterface<MouseMoveParam>
|
||||
{
|
||||
};
|
||||
|
||||
TEST_P(MoveMoveWrapParamQtEventToAzInputMapperFixture, DISABLED_MouseMove_NoAzHandlers_VerifyMouseMovementViewport)
|
||||
{
|
||||
|
||||
// setup
|
||||
const MouseMoveParam mouseMoveParam = GetParam();
|
||||
|
||||
AzFramework::InputChannelNotificationBus::Handler::BusConnect();
|
||||
m_captureAzEvents = true;
|
||||
|
||||
m_rootWidget->move(100, 100);
|
||||
QScreen* screen = m_rootWidget->screen();
|
||||
MouseMove(m_rootWidget.get(), mouseMoveParam.startPos, QPoint(0, 0));
|
||||
|
||||
// given
|
||||
m_inputChannelMapper->SetCursorMode(mouseMoveParam.mode);
|
||||
m_azCursorPositions.clear();
|
||||
for (float i = 0; i < mouseMoveParam.iterations; i++)
|
||||
{
|
||||
MouseMove(m_rootWidget.get(), m_rootWidget->mapFromGlobal(QCursor::pos(screen)), (mouseMoveParam.deltaPos / mouseMoveParam.iterations));
|
||||
}
|
||||
|
||||
AZ::Vector2 accumulatedPosition(0.0f,0.0f);
|
||||
for(const auto& pos: m_azCursorPositions) {
|
||||
accumulatedPosition += (pos.m_normalizedPositionDelta * AZ::Vector2(aznumeric_cast<float>(WidgetSize.width()), aznumeric_cast<float>(WidgetSize.height())));
|
||||
}
|
||||
|
||||
// validate
|
||||
const QPoint endPosition = m_rootWidget->mapFromGlobal(QCursor::pos(screen));
|
||||
EXPECT_NEAR(endPosition.x(), mouseMoveParam.expectedPos.x(), 1.0f);
|
||||
EXPECT_NEAR(endPosition.y(), mouseMoveParam.expectedPos.y(), 1.0f);
|
||||
|
||||
EXPECT_NEAR(accumulatedPosition.GetX(), mouseMoveParam.deltaPos.x(), 1.0f);
|
||||
EXPECT_NEAR(accumulatedPosition.GetY(), mouseMoveParam.deltaPos.y(), 1.0f);
|
||||
|
||||
// cleanup
|
||||
m_rootWidget->move(0, 0);
|
||||
m_inputChannelMapper->SetCursorMode(AzToolsFramework::CursorInputMode::CursorModeNone);
|
||||
AzFramework::InputChannelNotificationBus::Handler::BusDisconnect();
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(All, MoveMoveWrapParamQtEventToAzInputMapperFixture,
|
||||
testing::Values(
|
||||
// verify CursorModeWrappedX wrapping
|
||||
MouseMoveParam {AzToolsFramework::CursorInputMode::CursorModeWrappedX,
|
||||
40,
|
||||
QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() - 20, QtEventToAzInputMapperFixture::WidgetSize.height() / 2),
|
||||
QPoint(40, 0),
|
||||
QPoint(20, QtEventToAzInputMapperFixture::WidgetSize.height() / 2),
|
||||
"CursorModeWrappedX_Test_Right"
|
||||
},
|
||||
MouseMoveParam {AzToolsFramework::CursorInputMode::CursorModeWrappedX,
|
||||
40,
|
||||
QPoint(20, QtEventToAzInputMapperFixture::WidgetSize.height() / 2),
|
||||
QPoint(-40, 0),
|
||||
QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() - 20, QtEventToAzInputMapperFixture::WidgetSize.height()/2),
|
||||
"CursorModeWrappedX_Test_Left"
|
||||
},
|
||||
MouseMoveParam {AzToolsFramework::CursorInputMode::CursorModeWrappedX,
|
||||
40,
|
||||
QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() / 2, 20),
|
||||
QPoint(0, -40),
|
||||
QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() / 2, -20),
|
||||
"CursorModeWrappedX_Test_Top"
|
||||
},
|
||||
MouseMoveParam {AzToolsFramework::CursorInputMode::CursorModeWrappedX,
|
||||
40,
|
||||
QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() / 2, QtEventToAzInputMapperFixture::WidgetSize.height() - 20),
|
||||
QPoint(0, 40),
|
||||
QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() / 2, QtEventToAzInputMapperFixture::WidgetSize.height() + 20),
|
||||
"CursorModeWrappedX_Test_Bottom"
|
||||
},
|
||||
|
||||
// verify CursorModeWrappedY wrapping
|
||||
MouseMoveParam {AzToolsFramework::CursorInputMode::CursorModeWrappedY,
|
||||
40,
|
||||
QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() - 20, QtEventToAzInputMapperFixture::WidgetSize.height()/2),
|
||||
QPoint(40, 0),
|
||||
QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() + 20, QtEventToAzInputMapperFixture::WidgetSize.height()/2),
|
||||
"CursorModeWrappedY_Test_Right"
|
||||
},
|
||||
MouseMoveParam {AzToolsFramework::CursorInputMode::CursorModeWrappedY,
|
||||
40,
|
||||
QPoint(20, QtEventToAzInputMapperFixture::WidgetSize.height() / 2),
|
||||
QPoint(-40, 0),
|
||||
QPoint(-20, QtEventToAzInputMapperFixture::WidgetSize.height() / 2),
|
||||
"CursorModeWrappedY_Test_Left"
|
||||
},
|
||||
MouseMoveParam {AzToolsFramework::CursorInputMode::CursorModeWrappedY,
|
||||
40,
|
||||
QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() / 2, 20),
|
||||
QPoint(0, -40),
|
||||
QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() / 2, QtEventToAzInputMapperFixture::WidgetSize.height() - 20),
|
||||
"CursorModeWrappedY_Test_Top"
|
||||
},
|
||||
MouseMoveParam {AzToolsFramework::CursorInputMode::CursorModeWrappedY,
|
||||
40,
|
||||
QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() / 2, QtEventToAzInputMapperFixture::WidgetSize.height() - 20),
|
||||
QPoint(0, 40),
|
||||
QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() / 2, 20),
|
||||
"CursorModeWrappedY_Test_Bottom"
|
||||
},
|
||||
|
||||
// verify CursorModeWrapped wrapping
|
||||
MouseMoveParam {AzToolsFramework::CursorInputMode::CursorModeWrapped,
|
||||
40,
|
||||
QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() - 20, QtEventToAzInputMapperFixture::WidgetSize.height()/2),
|
||||
QPoint(40, 0),
|
||||
QPoint(20, QtEventToAzInputMapperFixture::WidgetSize.height()/2),
|
||||
"CursorModeWrapped_Test_Right"
|
||||
},
|
||||
MouseMoveParam {AzToolsFramework::CursorInputMode::CursorModeWrapped,
|
||||
40,
|
||||
QPoint(20, QtEventToAzInputMapperFixture::WidgetSize.height() / 2),
|
||||
QPoint(-40, 0),
|
||||
QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() - 20, QtEventToAzInputMapperFixture::WidgetSize.height()/2),
|
||||
"CursorModeWrapped_Test_Left"
|
||||
},
|
||||
MouseMoveParam {AzToolsFramework::CursorInputMode::CursorModeWrapped,
|
||||
40,
|
||||
QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() / 2, 20),
|
||||
QPoint(0, -40),
|
||||
QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() / 2, QtEventToAzInputMapperFixture::WidgetSize.height() - 20),
|
||||
"CursorModeWrapped_Test_Top"
|
||||
},
|
||||
MouseMoveParam {AzToolsFramework::CursorInputMode::CursorModeWrapped,
|
||||
40,
|
||||
QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() / 2, QtEventToAzInputMapperFixture::WidgetSize.height() - 20),
|
||||
QPoint(0, 40),
|
||||
QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() / 2, 20),
|
||||
"CursorModeWrapped_Test_Bottom"
|
||||
},
|
||||
// verify CursorModeCaptured
|
||||
MouseMoveParam {AzToolsFramework::CursorInputMode::CursorModeCaptured,
|
||||
40,
|
||||
QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() / 2, QtEventToAzInputMapperFixture::WidgetSize.height() / 2),
|
||||
QPoint(0, 40),
|
||||
QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() / 2, QtEventToAzInputMapperFixture::WidgetSize.height() / 2),
|
||||
"CursorModeCaptured"
|
||||
},
|
||||
// verify CursorModeNone
|
||||
MouseMoveParam {AzToolsFramework::CursorInputMode::CursorModeNone,
|
||||
40,
|
||||
QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() / 2, QtEventToAzInputMapperFixture::WidgetSize.height() / 2),
|
||||
QPoint(40, 0),
|
||||
QPoint((QtEventToAzInputMapperFixture::WidgetSize.width() / 2) + 40, (QtEventToAzInputMapperFixture::WidgetSize.height() / 2)),
|
||||
"CursorModeNone"
|
||||
}
|
||||
),
|
||||
[](const ::testing::TestParamInfo<MouseMoveParam>& info)
|
||||
{
|
||||
return info.param.name;
|
||||
}
|
||||
);
|
||||
|
||||
} // namespace UnitTest
|
||||
|
||||
@@ -332,12 +332,12 @@ namespace AtomToolsFramework
|
||||
|
||||
void RenderViewportWidget::BeginCursorCapture()
|
||||
{
|
||||
m_inputChannelMapper->SetCursorCaptureEnabled(true);
|
||||
m_inputChannelMapper->SetCursorMode(AzToolsFramework::CursorInputMode::CursorModeCaptured);
|
||||
}
|
||||
|
||||
void RenderViewportWidget::EndCursorCapture()
|
||||
{
|
||||
m_inputChannelMapper->SetCursorCaptureEnabled(false);
|
||||
m_inputChannelMapper->SetCursorMode(AzToolsFramework::CursorInputMode::CursorModeNone);
|
||||
}
|
||||
|
||||
void RenderViewportWidget::SetOverrideCursor(AzToolsFramework::ViewportInteraction::CursorStyleOverride cursorStyleOverride)
|
||||
|
||||
Reference in New Issue
Block a user