mirror of
https://github.com/dolphin-emu/dolphin.git
synced 2025-07-21 05:09:34 -06:00
Implement Cursor Locking and new input focus checks for it
This commit is contained in:
@ -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);
|
||||
}
|
||||
|
Reference in New Issue
Block a user