Implement Cursor Locking and new input focus checks for it

This commit is contained in:
Filoppi
2021-05-09 13:28:04 +03:00
parent ff08b85740
commit 3c7c2dfaa1
25 changed files with 400 additions and 15 deletions

View File

@ -32,9 +32,16 @@
#include "DolphinQt/Resources.h"
#include "DolphinQt/Settings.h"
#include "InputCommon/ControllerInterface/ControllerInterface.h"
#include "VideoCommon/RenderBase.h"
#include "VideoCommon/VideoConfig.h"
#ifdef _WIN32
#include <WinUser.h>
#include <windef.h>
#endif
RenderWidget::RenderWidget(QWidget* parent) : QWidget(parent)
{
setWindowTitle(QStringLiteral("Dolphin"));
@ -79,7 +86,10 @@ RenderWidget::RenderWidget(QWidget* parent) : QWidget(parent)
connect(&Settings::Instance(), &Settings::HideCursorChanged, this,
&RenderWidget::OnHideCursorChanged);
connect(&Settings::Instance(), &Settings::LockCursorChanged, this,
&RenderWidget::OnLockCursorChanged);
OnHideCursorChanged();
OnLockCursorChanged();
connect(&Settings::Instance(), &Settings::KeepWindowOnTopChanged, this,
&RenderWidget::OnKeepOnTopChanged);
OnKeepOnTopChanged(Settings::Instance().IsKeepWindowOnTopEnabled());
@ -128,7 +138,33 @@ void RenderWidget::dropEvent(QDropEvent* event)
void RenderWidget::OnHideCursorChanged()
{
setCursor(Settings::Instance().GetHideCursor() ? Qt::BlankCursor : Qt::ArrowCursor);
UpdateCursor();
}
void RenderWidget::OnLockCursorChanged()
{
SetCursorLocked(false);
UpdateCursor();
}
// Calling this at any time will set the cursor (image) to the correct state
void RenderWidget::UpdateCursor()
{
if (!Settings::Instance().GetLockCursor())
{
// Only hide if the cursor is automatically locking (it will hide on lock).
// "Unhide" the cursor if we lost focus, otherwise it will disappear when hovering
// on top of the game window in the background
const bool keep_on_top = (windowFlags() & Qt::WindowStaysOnTopHint) != 0;
const bool should_hide =
Settings::Instance().GetHideCursor() &&
(keep_on_top || SConfig::GetInstance().m_BackgroundInput || isActiveWindow());
setCursor(should_hide ? Qt::BlankCursor : Qt::ArrowCursor);
}
else
{
setCursor((m_cursor_locked && Settings::Instance().GetHideCursor()) ? Qt::BlankCursor :
Qt::ArrowCursor);
}
}
void RenderWidget::OnKeepOnTopChanged(bool top)
@ -138,14 +174,22 @@ void RenderWidget::OnKeepOnTopChanged(bool top)
setWindowFlags(top ? windowFlags() | Qt::WindowStaysOnTopHint :
windowFlags() & ~Qt::WindowStaysOnTopHint);
m_dont_lock_cursor_on_show = true;
if (was_visible)
show();
m_dont_lock_cursor_on_show = false;
UpdateCursor();
}
void RenderWidget::HandleCursorTimer()
{
if (isActiveWindow())
if (!isActiveWindow())
return;
if (!Settings::Instance().GetLockCursor() || m_cursor_locked)
{
setCursor(Qt::BlankCursor);
}
}
void RenderWidget::showFullScreen()
@ -159,6 +203,138 @@ void RenderWidget::showFullScreen()
emit SizeChanged(width() * dpr, height() * dpr);
}
// Lock the cursor within the window/widget internal borders, including the aspect ratio if wanted
void RenderWidget::SetCursorLocked(bool locked, bool follow_aspect_ratio)
{
// It seems like QT doesn't scale the window frame correctly with some DPIs
// so it might happen that the locked cursor can be on the frame of the window,
// being able to resize it, but that is a minor problem.
// As a hack, if necessary, we could always scale down the size by 2 pixel, to a min of 1 given
// that the size can be 0 already. We probably shouldn't scale axes already scaled by aspect ratio
QRect render_rect = geometry();
if (parentWidget())
{
render_rect.moveTopLeft(parentWidget()->mapToGlobal(render_rect.topLeft()));
}
auto scale = devicePixelRatioF(); // Seems to always be rounded on Win. Should we round results?
QPoint screen_offset = QPoint(0, 0);
if (window()->windowHandle() && window()->windowHandle()->screen())
{
screen_offset = window()->windowHandle()->screen()->geometry().topLeft();
}
render_rect.moveTopLeft(((render_rect.topLeft() - screen_offset) * scale) + screen_offset);
render_rect.setSize(render_rect.size() * scale);
if (follow_aspect_ratio)
{
// TODO: SetCursorLocked() should be re-called every time this value is changed?
// This might cause imprecisions of one pixel (but it won't cause the cursor to go over borders)
Common::Vec2 aspect_ratio = g_controller_interface.GetWindowInputScale();
if (aspect_ratio.x > 1.f)
{
const float new_half_width = float(render_rect.width()) / (aspect_ratio.x * 2.f);
// Only ceil if it was >= 0.25
const float ceiled_new_half_width = std::ceil(std::round(new_half_width * 2.f) / 2.f);
const int x_center = render_rect.center().x();
// Make a guess on which one to floor and ceil.
// For more precision, we should have kept the rounding point scale from above as well.
render_rect.setLeft(x_center - std::floor(new_half_width));
render_rect.setRight(x_center + ceiled_new_half_width);
}
if (aspect_ratio.y > 1.f)
{
const float new_half_height = render_rect.height() / (aspect_ratio.y * 2.f);
const float ceiled_new_half_height = std::ceil(std::round(new_half_height * 2.f) / 2.f);
const int y_center = render_rect.center().y();
render_rect.setTop(y_center - std::floor(new_half_height));
render_rect.setBottom(y_center + ceiled_new_half_height);
}
}
if (locked)
{
#ifdef _WIN32
RECT rect;
rect.left = render_rect.left();
rect.right = render_rect.right();
rect.top = render_rect.top();
rect.bottom = render_rect.bottom();
if (ClipCursor(&rect))
#else
// TODO: implement on other platforms. Probably XGrabPointer on Linux.
// The setting is hidden in the UI if not implemented
if (false)
#endif
{
m_cursor_locked = true;
if (Settings::Instance().GetHideCursor())
{
setCursor(Qt::BlankCursor);
}
Host::GetInstance()->SetRenderFullFocus(true);
}
}
else
{
#ifdef _WIN32
ClipCursor(nullptr);
#endif
if (m_cursor_locked)
{
m_cursor_locked = false;
if (!Settings::Instance().GetLockCursor())
{
return;
}
// Center the mouse in the window if it's still active
// Leave it where it was otherwise, e.g. a prompt has opened or we alt tabbed.
if (isActiveWindow())
{
cursor().setPos(render_rect.left() + render_rect.width() / 2,
render_rect.top() + render_rect.height() / 2);
}
// Show the cursor or the user won't know the mouse is now unlocked
setCursor(Qt::ArrowCursor);
Host::GetInstance()->SetRenderFullFocus(false);
}
}
}
void RenderWidget::SetCursorLockedOnNextActivation(bool locked)
{
if (Settings::Instance().GetLockCursor())
{
m_lock_cursor_on_next_activation = locked;
return;
}
m_lock_cursor_on_next_activation = false;
}
void RenderWidget::SetWaitingForMessageBox(bool waiting_for_message_box)
{
if (m_waiting_for_message_box == waiting_for_message_box)
{
return;
}
m_waiting_for_message_box = waiting_for_message_box;
if (!m_waiting_for_message_box && m_lock_cursor_on_next_activation && isActiveWindow())
{
if (Settings::Instance().GetLockCursor())
{
SetCursorLocked(true);
}
m_lock_cursor_on_next_activation = false;
}
}
bool RenderWidget::event(QEvent* event)
{
PassEventToImGui(event);
@ -178,23 +354,67 @@ bool RenderWidget::event(QEvent* event)
break;
}
// Needed in case a new window open and it moves the mouse
case QEvent::WindowBlocked:
SetCursorLocked(false);
break;
case QEvent::MouseButtonPress:
if (!Settings::Instance().GetHideCursor() && isActiveWindow())
if (isActiveWindow())
{
setCursor(Qt::ArrowCursor);
m_mouse_timer->start(MOUSE_HIDE_DELAY);
// Lock the cursor with any mouse button click (behave the same as window focus change).
// This event is occasionally missed because isActiveWindow is laggy
if (Settings::Instance().GetLockCursor() && event->type() == QEvent::MouseButtonPress)
{
SetCursorLocked(true);
}
// Unhide on movement
if (!Settings::Instance().GetHideCursor())
{
setCursor(Qt::ArrowCursor);
m_mouse_timer->start(MOUSE_HIDE_DELAY);
}
}
break;
case QEvent::WinIdChange:
emit HandleChanged(reinterpret_cast<void*>(winId()));
break;
case QEvent::Show:
// Don't do if "stay on top" changed (or was true)
if (Settings::Instance().GetLockCursor() && Settings::Instance().GetHideCursor() &&
!m_dont_lock_cursor_on_show)
{
// Auto lock when this window is shown (it was hidden)
if (isActiveWindow())
SetCursorLocked(true);
else
SetCursorLockedOnNextActivation();
}
break;
// Note that this event in Windows is not always aligned to the window that is highlighted,
// it's the window that has keyboard and mouse focus
case QEvent::WindowActivate:
if (SConfig::GetInstance().m_PauseOnFocusLost && Core::GetState() == Core::State::Paused)
Core::SetState(Core::State::Running);
UpdateCursor();
// Avoid "race conditions" with message boxes
if (m_lock_cursor_on_next_activation && !m_waiting_for_message_box)
{
if (Settings::Instance().GetLockCursor())
{
SetCursorLocked(true);
}
m_lock_cursor_on_next_activation = false;
}
emit FocusChanged(true);
break;
case QEvent::WindowDeactivate:
SetCursorLocked(false);
UpdateCursor();
if (SConfig::GetInstance().m_PauseOnFocusLost && Core::GetState() == Core::State::Running)
{
// If we are declared as the CPU thread, it means that the real CPU thread is waiting
@ -206,8 +426,13 @@ bool RenderWidget::event(QEvent* event)
emit FocusChanged(false);
break;
case QEvent::Move:
SetCursorLocked(m_cursor_locked);
break;
case QEvent::Resize:
{
SetCursorLocked(m_cursor_locked);
const QResizeEvent* se = static_cast<QResizeEvent*>(event);
QSize new_size = se->size();
@ -218,14 +443,18 @@ bool RenderWidget::event(QEvent* event)
emit SizeChanged(new_size.width() * dpr, new_size.height() * dpr);
break;
}
// Happens when we add/remove the widget from the main window instead of the dedicated one
case QEvent::ParentChange:
SetCursorLocked(false);
break;
case QEvent::WindowStateChange:
// Lock the mouse again when fullscreen changes (we might have missed some events)
SetCursorLocked(m_cursor_locked || (isFullScreen() && Settings::Instance().GetLockCursor()));
emit StateChanged(isFullScreen());
break;
case QEvent::Close:
emit Closed();
break;
default:
break;
}
return QWidget::event(event);
}