From 731532fdd87cafffc6159f4d226f23cf6d6af4c9 Mon Sep 17 00:00:00 2001 From: Mike Hall Date: Thu, 5 Feb 2026 10:37:10 +0000 Subject: [PATCH] Add option to disable CursorWrap when on a single monitor. (#45303) ## Summary of the Pull Request CursorWrap wraps on the outer edge of monitors, if a user is swapping between a laptop and docked laptop with external monitors the user might want to only enable wrapping when connected to external monitors, and disable when only on the laptop. ## PR Checklist - [ ] Closes: #45198 - [ ] Closes: #45154 - [ ] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [ ] **Tests:** Added/updated and all pass - [ ] **Localization:** All end-user-facing strings can be localized - [ ] **Dev docs:** Added/updated - [ ] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx ## Detailed Description of the Pull Request / Additional comments Currently CursorWrap will wrap around the horizontal/vertical edges of monitors, if the user has more than one monitor the outer edges are used as wrap targets, if the user only has one monitor (perhaps a laptop) wrapping might be temporarily disabled until additional external monitors are added (such as being plugged into a dock or using a USB-C monitor). The new option will disable wrapping if only a single monitor is detected, monitor detection is dynamic. ## Validation Steps Performed Validated on a Surface Laptop 7 Pro (Intel) with a USB-C External Monitor. --------- Co-authored-by: Niels Laute --- .../MouseUtils/CursorWrap/CursorWrapCore.cpp | 16 +++++++++- .../MouseUtils/CursorWrap/CursorWrapCore.h | 4 ++- src/modules/MouseUtils/CursorWrap/dllmain.cpp | 20 +++++++++++- .../CursorWrapProperties.cs | 4 +++ .../Settings.UI.Library/CursorWrapSettings.cs | 7 ++++ .../SettingsXAML/Views/MouseUtilsPage.xaml | 3 ++ .../Settings.UI/Strings/en-us/Resources.resw | 3 ++ .../ViewModels/MouseUtilsViewModel.cs | 32 +++++++++++++++++++ 8 files changed, 86 insertions(+), 3 deletions(-) diff --git a/src/modules/MouseUtils/CursorWrap/CursorWrapCore.cpp b/src/modules/MouseUtils/CursorWrap/CursorWrapCore.cpp index bea59e6186..c1d4a9b36b 100644 --- a/src/modules/MouseUtils/CursorWrap/CursorWrapCore.cpp +++ b/src/modules/MouseUtils/CursorWrap/CursorWrapCore.cpp @@ -163,8 +163,22 @@ void CursorWrapCore::UpdateMonitorInfo() Logger::info(L"======= UPDATE MONITOR INFO END ======="); } -POINT CursorWrapCore::HandleMouseMove(const POINT& currentPos, bool disableWrapDuringDrag, int wrapMode) +POINT CursorWrapCore::HandleMouseMove(const POINT& currentPos, bool disableWrapDuringDrag, int wrapMode, bool disableOnSingleMonitor) { + // Check if wrapping should be disabled on single monitor + if (disableOnSingleMonitor && m_monitors.size() <= 1) + { +#ifdef _DEBUG + static bool loggedOnce = false; + if (!loggedOnce) + { + OutputDebugStringW(L"[CursorWrap] Single monitor detected - cursor wrapping disabled\n"); + loggedOnce = true; + } +#endif + return currentPos; + } + // Check if wrapping should be disabled during drag if (disableWrapDuringDrag && (GetAsyncKeyState(VK_LBUTTON) & 0x8000)) { diff --git a/src/modules/MouseUtils/CursorWrap/CursorWrapCore.h b/src/modules/MouseUtils/CursorWrap/CursorWrapCore.h index 6c19a26e39..d8472efd08 100644 --- a/src/modules/MouseUtils/CursorWrap/CursorWrapCore.h +++ b/src/modules/MouseUtils/CursorWrap/CursorWrapCore.h @@ -18,9 +18,11 @@ public: // Handle mouse move with wrap mode filtering // wrapMode: 0=Both, 1=VerticalOnly, 2=HorizontalOnly - POINT HandleMouseMove(const POINT& currentPos, bool disableWrapDuringDrag, int wrapMode); + // disableOnSingleMonitor: if true, cursor wrapping is disabled when only one monitor is connected + POINT HandleMouseMove(const POINT& currentPos, bool disableWrapDuringDrag, int wrapMode, bool disableOnSingleMonitor); const std::vector& GetMonitors() const { return m_monitors; } + size_t GetMonitorCount() const { return m_monitors.size(); } const MonitorTopology& GetTopology() const { return m_topology; } private: diff --git a/src/modules/MouseUtils/CursorWrap/dllmain.cpp b/src/modules/MouseUtils/CursorWrap/dllmain.cpp index 08c39bab60..add9fb7f92 100644 --- a/src/modules/MouseUtils/CursorWrap/dllmain.cpp +++ b/src/modules/MouseUtils/CursorWrap/dllmain.cpp @@ -54,6 +54,7 @@ namespace const wchar_t JSON_KEY_AUTO_ACTIVATE[] = L"auto_activate"; const wchar_t JSON_KEY_DISABLE_WRAP_DURING_DRAG[] = L"disable_wrap_during_drag"; const wchar_t JSON_KEY_WRAP_MODE[] = L"wrap_mode"; + const wchar_t JSON_KEY_DISABLE_ON_SINGLE_MONITOR[] = L"disable_cursor_wrap_on_single_monitor"; } // The PowerToy name that will be shown in the settings. @@ -80,6 +81,7 @@ private: bool m_enabled = false; bool m_autoActivate = false; bool m_disableWrapDuringDrag = true; // Default to true to prevent wrap during drag + bool m_disableOnSingleMonitor = false; // Default to false int m_wrapMode = 0; // 0=Both (default), 1=VerticalOnly, 2=HorizontalOnly // Mouse hook @@ -415,6 +417,21 @@ private: { Logger::warn("Failed to initialize CursorWrap wrap mode from settings. Will use default value (0=Both)"); } + + try + { + // Parse disable on single monitor + auto propertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES); + if (propertiesObject.HasKey(JSON_KEY_DISABLE_ON_SINGLE_MONITOR)) + { + auto disableOnSingleMonitorObject = propertiesObject.GetNamedObject(JSON_KEY_DISABLE_ON_SINGLE_MONITOR); + m_disableOnSingleMonitor = disableOnSingleMonitorObject.GetNamedBoolean(JSON_KEY_VALUE); + } + } + catch (...) + { + Logger::warn("Failed to initialize CursorWrap disable on single monitor from settings. Will use default value (false)"); + } } else { @@ -646,7 +663,8 @@ private: POINT newPos = g_cursorWrapInstance->m_core.HandleMouseMove( currentPos, g_cursorWrapInstance->m_disableWrapDuringDrag, - g_cursorWrapInstance->m_wrapMode); + g_cursorWrapInstance->m_wrapMode, + g_cursorWrapInstance->m_disableOnSingleMonitor); if (newPos.x != currentPos.x || newPos.y != currentPos.y) { diff --git a/src/settings-ui/Settings.UI.Library/CursorWrapProperties.cs b/src/settings-ui/Settings.UI.Library/CursorWrapProperties.cs index bffa75a3f3..228cf74998 100644 --- a/src/settings-ui/Settings.UI.Library/CursorWrapProperties.cs +++ b/src/settings-ui/Settings.UI.Library/CursorWrapProperties.cs @@ -25,12 +25,16 @@ namespace Microsoft.PowerToys.Settings.UI.Library [JsonPropertyName("wrap_mode")] public IntProperty WrapMode { get; set; } + [JsonPropertyName("disable_cursor_wrap_on_single_monitor")] + public BoolProperty DisableCursorWrapOnSingleMonitor { get; set; } + public CursorWrapProperties() { ActivationShortcut = DefaultActivationShortcut; AutoActivate = new BoolProperty(false); DisableWrapDuringDrag = new BoolProperty(true); WrapMode = new IntProperty(0); // 0=Both (default), 1=VerticalOnly, 2=HorizontalOnly + DisableCursorWrapOnSingleMonitor = new BoolProperty(false); } } } diff --git a/src/settings-ui/Settings.UI.Library/CursorWrapSettings.cs b/src/settings-ui/Settings.UI.Library/CursorWrapSettings.cs index 0ee6c4a523..fc918c37db 100644 --- a/src/settings-ui/Settings.UI.Library/CursorWrapSettings.cs +++ b/src/settings-ui/Settings.UI.Library/CursorWrapSettings.cs @@ -56,6 +56,13 @@ namespace Microsoft.PowerToys.Settings.UI.Library settingsUpgraded = true; } + // Add DisableCursorWrapOnSingleMonitor property if it doesn't exist (for users upgrading from older versions) + if (Properties.DisableCursorWrapOnSingleMonitor == null) + { + Properties.DisableCursorWrapOnSingleMonitor = new BoolProperty(false); // Default to false + settingsUpgraded = true; + } + return settingsUpgraded; } } diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/MouseUtilsPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/MouseUtilsPage.xaml index 39c3800f93..1ded2db636 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/MouseUtilsPage.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/MouseUtilsPage.xaml @@ -54,6 +54,9 @@ + + + diff --git a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw index a11cbd72bc..cd53ee4dec 100644 --- a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw +++ b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw @@ -2726,6 +2726,9 @@ From there, simply click on one of the supported files in the File Explorer and Disable wrapping while dragging + + Disable wrapping when using a single monitor + Auto-activate on startup diff --git a/src/settings-ui/Settings.UI/ViewModels/MouseUtilsViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/MouseUtilsViewModel.cs index 11045e0108..bd6a431a20 100644 --- a/src/settings-ui/Settings.UI/ViewModels/MouseUtilsViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/MouseUtilsViewModel.cs @@ -116,6 +116,9 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels // Null-safe access in case property wasn't upgraded yet - default to 0 (Both) _cursorWrapWrapMode = CursorWrapSettingsConfig.Properties.WrapMode?.Value ?? 0; + // Null-safe access in case property wasn't upgraded yet - default to false + _cursorWrapDisableOnSingleMonitor = CursorWrapSettingsConfig.Properties.DisableCursorWrapOnSingleMonitor?.Value ?? false; + int isEnabled = 0; Utilities.NativeMethods.SystemParametersInfo(Utilities.NativeMethods.SPI_GETCLIENTAREAANIMATION, 0, ref isEnabled, 0); @@ -1114,6 +1117,34 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels } } + public bool CursorWrapDisableOnSingleMonitor + { + get + { + return _cursorWrapDisableOnSingleMonitor; + } + + set + { + if (value != _cursorWrapDisableOnSingleMonitor) + { + _cursorWrapDisableOnSingleMonitor = value; + + // Ensure the property exists before setting value + if (CursorWrapSettingsConfig.Properties.DisableCursorWrapOnSingleMonitor == null) + { + CursorWrapSettingsConfig.Properties.DisableCursorWrapOnSingleMonitor = new BoolProperty(value); + } + else + { + CursorWrapSettingsConfig.Properties.DisableCursorWrapOnSingleMonitor.Value = value; + } + + NotifyCursorWrapPropertyChanged(); + } + } + } + public void NotifyCursorWrapPropertyChanged([CallerMemberName] string propertyName = null) { OnPropertyChanged(propertyName); @@ -1186,5 +1217,6 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels private bool _cursorWrapAutoActivate; private bool _cursorWrapDisableWrapDuringDrag; // Will be initialized in constructor from settings private int _cursorWrapWrapMode; // 0=Both, 1=VerticalOnly, 2=HorizontalOnly + private bool _cursorWrapDisableOnSingleMonitor; // Disable cursor wrap when only one monitor is connected } }