summaryrefslogtreecommitdiff
path: root/Source
diff options
context:
space:
mode:
authorFiloppi <filippotarpini@hotmail.it>2021-05-09 13:28:04 +0300
committerFiloppi <filippotarpini@hotmail.it>2021-05-27 10:31:12 +0300
commit3c7c2dfaa10fc837f75d7e71d71b483db343dee3 (patch)
tree516ec8f5bb1a145ac6b244d97453d22b55e9a596 /Source
parentff08b85740f52394433ebc2a49c22b324c858dc7 (diff)
Implement Cursor Locking and new input focus checks for it
Diffstat (limited to 'Source')
-rw-r--r--Source/Android/jni/MainAndroid.cpp6
-rw-r--r--Source/Core/Core/ConfigManager.cpp2
-rw-r--r--Source/Core/Core/ConfigManager.h1
-rw-r--r--Source/Core/Core/Core.cpp11
-rw-r--r--Source/Core/Core/Core.h2
-rw-r--r--Source/Core/Core/HW/VideoInterface.cpp3
-rw-r--r--Source/Core/Core/Host.h1
-rw-r--r--Source/Core/Core/HotkeyManager.cpp3
-rw-r--r--Source/Core/Core/HotkeyManager.h1
-rw-r--r--Source/Core/DolphinNoGUI/MainNoGUI.cpp6
-rw-r--r--Source/Core/DolphinNoGUI/Platform.h2
-rw-r--r--Source/Core/DolphinQt/Host.cpp34
-rw-r--r--Source/Core/DolphinQt/Host.h6
-rw-r--r--Source/Core/DolphinQt/HotkeyScheduler.cpp4
-rw-r--r--Source/Core/DolphinQt/HotkeyScheduler.h1
-rw-r--r--Source/Core/DolphinQt/MainWindow.cpp41
-rw-r--r--Source/Core/DolphinQt/MainWindow.h1
-rw-r--r--Source/Core/DolphinQt/RenderWidget.cpp243
-rw-r--r--Source/Core/DolphinQt/RenderWidget.h10
-rw-r--r--Source/Core/DolphinQt/Settings.cpp11
-rw-r--r--Source/Core/DolphinQt/Settings.h3
-rw-r--r--Source/Core/DolphinQt/Settings/InterfacePane.cpp14
-rw-r--r--Source/Core/DolphinQt/Settings/InterfacePane.h1
-rw-r--r--Source/DSPTool/StubHost.cpp4
-rw-r--r--Source/UnitTests/StubHost.cpp4
25 files changed, 400 insertions, 15 deletions
diff --git a/Source/Android/jni/MainAndroid.cpp b/Source/Android/jni/MainAndroid.cpp
index 1b698d2a17..3e9ff48b67 100644
--- a/Source/Android/jni/MainAndroid.cpp
+++ b/Source/Android/jni/MainAndroid.cpp
@@ -151,6 +151,12 @@ bool Host_RendererHasFocus()
return true;
}
+bool Host_RendererHasFullFocus()
+{
+ // Mouse cursor locking actually exists in Android but we don't implement (nor need) that
+ return true;
+}
+
bool Host_RendererIsFullscreen()
{
return false;
diff --git a/Source/Core/Core/ConfigManager.cpp b/Source/Core/Core/ConfigManager.cpp
index e65fd6522e..ab9c9e26fd 100644
--- a/Source/Core/Core/ConfigManager.cpp
+++ b/Source/Core/Core/ConfigManager.cpp
@@ -147,6 +147,7 @@ void SConfig::SaveInterfaceSettings(IniFile& ini)
interface->Set("ConfirmStop", bConfirmStop);
interface->Set("HideCursor", bHideCursor);
+ interface->Set("LockCursor", bLockCursor);
interface->Set("LanguageCode", m_InterfaceLanguage);
interface->Set("ExtendedFPSInfo", m_InterfaceExtendedFPSInfo);
interface->Set("ShowActiveTitle", m_show_active_title);
@@ -401,6 +402,7 @@ void SConfig::LoadInterfaceSettings(IniFile& ini)
interface->Get("ConfirmStop", &bConfirmStop, true);
interface->Get("HideCursor", &bHideCursor, false);
+ interface->Get("LockCursor", &bLockCursor, false);
interface->Get("LanguageCode", &m_InterfaceLanguage, "");
interface->Get("ExtendedFPSInfo", &m_InterfaceExtendedFPSInfo, false);
interface->Get("ShowActiveTitle", &m_show_active_title, true);
diff --git a/Source/Core/Core/ConfigManager.h b/Source/Core/Core/ConfigManager.h
index 38c6abad74..995b84ab8f 100644
--- a/Source/Core/Core/ConfigManager.h
+++ b/Source/Core/Core/ConfigManager.h
@@ -150,6 +150,7 @@ struct SConfig
// Interface settings
bool bConfirmStop = false;
bool bHideCursor = false;
+ bool bLockCursor = false;
std::string theme_name;
// Bluetooth passthrough mode settings
diff --git a/Source/Core/Core/Core.cpp b/Source/Core/Core/Core.cpp
index a3f7036584..1ec847cecc 100644
--- a/Source/Core/Core/Core.cpp
+++ b/Source/Core/Core/Core.cpp
@@ -1113,10 +1113,15 @@ void DoFrameStep()
}
}
-void UpdateInputGate(bool require_focus)
+void UpdateInputGate(bool require_focus, bool require_full_focus)
{
- ControlReference::SetInputGate((!require_focus || Host_RendererHasFocus()) &&
- !Host_UIBlocksControllerState());
+ // If the user accepts background input, controls should pass even if an on screen interface is on
+ const bool focus_passes =
+ !require_focus || (Host_RendererHasFocus() && !Host_UIBlocksControllerState());
+ // Ignore full focus if we don't require basic focus
+ const bool full_focus_passes =
+ !require_focus || !require_full_focus || (focus_passes && Host_RendererHasFullFocus());
+ ControlReference::SetInputGate(focus_passes && full_focus_passes);
}
} // namespace Core
diff --git a/Source/Core/Core/Core.h b/Source/Core/Core/Core.h
index d21719f218..681ffbac87 100644
--- a/Source/Core/Core/Core.h
+++ b/Source/Core/Core/Core.h
@@ -169,6 +169,6 @@ void HostDispatchJobs();
void DoFrameStep();
-void UpdateInputGate(bool require_focus);
+void UpdateInputGate(bool require_focus, bool require_full_focus = false);
} // namespace Core
diff --git a/Source/Core/Core/HW/VideoInterface.cpp b/Source/Core/Core/HW/VideoInterface.cpp
index 0bcdd8df88..e198cb312b 100644
--- a/Source/Core/Core/HW/VideoInterface.cpp
+++ b/Source/Core/Core/HW/VideoInterface.cpp
@@ -867,7 +867,8 @@ void Update(u64 ticks)
if (s_half_line_of_next_si_poll == s_half_line_count)
{
- Core::UpdateInputGate(!SConfig::GetInstance().m_BackgroundInput);
+ Core::UpdateInputGate(!SConfig::GetInstance().m_BackgroundInput,
+ SConfig::GetInstance().bLockCursor);
SerialInterface::UpdateDevices();
s_half_line_of_next_si_poll += 2 * SerialInterface::GetPollXLines();
}
diff --git a/Source/Core/Core/Host.h b/Source/Core/Core/Host.h
index ec539a01d2..580d7eae64 100644
--- a/Source/Core/Core/Host.h
+++ b/Source/Core/Core/Host.h
@@ -36,6 +36,7 @@ enum class HostMessageID
std::vector<std::string> Host_GetPreferredLocales();
bool Host_UIBlocksControllerState();
bool Host_RendererHasFocus();
+bool Host_RendererHasFullFocus();
bool Host_RendererIsFullscreen();
void Host_Message(HostMessageID id);
diff --git a/Source/Core/Core/HotkeyManager.cpp b/Source/Core/Core/HotkeyManager.cpp
index f9c02e02e9..cd92572db9 100644
--- a/Source/Core/Core/HotkeyManager.cpp
+++ b/Source/Core/Core/HotkeyManager.cpp
@@ -24,7 +24,7 @@
#include "InputCommon/GCPadStatus.h"
// clang-format off
-constexpr std::array<const char*, 125> s_hotkey_labels{{
+constexpr std::array<const char*, 126> s_hotkey_labels{{
_trans("Open"),
_trans("Change Disc"),
_trans("Eject Disc"),
@@ -35,6 +35,7 @@ constexpr std::array<const char*, 125> s_hotkey_labels{{
_trans("Toggle Fullscreen"),
_trans("Take Screenshot"),
_trans("Exit"),
+ _trans("Unlock Cursor"),
_trans("Activate NetPlay Chat"),
_trans("Control NetPlay Golf Mode"),
diff --git a/Source/Core/Core/HotkeyManager.h b/Source/Core/Core/HotkeyManager.h
index 8bce8f86fe..66732f7d24 100644
--- a/Source/Core/Core/HotkeyManager.h
+++ b/Source/Core/Core/HotkeyManager.h
@@ -29,6 +29,7 @@ enum Hotkey
HK_FULLSCREEN,
HK_SCREENSHOT,
HK_EXIT,
+ HK_UNLOCK_CURSOR,
HK_ACTIVATE_CHAT,
HK_REQUEST_GOLF_CONTROL,
diff --git a/Source/Core/DolphinNoGUI/MainNoGUI.cpp b/Source/Core/DolphinNoGUI/MainNoGUI.cpp
index 7c64a5cbeb..dceecc789c 100644
--- a/Source/Core/DolphinNoGUI/MainNoGUI.cpp
+++ b/Source/Core/DolphinNoGUI/MainNoGUI.cpp
@@ -98,6 +98,12 @@ bool Host_RendererHasFocus()
return s_platform->IsWindowFocused();
}
+bool Host_RendererHasFullFocus()
+{
+ // Mouse capturing isn't implemented
+ return Host_RendererHasFocus();
+}
+
bool Host_RendererIsFullscreen()
{
return s_platform->IsWindowFullscreen();
diff --git a/Source/Core/DolphinNoGUI/Platform.h b/Source/Core/DolphinNoGUI/Platform.h
index b0d98e58be..f97d81855f 100644
--- a/Source/Core/DolphinNoGUI/Platform.h
+++ b/Source/Core/DolphinNoGUI/Platform.h
@@ -51,6 +51,6 @@ protected:
Common::Flag m_shutdown_requested{false};
Common::Flag m_tried_graceful_shutdown{false};
- bool m_window_focus = true;
+ bool m_window_focus = true; // Should be made atomic if actually implemented
bool m_window_fullscreen = false;
};
diff --git a/Source/Core/DolphinQt/Host.cpp b/Source/Core/DolphinQt/Host.cpp
index fdf0309b26..b0cfb507bd 100644
--- a/Source/Core/DolphinQt/Host.cpp
+++ b/Source/Core/DolphinQt/Host.cpp
@@ -10,6 +10,10 @@
#include <imgui.h>
+#ifdef _WIN32
+#include <windows.h>
+#endif
+
#include "Common/Common.h"
#include "Core/Config/MainSettings.h"
@@ -49,6 +53,8 @@ Host* Host::GetInstance()
void Host::SetRenderHandle(void* handle)
{
+ m_render_to_main = Config::Get(Config::MAIN_RENDER_TO_MAIN);
+
if (m_render_handle == handle)
return;
@@ -61,9 +67,27 @@ void Host::SetRenderHandle(void* handle)
}
}
+void Host::SetMainWindowHandle(void* handle)
+{
+ m_main_window_handle = handle;
+}
+
bool Host::GetRenderFocus()
{
+#ifdef _WIN32
+ // Unfortunately Qt calls SetRenderFocus() with a slight delay compared to what we actually need
+ // to avoid inputs that cause a focus loss to be processed by the emulation
+ if (m_render_to_main)
+ return GetForegroundWindow() == (HWND)m_main_window_handle.load();
+ return GetForegroundWindow() == (HWND)m_render_handle.load();
+#else
return m_render_focus;
+#endif
+}
+
+bool Host::GetRenderFullFocus()
+{
+ return m_render_full_focus;
}
void Host::SetRenderFocus(bool focus)
@@ -76,6 +100,11 @@ void Host::SetRenderFocus(bool focus)
});
}
+void Host::SetRenderFullFocus(bool focus)
+{
+ m_render_full_focus = focus;
+}
+
bool Host::GetRenderFullscreen()
{
return m_render_fullscreen;
@@ -131,6 +160,11 @@ bool Host_RendererHasFocus()
return Host::GetInstance()->GetRenderFocus();
}
+bool Host_RendererHasFullFocus()
+{
+ return Host::GetInstance()->GetRenderFullFocus();
+}
+
bool Host_RendererIsFullscreen()
{
return Host::GetInstance()->GetRenderFullscreen();
diff --git a/Source/Core/DolphinQt/Host.h b/Source/Core/DolphinQt/Host.h
index 090e620336..6be28d5464 100644
--- a/Source/Core/DolphinQt/Host.h
+++ b/Source/Core/DolphinQt/Host.h
@@ -23,10 +23,13 @@ public:
static Host* GetInstance();
bool GetRenderFocus();
+ bool GetRenderFullFocus();
bool GetRenderFullscreen();
+ void SetMainWindowHandle(void* handle);
void SetRenderHandle(void* handle);
void SetRenderFocus(bool focus);
+ void SetRenderFullFocus(bool focus);
void SetRenderFullscreen(bool fullscreen);
void ResizeSurface(int new_width, int new_height);
void RequestNotifyMapLoaded();
@@ -42,6 +45,9 @@ private:
Host();
std::atomic<void*> m_render_handle{nullptr};
+ std::atomic<void*> m_main_window_handle{nullptr};
+ std::atomic<bool> m_render_to_main{false};
std::atomic<bool> m_render_focus{false};
+ std::atomic<bool> m_render_full_focus{false};
std::atomic<bool> m_render_fullscreen{false};
};
diff --git a/Source/Core/DolphinQt/HotkeyScheduler.cpp b/Source/Core/DolphinQt/HotkeyScheduler.cpp
index 30bc61aec0..85cd4d469a 100644
--- a/Source/Core/DolphinQt/HotkeyScheduler.cpp
+++ b/Source/Core/DolphinQt/HotkeyScheduler.cpp
@@ -213,6 +213,10 @@ void HotkeyScheduler::Run()
if (IsHotkey(HK_EXIT))
emit ExitHotkey();
+ // Unlock Cursor
+ if (IsHotkey(HK_UNLOCK_CURSOR))
+ emit UnlockCursor();
+
auto& settings = Settings::Instance();
// Toggle Chat
diff --git a/Source/Core/DolphinQt/HotkeyScheduler.h b/Source/Core/DolphinQt/HotkeyScheduler.h
index 4717eb4492..8ae02301ef 100644
--- a/Source/Core/DolphinQt/HotkeyScheduler.h
+++ b/Source/Core/DolphinQt/HotkeyScheduler.h
@@ -27,6 +27,7 @@ signals:
void ChangeDisc();
void ExitHotkey();
+ void UnlockCursor();
void ActivateChat();
void RequestGolfControl();
void FullScreenHotkey();
diff --git a/Source/Core/DolphinQt/MainWindow.cpp b/Source/Core/DolphinQt/MainWindow.cpp
index 61e58eb19f..df580398d8 100644
--- a/Source/Core/DolphinQt/MainWindow.cpp
+++ b/Source/Core/DolphinQt/MainWindow.cpp
@@ -269,6 +269,8 @@ MainWindow::MainWindow(std::unique_ptr<BootParameters> boot_parameters,
return;
}
}
+
+ Host::GetInstance()->SetMainWindowHandle(reinterpret_cast<void*>(winId()));
}
MainWindow::~MainWindow()
@@ -547,6 +549,7 @@ void MainWindow::ConnectHotkeys()
connect(m_hotkey_scheduler, &HotkeyScheduler::ChangeDisc, this, &MainWindow::ChangeDisc);
connect(m_hotkey_scheduler, &HotkeyScheduler::EjectDisc, this, &MainWindow::EjectDisc);
connect(m_hotkey_scheduler, &HotkeyScheduler::ExitHotkey, this, &MainWindow::close);
+ connect(m_hotkey_scheduler, &HotkeyScheduler::UnlockCursor, this, &MainWindow::UnlockCursor);
connect(m_hotkey_scheduler, &HotkeyScheduler::TogglePauseHotkey, this, &MainWindow::TogglePause);
connect(m_hotkey_scheduler, &HotkeyScheduler::ActivateChat, this, &MainWindow::OnActivateChat);
connect(m_hotkey_scheduler, &HotkeyScheduler::RequestGolfControl, this,
@@ -813,6 +816,13 @@ bool MainWindow::RequestStop()
return true;
}
+ const bool rendered_widget_was_active =
+ m_render_widget->isActiveWindow() && !m_render_widget->isFullScreen();
+ QWidget* confirm_parent = (!m_rendering_to_main && rendered_widget_was_active) ?
+ m_render_widget :
+ static_cast<QWidget*>(this);
+ const bool was_cursor_locked = m_render_widget->IsCursorLocked();
+
if (!m_render_widget->isFullScreen())
m_render_widget_geometry = m_render_widget->saveGeometry();
else
@@ -833,21 +843,44 @@ bool MainWindow::RequestStop()
if (pause)
Core::SetState(Core::State::Paused);
+ if (rendered_widget_was_active)
+ {
+ // We have to do this before creating the message box, otherwise we might receive the window
+ // activation event before we know we need to lock the cursor again.
+ m_render_widget->SetCursorLockedOnNextActivation(was_cursor_locked);
+ }
+
+ // This is to avoid any "race conditions" between the "Window Activate" message and the
+ // message box returning, which could break cursor locking depending on the order
+ m_render_widget->SetWaitingForMessageBox(true);
auto confirm = ModalMessageBox::question(
- m_rendering_to_main ? static_cast<QWidget*>(this) : m_render_widget, tr("Confirm"),
+ confirm_parent, tr("Confirm"),
m_stop_requested ? tr("A shutdown is already in progress. Unsaved data "
"may be lost if you stop the current emulation "
"before it completes. Force stop?") :
tr("Do you want to stop the current emulation?"),
QMessageBox::Yes | QMessageBox::No, QMessageBox::NoButton, Qt::ApplicationModal);
+ // If a user confirmed stopping the emulation, we do not capture the cursor again,
+ // even if the render widget will stay alive for a while.
+ // If a used rejected stopping the emulation, we instead capture the cursor again,
+ // and let them continue playing as if nothing had happened
+ // (assuming cursor locking is on).
if (confirm != QMessageBox::Yes)
{
+ m_render_widget->SetWaitingForMessageBox(false);
+
if (pause)
Core::SetState(state);
return false;
}
+ else
+ {
+ m_render_widget->SetCursorLockedOnNextActivation(false);
+ // This needs to be after SetCursorLockedOnNextActivation(false) as it depends on it
+ m_render_widget->SetWaitingForMessageBox(false);
+ }
}
OnStopRecording();
@@ -917,6 +950,12 @@ void MainWindow::FullScreen()
}
}
+void MainWindow::UnlockCursor()
+{
+ if (!m_render_widget->isFullScreen())
+ m_render_widget->SetCursorLocked(false);
+}
+
void MainWindow::ScreenShot()
{
Core::SaveScreenShot();
diff --git a/Source/Core/DolphinQt/MainWindow.h b/Source/Core/DolphinQt/MainWindow.h
index 60653412bb..79383b3541 100644
--- a/Source/Core/DolphinQt/MainWindow.h
+++ b/Source/Core/DolphinQt/MainWindow.h
@@ -108,6 +108,7 @@ private:
void SetFullScreenResolution(bool fullscreen);
void FullScreen();
+ void UnlockCursor();
void ScreenShot();
void CreateComponents();
diff --git a/Source/Core/DolphinQt/RenderWidget.cpp b/Source/Core/DolphinQt/RenderWidget.cpp
index 7347fa3ef8..6ce9d6336f 100644
--- a/Source/Core/DolphinQt/RenderWidget.cpp
+++ b/Source/Core/DolphinQt/RenderWidget.cpp
@@ -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);
}
diff --git a/Source/Core/DolphinQt/RenderWidget.h b/Source/Core/DolphinQt/RenderWidget.h
index 975b8399f1..0044981f9b 100644
--- a/Source/Core/DolphinQt/RenderWidget.h
+++ b/Source/Core/DolphinQt/RenderWidget.h
@@ -20,6 +20,10 @@ public:
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();
@@ -32,7 +36,9 @@ signals:
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;
@@ -41,4 +47,8 @@ private:
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;
};
diff --git a/Source/Core/DolphinQt/Settings.cpp b/Source/Core/DolphinQt/Settings.cpp
index d448ca0abf..7541a9087f 100644
--- a/Source/Core/DolphinQt/Settings.cpp
+++ b/Source/Core/DolphinQt/Settings.cpp
@@ -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)
diff --git a/Source/Core/DolphinQt/Settings.h b/Source/Core/DolphinQt/Settings.h
index f3753d1d5f..d5fd90989c 100644
--- a/Source/Core/DolphinQt/Settings.h
+++ b/Source/Core/DolphinQt/Settings.h
@@ -100,6 +100,8 @@ public:
// Graphics
void SetHideCursor(bool hide_cursor);
bool GetHideCursor() const;
+ void SetLockCursor(bool lock_cursor);
+ bool GetLockCursor() const;
void SetKeepWindowOnTop(bool top);
bool IsKeepWindowOnTopEnabled() const;
@@ -168,6 +170,7 @@ signals:
void MetadataRefreshCompleted();
void AutoRefreshToggled(bool enabled);
void HideCursorChanged();
+ void LockCursorChanged();
void KeepWindowOnTopChanged(bool top);
void VolumeChanged(int volume);
void NANDRefresh();
diff --git a/Source/Core/DolphinQt/Settings/InterfacePane.cpp b/Source/Core/DolphinQt/Settings/InterfacePane.cpp
index cd0596f319..5a0a3fa500 100644
--- a/Source/Core/DolphinQt/Settings/InterfacePane.cpp
+++ b/Source/Core/DolphinQt/Settings/InterfacePane.cpp
@@ -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);
@@ -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()
@@ -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);
}
@@ -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));
}
diff --git a/Source/Core/DolphinQt/Settings/InterfacePane.h b/Source/Core/DolphinQt/Settings/InterfacePane.h
index d135de5992..6fde8d25fe 100644
--- a/Source/Core/DolphinQt/Settings/InterfacePane.h
+++ b/Source/Core/DolphinQt/Settings/InterfacePane.h
@@ -45,4 +45,5 @@ private:
QCheckBox* m_checkbox_show_active_title;
QCheckBox* m_checkbox_pause_on_focus_lost;
QCheckBox* m_checkbox_hide_mouse;
+ QCheckBox* m_checkbox_lock_mouse;
};
diff --git a/Source/DSPTool/StubHost.cpp b/Source/DSPTool/StubHost.cpp
index 19c85a642b..6dce41694b 100644
--- a/Source/DSPTool/StubHost.cpp
+++ b/Source/DSPTool/StubHost.cpp
@@ -39,6 +39,10 @@ bool Host_RendererHasFocus()
{
return false;
}
+bool Host_RendererHasFullFocus()
+{
+ return false;
+}
bool Host_RendererIsFullscreen()
{
return false;
diff --git a/Source/UnitTests/StubHost.cpp b/Source/UnitTests/StubHost.cpp
index 4b9db8f033..7a84a9ece9 100644
--- a/Source/UnitTests/StubHost.cpp
+++ b/Source/UnitTests/StubHost.cpp
@@ -43,6 +43,10 @@ bool Host_RendererHasFocus()
{
return false;
}
+bool Host_RendererHasFullFocus()
+{
+ return false;
+}
bool Host_RendererIsFullscreen()
{
return false;