-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/Mouse/InputDeviceMouse.h>
#include <QApplication>
#include <QCursor>
#include <QEvent>
#include <QKeyEvent>
@ -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<AzFramework::ModifierKeyStates>())
, m_cursorPosition(AZStd::make_shared<AzFramework::InputChannel::PositionData2D>())
, 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<AzFramework::InputChannel*> QtEventToAzInputMapper::MapQtEventToAzInput(QEvent* event)
bool QtEventToAzInputMapper::HandlesInputEvent(const AzFramework::InputChannel& channel) const
{
AZStd::vector<AzFramework::InputChannel*> 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::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);
};
// 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<QKeyEvent*>(event);
const Qt::Key key = static_cast<Qt::Key>(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<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.
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<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);
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<QMouseEvent*>(event);
const Qt::MouseButton button = mouseEvent->button();
}
void QtEventToAzInputMapper::HandleMouseMoveEvent(QMouseEvent* mouseEvent)
{
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
{
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<QMouseEvent*>(event);
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;
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::InputChannelDeltaWithSharedPosition2D>(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<QWheelEvent*>(event);
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;
wheelAngle = angleDelta.y();
}
return mappedChannels;
cursorZChannel->ProcessRawInputEvent(aznumeric_cast<float>(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

@ -12,37 +12,52 @@
#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/unique_ptr.h>
#include <AzFramework/Input/Channels/InputChannel.h>
#include <AzFramework/Input/Channels/InputChannelDeltaWithSharedPosition2D.h>
#include <AzFramework/Input/Channels/InputChannelDigitalWithSharedModifierKeyStates.h>
#include <AzFramework/Input/Channels/InputChannelDigitalWithSharedPosition2D.h>
#include <AzFramework/Input/Channels/InputChannelDeltaWithSharedPosition2D.h>
#include <QEvent>
#include <QObject>
#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<AzFramework::InputChannel*> 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 <class TInputChannel>
template<class TInputChannel>
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<AZStd::pair<Qt::Key, AzFramework::InputChannelId>, 91> QtKeyMappings;

@ -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<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)
@ -201,22 +216,7 @@ namespace AtomToolsFramework
SendWindowResizeEvent();
}
bool eventHandled = false;
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;
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
{

Loading…
Cancel
Save