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 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 s_hotkey_labels{{ +constexpr std::array s_hotkey_labels{{ _trans("Open"), _trans("Change Disc"), _trans("Eject Disc"), @@ -35,6 +35,7 @@ constexpr std::array 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 +#ifdef _WIN32 +#include +#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 m_render_handle{nullptr}; + std::atomic m_main_window_handle{nullptr}; + std::atomic m_render_to_main{false}; std::atomic m_render_focus{false}; + std::atomic m_render_full_focus{false}; std::atomic 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 boot_parameters, return; } } + + Host::GetInstance()->SetMainWindowHandle(reinterpret_cast(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(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(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 +#include +#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(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(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;