diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputManager.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputManager.cpp index f842777fb3..512922aad8 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputManager.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputManager.cpp @@ -20,6 +20,7 @@ #include #include +#include #include #include #include @@ -141,7 +142,8 @@ namespace AzToolsFramework Qt::Key_Super_R }; QtEventToAzInputMapper::QtEventToAzInputMapper(QWidget* sourceWidget) - : m_sourceWidget(sourceWidget) + : QObject(sourceWidget) + , m_sourceWidget(sourceWidget) , m_keyboardModifiers(AZStd::make_shared()) , m_cursorPosition(AZStd::make_shared()) , m_keyMappings(QtKeyMappings.begin(), QtKeyMappings.end()) @@ -181,152 +183,199 @@ namespace AzToolsFramework channel->SetUpdateEventsEnabled(false); m_channels.emplace(id, AZStd::move(channel)); } + + // Install a global event filter to ensure we don't miss mouse and key release events. + QApplication::instance()->installEventFilter(this); } - AZStd::vector QtEventToAzInputMapper::MapQtEventToAzInput(QEvent* event) + bool QtEventToAzInputMapper::HandlesInputEvent(const AzFramework::InputChannel& channel) const { - AZStd::vector mappedChannels; - - // If our focus changes, go ahead and reset all input devices. - if (event->type() == QEvent::FocusIn || event->type() == QEvent::FocusOut) - { - for (auto& channelData : m_channels) - { - // If resetting the input device changed the channel state, submit it to the mapped channel list - // for processing. - if (channelData.second->UpdateState(false)) - { - mappedChannels.push_back(channelData.second.get()); - } - } - m_mouseChannelsNeedUpdate = false; - } - - // Update mouse movement channels based on the cursor delta (if 0, ProcessRawInputEvent will change state to Ended). - auto processMouseMoveChannels = [this, &mappedChannels]() - { - auto systemCursorChannel = - GetInputChannel(AzFramework::InputDeviceMouse::SystemCursorPosition); - auto cursorXChannel = - GetInputChannel(AzFramework::InputDeviceMouse::Movement::X); - auto cursorYChannel = - GetInputChannel(AzFramework::InputDeviceMouse::Movement::Y); - auto cursorZChannel = - GetInputChannel(AzFramework::InputDeviceMouse::Movement::Z); - - systemCursorChannel->ProcessRawInputEvent(m_cursorPosition->m_normalizedPositionDelta.GetLength()); - cursorXChannel->ProcessRawInputEvent( - m_cursorPosition->m_normalizedPositionDelta.GetX() * aznumeric_cast(m_sourceWidget->width())); - cursorYChannel->ProcessRawInputEvent( - m_cursorPosition->m_normalizedPositionDelta.GetY() * aznumeric_cast(m_sourceWidget->height())); - cursorZChannel->ProcessRawInputEvent(0.f); - - mappedChannels.push_back(systemCursorChannel); - mappedChannels.push_back(cursorXChannel); - mappedChannels.push_back(cursorYChannel); - mappedChannels.push_back(cursorZChannel); - }; + // We map keyboard and mouse events from Qt, so flag all events coming from those devices + // as handled by our synthetic event system. + const AzFramework::InputDeviceId& deviceId = channel.GetInputDevice().GetInputDeviceId(); + return deviceId == AzFramework::InputDeviceMouse::Id || deviceId == AzFramework::InputDeviceKeyboard::Id; + } + bool QtEventToAzInputMapper::eventFilter(QObject* object, QEvent* event) + { // Because there's no "end" to mouse movement and wheel events, we reset mouse movement channels that have been opened // during the next processed non-mouse event. if (m_mouseChannelsNeedUpdate && event->type() != QEvent::Type::MouseMove && event->type() != QEvent::Type::Wheel) { m_cursorPosition->m_normalizedPositionDelta = AZ::Vector2::CreateZero(); - processMouseMoveChannels(); + ProcessPendingMouseEvents(); m_mouseChannelsNeedUpdate = false; } + // 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 && event->type() != QEvent::Type::KeyRelease && event->type() != QEvent::Type::MouseButtonRelease) + { + return false; + } + + // If our focus changes, go ahead and reset all input devices. + if (event->type() == QEvent::FocusIn || event->type() == QEvent::FocusOut) + { + HandleFocusChange(event); + } // Map key events to input channels. // ShortcutOverride is used in lieu of KeyPress for high priority input channels like Alt // that need to be accepted and stopped before they bubble up and cause unintended behavior. - if (event->type() == QEvent::Type::KeyPress || event->type() == QEvent::Type::KeyRelease || + else if (event->type() == QEvent::Type::KeyPress || event->type() == QEvent::Type::KeyRelease || event->type() == QEvent::Type::ShortcutOverride) { QKeyEvent* keyEvent = static_cast(event); - const Qt::Key key = static_cast(keyEvent->key()); - const auto modifiers = keyEvent->modifiers(); + HandleKeyEvent(keyEvent); + } + // Map mouse events to input channels. + else if (event->type() == QEvent::Type::MouseButtonPress || event->type() == QEvent::Type::MouseButtonRelease) + { + QMouseEvent* 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 (event->type() == QEvent::Type::MouseMove) + { + QMouseEvent* mouseEvent = static_cast(event); + HandleMouseMoveEvent(mouseEvent); + } + // Map wheel events to the mouse Z movement channel. + else if (event->type() == QEvent::Type::Wheel) + { + QWheelEvent* wheelEvent = static_cast(event); + HandleWheelEvent(wheelEvent); + } + + return false; + } + + void QtEventToAzInputMapper::NotifyUpdateChannelIfNotIdle(const AzFramework::InputChannel* channel, QEvent* event) + { + if (channel->GetState() != AzFramework::InputChannel::State::Idle) + { + emit InputChannelUpdated(channel, event); + } + } + + void QtEventToAzInputMapper::ProcessPendingMouseEvents() + { + auto systemCursorChannel = + GetInputChannel(AzFramework::InputDeviceMouse::SystemCursorPosition); + auto cursorXChannel = + GetInputChannel(AzFramework::InputDeviceMouse::Movement::X); + auto cursorYChannel = + GetInputChannel(AzFramework::InputDeviceMouse::Movement::Y); + auto cursorZChannel = + GetInputChannel(AzFramework::InputDeviceMouse::Movement::Z); + + systemCursorChannel->ProcessRawInputEvent(m_cursorPosition->m_normalizedPositionDelta.GetLength()); + cursorXChannel->ProcessRawInputEvent( + m_cursorPosition->m_normalizedPositionDelta.GetX() * aznumeric_cast(m_sourceWidget->width())); + cursorYChannel->ProcessRawInputEvent( + m_cursorPosition->m_normalizedPositionDelta.GetY() * aznumeric_cast(m_sourceWidget->height())); + cursorZChannel->ProcessRawInputEvent(0.f); - // For ShortcutEvent, only continue processing if we're in the HighPriorityKeys set. - if (event->type() != QEvent::Type::ShortcutOverride || - AZStd::find(HighPriorityKeys.begin(), HighPriorityKeys.end(), key) != HighPriorityKeys.end()) + NotifyUpdateChannelIfNotIdle(systemCursorChannel, nullptr); + NotifyUpdateChannelIfNotIdle(cursorXChannel, nullptr); + NotifyUpdateChannelIfNotIdle(cursorYChannel, nullptr); + NotifyUpdateChannelIfNotIdle(cursorZChannel, nullptr); + } + + void QtEventToAzInputMapper::HandleMouseButtonEvent(QMouseEvent* mouseEvent) + { + const Qt::MouseButton button = mouseEvent->button(); + + if (auto buttonIt = m_mouseButtonMappings.find(button); buttonIt != m_mouseButtonMappings.end()) + { + auto buttonChannel = GetInputChannel(buttonIt->second); + + if (buttonChannel) { - if (auto keyIt = m_keyMappings.find(key); keyIt != m_keyMappings.end()) + if (mouseEvent->type() == QEvent::Type::MouseButtonPress) { - auto keyChannel = GetInputChannel(keyIt->second); - - if (keyChannel) - { - if (event->type() == QEvent::Type::KeyPress || event->type() == QEvent::Type::ShortcutOverride) - { - keyChannel->UpdateState(true); - } - else - { - keyChannel->UpdateState(false); - } - mappedChannels.push_back(keyChannel); - } + buttonChannel->UpdateState(true); } + else + { + buttonChannel->UpdateState(false); + } + + NotifyUpdateChannelIfNotIdle(buttonChannel, mouseEvent); } } - // Map mouse events to input channels. - else if (event->type() == QEvent::Type::MouseButtonPress || event->type() == QEvent::Type::MouseButtonRelease) - { - QMouseEvent* mouseEvent = static_cast(event); - const Qt::MouseButton button = mouseEvent->button(); + } + + void QtEventToAzInputMapper::HandleMouseMoveEvent(QMouseEvent* mouseEvent) + { + const QPoint mousePos = mouseEvent->pos(); + const float normalizedX = aznumeric_cast(mousePos.x()) / aznumeric_cast(m_sourceWidget->width()); + const float normalizedY = aznumeric_cast(mousePos.y()) / aznumeric_cast(m_sourceWidget->height()); + const AZ::Vector2 normalizedPosition(normalizedX, normalizedY); + m_cursorPosition->m_normalizedPositionDelta = normalizedPosition - m_cursorPosition->m_normalizedPosition; + m_cursorPosition->m_normalizedPosition = normalizedPosition; + ProcessPendingMouseEvents(); + m_mouseChannelsNeedUpdate = true; + } + + void QtEventToAzInputMapper::HandleKeyEvent(QKeyEvent* keyEvent) + { + const Qt::Key key = static_cast(keyEvent->key()); - if (auto buttonIt = m_mouseButtonMappings.find(button); buttonIt != m_mouseButtonMappings.end()) + // For ShortcutEvent, only continue processing if we're in the HighPriorityKeys set. + if (keyEvent->type() != QEvent::Type::ShortcutOverride || + AZStd::find(HighPriorityKeys.begin(), HighPriorityKeys.end(), key) != HighPriorityKeys.end()) + { + if (auto keyIt = m_keyMappings.find(key); keyIt != m_keyMappings.end()) { - auto buttonChannel = GetInputChannel(buttonIt->second); + auto keyChannel = GetInputChannel(keyIt->second); - if (buttonChannel) + if (keyChannel) { - if (event->type() == QEvent::Type::MouseButtonPress) + if (keyEvent->type() == QEvent::Type::KeyPress || keyEvent->type() == QEvent::Type::ShortcutOverride) { - buttonChannel->UpdateState(true); + keyChannel->UpdateState(true); } else { - buttonChannel->UpdateState(false); + keyChannel->UpdateState(false); } - mappedChannels.push_back(buttonChannel); + + NotifyUpdateChannelIfNotIdle(keyChannel, keyEvent); } } } - // Map mouse movement to the movement input channels. - // This includes SystemCursorPosition alongside Movement::X and Movement::Y. - else if (event->type() == QEvent::Type::MouseMove) - { - QMouseEvent* mouseEvent = static_cast(event); - const QPoint mousePos = mouseEvent->pos(); - const float normalizedX = aznumeric_cast(mousePos.x()) / aznumeric_cast(m_sourceWidget->width()); - const float normalizedY = aznumeric_cast(mousePos.y()) / aznumeric_cast(m_sourceWidget->height()); - const AZ::Vector2 normalizedPosition(normalizedX, normalizedY); - m_cursorPosition->m_normalizedPositionDelta = normalizedPosition - m_cursorPosition->m_normalizedPosition; - m_cursorPosition->m_normalizedPosition = normalizedPosition; - processMouseMoveChannels(); - m_mouseChannelsNeedUpdate = true; - } - // Map wheel events to the mouse Z movement channel. - else if (event->type() == QEvent::Type::Wheel) + } + + void QtEventToAzInputMapper::HandleWheelEvent(QWheelEvent* wheelEvent) + { + auto cursorZChannel = + GetInputChannel(AzFramework::InputDeviceMouse::Movement::Z); + const QPoint angleDelta = wheelEvent->angleDelta(); + // Check both angles, as the alt modifier can change the wheel direction. + int wheelAngle = angleDelta.x(); + if (wheelAngle == 0) { - QWheelEvent* wheelEvent = static_cast(event); - auto cursorZChannel = - GetInputChannel(AzFramework::InputDeviceMouse::Movement::Z); - const QPoint angleDelta = wheelEvent->angleDelta(); - cursorZChannel->ProcessRawInputEvent(aznumeric_cast(angleDelta.y())); - mappedChannels.push_back(cursorZChannel); - m_mouseChannelsNeedUpdate = true; + wheelAngle = angleDelta.y(); } - - return mappedChannels; + cursorZChannel->ProcessRawInputEvent(aznumeric_cast(wheelAngle)); + NotifyUpdateChannelIfNotIdle(cursorZChannel, wheelEvent); + m_mouseChannelsNeedUpdate = true; } - bool QtEventToAzInputMapper::HandlesInputEvent(const AzFramework::InputChannel& channel) const + void QtEventToAzInputMapper::HandleFocusChange(QEvent* event) { - // We map keyboard and mouse events from Qt, so flag all events coming from those devices - // as handled by our synthetic event system. - const AzFramework::InputDeviceId& deviceId = channel.GetInputDevice().GetInputDeviceId(); - return deviceId == AzFramework::InputDeviceMouse::Id || deviceId == AzFramework::InputDeviceKeyboard::Id; + for (auto& channelData : m_channels) + { + // If resetting the input device changed the channel state, submit it to the mapped channel list + // for processing. + if (channelData.second->IsActive()) + { + channelData.second->UpdateState(false); + NotifyUpdateChannelIfNotIdle(channelData.second.get(), event); + } + } + m_mouseChannelsNeedUpdate = false; } } // namespace AzToolsFramework diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputManager.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputManager.h index 0bfe7d33eb..8ad96e3949 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputManager.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputManager.h @@ -12,37 +12,52 @@ #pragma once -#include +#if !defined(Q_MOC_RUN) #include +#include #include +#include #include #include -#include #include +#include +#endif //!defined(Q_MOC_RUN) class QWidget; +class QKeyEvent; +class QMouseEvent; +class QWheelEvent; namespace AzToolsFramework { //! Maps events from the Qt input system to synthetic InputChannels in AzFramework //! that can be used by AzFramework::ViewportControllers. - class QtEventToAzInputMapper final + class QtEventToAzInputMapper final : public QObject { + Q_OBJECT + public: QtEventToAzInputMapper(QWidget* sourceWidget); ~QtEventToAzInputMapper() = default; - //! Maps a Qt event to any relevant input channels - //! \returns A vector containing all InputChannels that have changed state. - AZStd::vector MapQtEventToAzInput(QEvent* event); //! Queries whether a given input channel has a synthetic equivalent mapped //! by this system. //! \returns true if the channel is handled by MapQtEventToAzInput. bool HandlesInputEvent(const AzFramework::InputChannel& channel) const; + // QObject overrides... + bool eventFilter(QObject* object, QEvent* event) override; + + signals: + //! This signal fires whenever the state of the specified input channel changes. + //! This is determined by Qt events dispatched to the source widget. + //! \param channel The AZ input channel that has been updated. + //! \param event The underlying Qt event that triggered this change, if applicable. + void InputChannelUpdated(const AzFramework::InputChannel* channel, QEvent* event); + private: - template + template TInputChannel* GetInputChannel(const AzFramework::InputChannelId& id) { auto channelIt = m_channels.find(id); @@ -53,6 +68,16 @@ namespace AzToolsFramework return {}; } + void NotifyUpdateChannelIfNotIdle(const AzFramework::InputChannel* channel, QEvent* event); + + void ProcessPendingMouseEvents(); + + void HandleMouseButtonEvent(QMouseEvent* mouseEvent); + void HandleMouseMoveEvent(QMouseEvent* mouseEvent); + void HandleKeyEvent(QKeyEvent* keyEvent); + void HandleWheelEvent(QWheelEvent* wheelEvent); + void HandleFocusChange(QEvent* event); + // Mapping from Qt::Keys to InputChannelIds. // Used to populate m_keyMappings. static AZStd::array, 91> QtKeyMappings; diff --git a/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Viewport/RenderViewportWidget.cpp b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Viewport/RenderViewportWidget.cpp index d1702d52df..8488fb574a 100644 --- a/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Viewport/RenderViewportWidget.cpp +++ b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Viewport/RenderViewportWidget.cpp @@ -38,6 +38,21 @@ namespace AtomToolsFramework setUpdatesEnabled(false); setFocusPolicy(Qt::FocusPolicy::WheelFocus); setMouseTracking(true); + + // Forward input events to our controller list. + QObject::connect(&m_inputChannelMapper, &AzToolsFramework::QtEventToAzInputMapper::InputChannelUpdated, this, + [this](const AzFramework::InputChannel* inputChannel, QEvent* event) + { + AzFramework::NativeWindowHandle windowId = reinterpret_cast(winId()); + if (m_controllerList->HandleInputChannelEvent({GetId(), windowId, *inputChannel})) + { + // If the controller handled the input event, mark the event as accepted so it doesn't continue to propagate. + if (event) + { + event->setAccepted(true); + } + } + }); } bool RenderViewportWidget::InitializeViewportContext(AzFramework::ViewportId id) @@ -201,22 +216,7 @@ namespace AtomToolsFramework SendWindowResizeEvent(); } - bool eventHandled = false; - for(AzFramework::InputChannel* mappedInputChannel : m_inputChannelMapper.MapQtEventToAzInput(event)) - { - AzFramework::NativeWindowHandle windowId = reinterpret_cast(winId()); - if (m_controllerList->HandleInputChannelEvent({GetId(), windowId, *mappedInputChannel})) - { - eventHandled = true; - } - } - - if (eventHandled) - { - event->setAccepted(true); - } - - return QWidget::event(event) || eventHandled; + return QWidget::event(event); } void RenderViewportWidget::enterEvent([[maybe_unused]] QEvent* event) @@ -236,6 +236,9 @@ namespace AtomToolsFramework if (m_capturingCursor && m_lastCursorPosition.has_value()) { AzQtComponents::SetCursorPos(m_lastCursorPosition.value()); + // 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. + m_lastCursorPosition = QCursor::pos(); } else {