diff --git a/Code/Editor/Lib/Tests/test_ViewportManipulatorController.cpp b/Code/Editor/Lib/Tests/test_ViewportManipulatorController.cpp index 8c7023634e..63e59ea940 100644 --- a/Code/Editor/Lib/Tests/test_ViewportManipulatorController.cpp +++ b/Code/Editor/Lib/Tests/test_ViewportManipulatorController.cpp @@ -85,6 +85,7 @@ namespace UnitTest m_rootWidget = AZStd::make_unique(); m_rootWidget->setFixedSize(QSize(100, 100)); + QApplication::setActiveWindow(m_rootWidget.get()); m_controllerList = AZStd::make_shared(); m_controllerList->RegisterViewportContext(TestViewportId); @@ -100,6 +101,8 @@ namespace UnitTest m_controllerList.reset(); m_rootWidget.reset(); + QApplication::setActiveWindow(nullptr); + AllocatorsTestFixture::TearDown(); } @@ -110,7 +113,7 @@ namespace UnitTest const AzFramework::ViewportId ViewportManipulatorControllerFixture::TestViewportId = AzFramework::ViewportId(0); - TEST_F(ViewportManipulatorControllerFixture, An_event_is_not_propagated_to_the_viewport_when_a_manipulator_handles_it_first) + TEST_F(ViewportManipulatorControllerFixture, AnEventIsNotPropagatedToTheViewportWhenAManipulatorHandlesItFirst) { // forward input events to our controller list QObject::connect( @@ -151,4 +154,77 @@ namespace UnitTest editorInteractionViewportFake.Disconnect(); } + + TEST_F(ViewportManipulatorControllerFixture, ChangingFocusDoesNotClearInput) + { + bool endedEvent = false; + // detect input events and ensure that the Alt key press does not end before the end of the test + QObject::connect( + m_inputChannelMapper.get(), &AzToolsFramework::QtEventToAzInputMapper::InputChannelUpdated, m_rootWidget.get(), + [&endedEvent](const AzFramework::InputChannel* inputChannel, [[maybe_unused]] QEvent* event) + { + if (inputChannel->GetInputChannelId() == AzFramework::InputDeviceKeyboard::Key::ModifierAltL && + inputChannel->IsStateEnded()) + { + endedEvent = true; + } + }); + + // given + auto* secondaryWidget = new QWidget(m_rootWidget.get()); + + m_rootWidget->show(); + secondaryWidget->show(); + + m_rootWidget->setFocus(); + + // simulate a key press when root widget has focus + QTest::keyPress(m_rootWidget.get(), Qt::Key_Alt, Qt::KeyboardModifier::AltModifier); + + // when + // change focus to secondary widget + secondaryWidget->setFocus(); + + // then + // the alt key was not released (cleared) + EXPECT_FALSE(endedEvent); + } + + // note: Application State Change includes events such as switching to another application or minimizing + // the current application + TEST_F(ViewportManipulatorControllerFixture, ApplicationStateChangeDoesClearInput) + { + bool endedEvent = false; + // detect input events and ensure that the Alt key press does not end before the end of the test + QObject::connect( + m_inputChannelMapper.get(), &AzToolsFramework::QtEventToAzInputMapper::InputChannelUpdated, m_rootWidget.get(), + [&endedEvent](const AzFramework::InputChannel* inputChannel, [[maybe_unused]] QEvent* event) + { + if (inputChannel->GetInputChannelId() == AzFramework::InputDeviceKeyboard::Key::AlphanumericW && + inputChannel->IsStateEnded()) + { + endedEvent = true; + } + }); + + // given + auto* secondaryWidget = new QWidget(m_rootWidget.get()); + + m_rootWidget->show(); + secondaryWidget->show(); + + m_rootWidget->setFocus(); + + // simulate a key press when root widget has focus + QTest::keyPress(m_rootWidget.get(), Qt::Key_W); + + // when + // simulate changing the window state + QApplicationStateChangeEvent applicationStateChangeEvent(Qt::ApplicationState::ApplicationInactive); + QCoreApplication::sendEvent(m_rootWidget.get(), &applicationStateChangeEvent); + + // then + // the key was released (cleared) + EXPECT_TRUE(endedEvent); + } } // namespace UnitTest diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputManager.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputManager.cpp index 574470ad6e..7ee20997f9 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputManager.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputManager.cpp @@ -210,8 +210,8 @@ namespace AzToolsFramework m_enabled = enabled; if (!enabled) { - // Send an internal focus change event to reset our input state to fresh if we're disabled. - HandleFocusChange(nullptr); + // Clear input channels to reset our input state if we're disabled. + ClearInputChannels(nullptr); } } @@ -246,7 +246,7 @@ namespace AzToolsFramework if (eventType == QEvent::Type::MouseMove) { - // clear override cursor when moving outside of the viewport + // Clear override cursor when moving outside of the viewport const auto* mouseEvent = static_cast(event); if (m_overrideCursor && !m_sourceWidget->geometry().contains(m_sourceWidget->mapFromGlobal(mouseEvent->globalPos()))) { @@ -255,6 +255,13 @@ namespace AzToolsFramework } } + // If the application state changes (e.g. we have alt-tabbed or minimized the + // main editor window) then ensure all input channels are cleared + if (eventType == QEvent::ApplicationStateChange) + { + ClearInputChannels(event); + } + // Only accept mouse & key release events that originate from an object that is not our target widget, // as we don't want to erroneously intercept user input meant for another component. if (object != m_sourceWidget && eventType != QEvent::Type::KeyRelease && eventType != QEvent::Type::MouseButtonRelease) @@ -264,9 +271,6 @@ namespace AzToolsFramework 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) @@ -451,7 +455,7 @@ namespace AzToolsFramework NotifyUpdateChannelIfNotIdle(cursorZChannel, wheelEvent); } - void QtEventToAzInputMapper::HandleFocusChange(QEvent* event) + void QtEventToAzInputMapper::ClearInputChannels(QEvent* event) { for (auto& channelData : m_channels) { diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputManager.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputManager.h index 4cb391e63b..4c4e09ea05 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputManager.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputManager.h @@ -138,8 +138,9 @@ namespace AzToolsFramework void HandleKeyEvent(QKeyEvent* keyEvent); // Handles mouse wheel events. void HandleWheelEvent(QWheelEvent* wheelEvent); - // Handles focus change events. - void HandleFocusChange(QEvent* event); + + // Clear all input channels (set all channel states to 'ended'). + void ClearInputChannels(QEvent* event); // Populates m_keyMappings. void InitializeKeyMappings();