-Rework input handling to filter through a global event filter

-Attempt to support remote desktop for viewport cursor capture

Signed-off-by: nvsickle <nvsickle@amazon.com>
monroegm-disable-blank-issue-2
nvsickle 5 years ago
parent a4a038cb28
commit f4c96fc9df

@ -20,6 +20,7 @@
#include <AzFramework/Input/Devices/Keyboard/InputDeviceKeyboard.h> #include <AzFramework/Input/Devices/Keyboard/InputDeviceKeyboard.h>
#include <AzFramework/Input/Devices/Mouse/InputDeviceMouse.h> #include <AzFramework/Input/Devices/Mouse/InputDeviceMouse.h>
#include <QApplication>
#include <QCursor> #include <QCursor>
#include <QEvent> #include <QEvent>
#include <QKeyEvent> #include <QKeyEvent>
@ -141,7 +142,8 @@ namespace AzToolsFramework
Qt::Key_Super_R }; Qt::Key_Super_R };
QtEventToAzInputMapper::QtEventToAzInputMapper(QWidget* sourceWidget) QtEventToAzInputMapper::QtEventToAzInputMapper(QWidget* sourceWidget)
: m_sourceWidget(sourceWidget) : QObject(sourceWidget)
, m_sourceWidget(sourceWidget)
, m_keyboardModifiers(AZStd::make_shared<AzFramework::ModifierKeyStates>()) , m_keyboardModifiers(AZStd::make_shared<AzFramework::ModifierKeyStates>())
, m_cursorPosition(AZStd::make_shared<AzFramework::InputChannel::PositionData2D>()) , m_cursorPosition(AZStd::make_shared<AzFramework::InputChannel::PositionData2D>())
, m_keyMappings(QtKeyMappings.begin(), QtKeyMappings.end()) , m_keyMappings(QtKeyMappings.begin(), QtKeyMappings.end())
@ -181,152 +183,199 @@ namespace AzToolsFramework
channel->SetUpdateEventsEnabled(false); channel->SetUpdateEventsEnabled(false);
m_channels.emplace(id, AZStd::move(channel)); 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<AzFramework::InputChannel*> QtEventToAzInputMapper::MapQtEventToAzInput(QEvent* event) bool QtEventToAzInputMapper::HandlesInputEvent(const AzFramework::InputChannel& channel) const
{ {
AZStd::vector<AzFramework::InputChannel*> mappedChannels; // We map keyboard and mouse events from Qt, so flag all events coming from those devices
// as handled by our synthetic event system.
// If our focus changes, go ahead and reset all input devices. const AzFramework::InputDeviceId& deviceId = channel.GetInputDevice().GetInputDeviceId();
if (event->type() == QEvent::FocusIn || event->type() == QEvent::FocusOut) 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->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::InputChannelDeltaWithSharedPosition2D>(AzFramework::InputDeviceMouse::SystemCursorPosition);
auto cursorXChannel =
GetInputChannel<AzFramework::InputChannelDeltaWithSharedPosition2D>(AzFramework::InputDeviceMouse::Movement::X);
auto cursorYChannel =
GetInputChannel<AzFramework::InputChannelDeltaWithSharedPosition2D>(AzFramework::InputDeviceMouse::Movement::Y);
auto cursorZChannel =
GetInputChannel<AzFramework::InputChannelDeltaWithSharedPosition2D>(AzFramework::InputDeviceMouse::Movement::Z);
systemCursorChannel->ProcessRawInputEvent(m_cursorPosition->m_normalizedPositionDelta.GetLength());
cursorXChannel->ProcessRawInputEvent(
m_cursorPosition->m_normalizedPositionDelta.GetX() * aznumeric_cast<float>(m_sourceWidget->width()));
cursorYChannel->ProcessRawInputEvent(
m_cursorPosition->m_normalizedPositionDelta.GetY() * aznumeric_cast<float>(m_sourceWidget->height()));
cursorZChannel->ProcessRawInputEvent(0.f);
mappedChannels.push_back(systemCursorChannel);
mappedChannels.push_back(cursorXChannel);
mappedChannels.push_back(cursorYChannel);
mappedChannels.push_back(cursorZChannel);
};
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 // 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. // during the next processed non-mouse event.
if (m_mouseChannelsNeedUpdate && event->type() != QEvent::Type::MouseMove && event->type() != QEvent::Type::Wheel) if (m_mouseChannelsNeedUpdate && event->type() != QEvent::Type::MouseMove && event->type() != QEvent::Type::Wheel)
{ {
m_cursorPosition->m_normalizedPositionDelta = AZ::Vector2::CreateZero(); m_cursorPosition->m_normalizedPositionDelta = AZ::Vector2::CreateZero();
processMouseMoveChannels(); ProcessPendingMouseEvents();
m_mouseChannelsNeedUpdate = false; 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. // Map key events to input channels.
// ShortcutOverride is used in lieu of KeyPress for high priority input channels like Alt // 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. // 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) event->type() == QEvent::Type::ShortcutOverride)
{ {
QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event); QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event);
const Qt::Key key = static_cast<Qt::Key>(keyEvent->key()); HandleKeyEvent(keyEvent);
const auto modifiers = keyEvent->modifiers(); }
// Map mouse events to input channels.
else if (event->type() == QEvent::Type::MouseButtonPress || event->type() == QEvent::Type::MouseButtonRelease)
{
QMouseEvent* mouseEvent = static_cast<QMouseEvent*>(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<QMouseEvent*>(event);
HandleMouseMoveEvent(mouseEvent);
}
// Map wheel events to the mouse Z movement channel.
else if (event->type() == QEvent::Type::Wheel)
{
QWheelEvent* wheelEvent = static_cast<QWheelEvent*>(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::InputChannelDeltaWithSharedPosition2D>(AzFramework::InputDeviceMouse::SystemCursorPosition);
auto cursorXChannel =
GetInputChannel<AzFramework::InputChannelDeltaWithSharedPosition2D>(AzFramework::InputDeviceMouse::Movement::X);
auto cursorYChannel =
GetInputChannel<AzFramework::InputChannelDeltaWithSharedPosition2D>(AzFramework::InputDeviceMouse::Movement::Y);
auto cursorZChannel =
GetInputChannel<AzFramework::InputChannelDeltaWithSharedPosition2D>(AzFramework::InputDeviceMouse::Movement::Z);
systemCursorChannel->ProcessRawInputEvent(m_cursorPosition->m_normalizedPositionDelta.GetLength());
cursorXChannel->ProcessRawInputEvent(
m_cursorPosition->m_normalizedPositionDelta.GetX() * aznumeric_cast<float>(m_sourceWidget->width()));
cursorYChannel->ProcessRawInputEvent(
m_cursorPosition->m_normalizedPositionDelta.GetY() * aznumeric_cast<float>(m_sourceWidget->height()));
cursorZChannel->ProcessRawInputEvent(0.f);
// For ShortcutEvent, only continue processing if we're in the HighPriorityKeys set. NotifyUpdateChannelIfNotIdle(systemCursorChannel, nullptr);
if (event->type() != QEvent::Type::ShortcutOverride || NotifyUpdateChannelIfNotIdle(cursorXChannel, nullptr);
AZStd::find(HighPriorityKeys.begin(), HighPriorityKeys.end(), key) != HighPriorityKeys.end()) 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<AzFramework::InputChannelDigitalWithSharedPosition2D>(buttonIt->second);
if (buttonChannel)
{ {
if (auto keyIt = m_keyMappings.find(key); keyIt != m_keyMappings.end()) if (mouseEvent->type() == QEvent::Type::MouseButtonPress)
{ {
auto keyChannel = GetInputChannel<AzFramework::InputChannelDigitalWithSharedModifierKeyStates>(keyIt->second); buttonChannel->UpdateState(true);
if (keyChannel)
{
if (event->type() == QEvent::Type::KeyPress || event->type() == QEvent::Type::ShortcutOverride)
{
keyChannel->UpdateState(true);
}
else
{
keyChannel->UpdateState(false);
}
mappedChannels.push_back(keyChannel);
}
} }
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)
{ void QtEventToAzInputMapper::HandleMouseMoveEvent(QMouseEvent* mouseEvent)
QMouseEvent* mouseEvent = static_cast<QMouseEvent*>(event); {
const Qt::MouseButton button = mouseEvent->button(); const QPoint mousePos = mouseEvent->pos();
const float normalizedX = aznumeric_cast<float>(mousePos.x()) / aznumeric_cast<float>(m_sourceWidget->width());
const float normalizedY = aznumeric_cast<float>(mousePos.y()) / aznumeric_cast<float>(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<Qt::Key>(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<AzFramework::InputChannelDigitalWithSharedPosition2D>(buttonIt->second); auto keyChannel = GetInputChannel<AzFramework::InputChannelDigitalWithSharedModifierKeyStates>(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 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) void QtEventToAzInputMapper::HandleWheelEvent(QWheelEvent* wheelEvent)
{ {
QMouseEvent* mouseEvent = static_cast<QMouseEvent*>(event); auto cursorZChannel =
const QPoint mousePos = mouseEvent->pos(); GetInputChannel<AzFramework::InputChannelDeltaWithSharedPosition2D>(AzFramework::InputDeviceMouse::Movement::Z);
const float normalizedX = aznumeric_cast<float>(mousePos.x()) / aznumeric_cast<float>(m_sourceWidget->width()); const QPoint angleDelta = wheelEvent->angleDelta();
const float normalizedY = aznumeric_cast<float>(mousePos.y()) / aznumeric_cast<float>(m_sourceWidget->height()); // Check both angles, as the alt modifier can change the wheel direction.
const AZ::Vector2 normalizedPosition(normalizedX, normalizedY); int wheelAngle = angleDelta.x();
m_cursorPosition->m_normalizedPositionDelta = normalizedPosition - m_cursorPosition->m_normalizedPosition; if (wheelAngle == 0)
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)
{ {
QWheelEvent* wheelEvent = static_cast<QWheelEvent*>(event); wheelAngle = angleDelta.y();
auto cursorZChannel =
GetInputChannel<AzFramework::InputChannelDeltaWithSharedPosition2D>(AzFramework::InputDeviceMouse::Movement::Z);
const QPoint angleDelta = wheelEvent->angleDelta();
cursorZChannel->ProcessRawInputEvent(aznumeric_cast<float>(angleDelta.y()));
mappedChannels.push_back(cursorZChannel);
m_mouseChannelsNeedUpdate = true;
} }
cursorZChannel->ProcessRawInputEvent(aznumeric_cast<float>(wheelAngle));
return mappedChannels; 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 for (auto& channelData : m_channels)
// as handled by our synthetic event system. {
const AzFramework::InputDeviceId& deviceId = channel.GetInputDevice().GetInputDeviceId(); // If resetting the input device changed the channel state, submit it to the mapped channel list
return deviceId == AzFramework::InputDeviceMouse::Id || deviceId == AzFramework::InputDeviceKeyboard::Id; // for processing.
if (channelData.second->IsActive())
{
channelData.second->UpdateState(false);
NotifyUpdateChannelIfNotIdle(channelData.second.get(), event);
}
}
m_mouseChannelsNeedUpdate = false;
} }
} // namespace AzToolsFramework } // namespace AzToolsFramework

@ -12,37 +12,52 @@
#pragma once #pragma once
#include <AzCore/std/smart_ptr/unique_ptr.h> #if !defined(Q_MOC_RUN)
#include <AzCore/std/smart_ptr/shared_ptr.h> #include <AzCore/std/smart_ptr/shared_ptr.h>
#include <AzCore/std/smart_ptr/unique_ptr.h>
#include <AzFramework/Input/Channels/InputChannel.h> #include <AzFramework/Input/Channels/InputChannel.h>
#include <AzFramework/Input/Channels/InputChannelDeltaWithSharedPosition2D.h>
#include <AzFramework/Input/Channels/InputChannelDigitalWithSharedModifierKeyStates.h> #include <AzFramework/Input/Channels/InputChannelDigitalWithSharedModifierKeyStates.h>
#include <AzFramework/Input/Channels/InputChannelDigitalWithSharedPosition2D.h> #include <AzFramework/Input/Channels/InputChannelDigitalWithSharedPosition2D.h>
#include <AzFramework/Input/Channels/InputChannelDeltaWithSharedPosition2D.h>
#include <QEvent> #include <QEvent>
#include <QObject>
#endif //!defined(Q_MOC_RUN)
class QWidget; class QWidget;
class QKeyEvent;
class QMouseEvent;
class QWheelEvent;
namespace AzToolsFramework namespace AzToolsFramework
{ {
//! Maps events from the Qt input system to synthetic InputChannels in AzFramework //! Maps events from the Qt input system to synthetic InputChannels in AzFramework
//! that can be used by AzFramework::ViewportControllers. //! that can be used by AzFramework::ViewportControllers.
class QtEventToAzInputMapper final class QtEventToAzInputMapper final : public QObject
{ {
Q_OBJECT
public: public:
QtEventToAzInputMapper(QWidget* sourceWidget); QtEventToAzInputMapper(QWidget* sourceWidget);
~QtEventToAzInputMapper() = default; ~QtEventToAzInputMapper() = default;
//! Maps a Qt event to any relevant input channels
//! \returns A vector containing all InputChannels that have changed state.
AZStd::vector<AzFramework::InputChannel*> MapQtEventToAzInput(QEvent* event);
//! Queries whether a given input channel has a synthetic equivalent mapped //! Queries whether a given input channel has a synthetic equivalent mapped
//! by this system. //! by this system.
//! \returns true if the channel is handled by MapQtEventToAzInput. //! \returns true if the channel is handled by MapQtEventToAzInput.
bool HandlesInputEvent(const AzFramework::InputChannel& channel) const; 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: private:
template <class TInputChannel> template<class TInputChannel>
TInputChannel* GetInputChannel(const AzFramework::InputChannelId& id) TInputChannel* GetInputChannel(const AzFramework::InputChannelId& id)
{ {
auto channelIt = m_channels.find(id); auto channelIt = m_channels.find(id);
@ -53,6 +68,16 @@ namespace AzToolsFramework
return {}; 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. // Mapping from Qt::Keys to InputChannelIds.
// Used to populate m_keyMappings. // Used to populate m_keyMappings.
static AZStd::array<AZStd::pair<Qt::Key, AzFramework::InputChannelId>, 91> QtKeyMappings; static AZStd::array<AZStd::pair<Qt::Key, AzFramework::InputChannelId>, 91> QtKeyMappings;

@ -38,6 +38,21 @@ namespace AtomToolsFramework
setUpdatesEnabled(false); setUpdatesEnabled(false);
setFocusPolicy(Qt::FocusPolicy::WheelFocus); setFocusPolicy(Qt::FocusPolicy::WheelFocus);
setMouseTracking(true); 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<AzFramework::NativeWindowHandle>(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) bool RenderViewportWidget::InitializeViewportContext(AzFramework::ViewportId id)
@ -201,22 +216,7 @@ namespace AtomToolsFramework
SendWindowResizeEvent(); SendWindowResizeEvent();
} }
bool eventHandled = false; return QWidget::event(event);
for(AzFramework::InputChannel* mappedInputChannel : m_inputChannelMapper.MapQtEventToAzInput(event))
{
AzFramework::NativeWindowHandle windowId = reinterpret_cast<AzFramework::NativeWindowHandle>(winId());
if (m_controllerList->HandleInputChannelEvent({GetId(), windowId, *mappedInputChannel}))
{
eventHandled = true;
}
}
if (eventHandled)
{
event->setAccepted(true);
}
return QWidget::event(event) || eventHandled;
} }
void RenderViewportWidget::enterEvent([[maybe_unused]] QEvent* event) void RenderViewportWidget::enterEvent([[maybe_unused]] QEvent* event)
@ -236,6 +236,9 @@ namespace AtomToolsFramework
if (m_capturingCursor && m_lastCursorPosition.has_value()) if (m_capturingCursor && m_lastCursorPosition.has_value())
{ {
AzQtComponents::SetCursorPos(m_lastCursorPosition.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 else
{ {

Loading…
Cancel
Save