243 changes: 236 additions & 7 deletions Source/Core/DolphinQt/RenderWidget.cpp
Expand Up @@ -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"));
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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)
Expand All @@ -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()
Expand All @@ -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);
Expand All @@ -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
Expand All @@ -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();

Expand All @@ -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);
}
Expand Down
10 changes: 10 additions & 0 deletions Source/Core/DolphinQt/RenderWidget.h
Expand Up @@ -20,6 +20,10 @@ class RenderWidget final : public QWidget
bool event(QEvent* event) override;
void showFullScreen();
QPaintEngine* paintEngine() const override;
bool IsCursorLocked() const { return m_cursor_locked; }
void SetCursorLockedOnNextActivation(bool locked = true);
void SetWaitingForMessageBox(bool waiting_for_message_box);
void SetCursorLocked(bool locked, bool follow_aspect_ratio = true);

signals:
void EscapePressed();
Expand All @@ -32,7 +36,9 @@ class RenderWidget final : public QWidget
private:
void HandleCursorTimer();
void OnHideCursorChanged();
void OnLockCursorChanged();
void OnKeepOnTopChanged(bool top);
void UpdateCursor();
void PassEventToImGui(const QEvent* event);
void SetImGuiKeyMap();
void dragEnterEvent(QDragEnterEvent* event) override;
Expand All @@ -41,4 +47,8 @@ class RenderWidget final : public QWidget
static constexpr int MOUSE_HIDE_DELAY = 3000;
QTimer* m_mouse_timer;
QPoint m_last_mouse{};
bool m_cursor_locked = false;
bool m_lock_cursor_on_next_activation = false;
bool m_dont_lock_cursor_on_show = false;
bool m_waiting_for_message_box = false;
};
11 changes: 11 additions & 0 deletions Source/Core/DolphinQt/Settings.cpp
Expand Up @@ -280,6 +280,17 @@ bool Settings::GetHideCursor() const
return SConfig::GetInstance().bHideCursor;
}

void Settings::SetLockCursor(bool lock_cursor)
{
SConfig::GetInstance().bLockCursor = lock_cursor;
emit LockCursorChanged();
}

bool Settings::GetLockCursor() const
{
return SConfig::GetInstance().bLockCursor;
}

void Settings::SetKeepWindowOnTop(bool top)
{
if (IsKeepWindowOnTopEnabled() == top)
Expand Down
3 changes: 3 additions & 0 deletions Source/Core/DolphinQt/Settings.h
Expand Up @@ -100,6 +100,8 @@ class Settings final : public QObject
// Graphics
void SetHideCursor(bool hide_cursor);
bool GetHideCursor() const;
void SetLockCursor(bool lock_cursor);
bool GetLockCursor() const;
void SetKeepWindowOnTop(bool top);
bool IsKeepWindowOnTopEnabled() const;

Expand Down Expand Up @@ -168,6 +170,7 @@ class Settings final : public QObject
void MetadataRefreshCompleted();
void AutoRefreshToggled(bool enabled);
void HideCursorChanged();
void LockCursorChanged();
void KeepWindowOnTopChanged(bool top);
void VolumeChanged(int volume);
void NANDRefresh();
Expand Down
14 changes: 14 additions & 0 deletions Source/Core/DolphinQt/Settings/InterfacePane.cpp
Expand Up @@ -171,6 +171,14 @@ void InterfacePane::CreateInGame()
m_checkbox_show_active_title = new QCheckBox(tr("Show Active Title in Window Title"));
m_checkbox_pause_on_focus_lost = new QCheckBox(tr("Pause on Focus Loss"));
m_checkbox_hide_mouse = new QCheckBox(tr("Always Hide Mouse Cursor"));
m_checkbox_lock_mouse = new QCheckBox(tr("Lock Mouse Cursor"));

m_checkbox_hide_mouse->setToolTip(
tr("Will immediately hide the Mouse Cursor when it hovers on top of the Render Widget, "
"otherwise "
"there is a delay.\nIf \"Lock Mouse Cursor\" is enabled, it will hide on Mouse locked"));
m_checkbox_lock_mouse->setToolTip(tr("Will lock the Mouse Cursor to the Render Widget as long as "
"it has focus. You can set a hotkey to unlock it."));

groupbox_layout->addWidget(m_checkbox_top_window);
groupbox_layout->addWidget(m_checkbox_confirm_on_stop);
Expand All @@ -179,6 +187,9 @@ void InterfacePane::CreateInGame()
groupbox_layout->addWidget(m_checkbox_show_active_title);
groupbox_layout->addWidget(m_checkbox_pause_on_focus_lost);
groupbox_layout->addWidget(m_checkbox_hide_mouse);
#ifdef _WIN32
groupbox_layout->addWidget(m_checkbox_lock_mouse);
#endif
}

void InterfacePane::ConnectLayout()
Expand All @@ -203,6 +214,8 @@ void InterfacePane::ConnectLayout()
connect(m_checkbox_pause_on_focus_lost, &QCheckBox::toggled, this, &InterfacePane::OnSaveConfig);
connect(m_checkbox_hide_mouse, &QCheckBox::toggled, &Settings::Instance(),
&Settings::SetHideCursor);
connect(m_checkbox_lock_mouse, &QCheckBox::toggled, &Settings::Instance(),
&Settings::SetLockCursor);
connect(m_checkbox_use_userstyle, &QCheckBox::toggled, this, &InterfacePane::OnSaveConfig);
}

Expand Down Expand Up @@ -239,6 +252,7 @@ void InterfacePane::LoadConfig()
m_checkbox_use_covers->setChecked(Config::Get(Config::MAIN_USE_GAME_COVERS));
m_checkbox_focused_hotkeys->setChecked(Config::Get(Config::MAIN_FOCUSED_HOTKEYS));
m_checkbox_hide_mouse->setChecked(Settings::Instance().GetHideCursor());
m_checkbox_lock_mouse->setChecked(Settings::Instance().GetLockCursor());
m_checkbox_disable_screensaver->setChecked(Config::Get(Config::MAIN_DISABLE_SCREENSAVER));
}

Expand Down
1 change: 1 addition & 0 deletions Source/Core/DolphinQt/Settings/InterfacePane.h
Expand Up @@ -45,4 +45,5 @@ class InterfacePane final : public QWidget
QCheckBox* m_checkbox_show_active_title;
QCheckBox* m_checkbox_pause_on_focus_lost;
QCheckBox* m_checkbox_hide_mouse;
QCheckBox* m_checkbox_lock_mouse;
};
4 changes: 4 additions & 0 deletions Source/DSPTool/StubHost.cpp
Expand Up @@ -39,6 +39,10 @@ bool Host_RendererHasFocus()
{
return false;
}
bool Host_RendererHasFullFocus()
{
return false;
}
bool Host_RendererIsFullscreen()
{
return false;
Expand Down
4 changes: 4 additions & 0 deletions Source/UnitTests/StubHost.cpp
Expand Up @@ -43,6 +43,10 @@ bool Host_RendererHasFocus()
{
return false;
}
bool Host_RendererHasFullFocus()
{
return false;
}
bool Host_RendererIsFullscreen()
{
return false;
Expand Down