Add Cursor Wrap functionality to Powertoys Mouse Utils (#41826)

## Summary of the Pull Request

Cursor Wrap makes it simple to move the mouse from one edge of a display
(or set of displays) to the opposite edge of the display stack - on a
single display Cursor Wrap will wrap top/bottom and left/right edges.


https://github.com/user-attachments/assets/3feb606c-142b-4dab-9824-7597833d3ba4


## PR Checklist

- [x] Closes: CursorWrap #41759
- [x] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [x] **Tests:** Added/updated and all pass
- [x] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [x] **New binaries:** Added on the required places
- [x] [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
PR adds a new mouse utils module, this is 'Cursor Wrap' - Cursor Wrap
works with 1-9 monitors based on the logical monitor layout of the PC -
for a single monitor device the cursor is wrapped for the top/bottom and
left/right edges of the display - for a multi-monitor setup the cursor
is wrapped on the top/bottom left/right of the displays in the logical
display layout.

## Validation Steps Performed
Validation has been performed on a Surface Laptop 7 Pro (Intel) with a
single display and with an HDMI USB-C second display configured to be a
second monitor in top/left/right/bottom configuration - there are also
tests that run as part of the build to validate logical monitor layout
and cursor positioning.

---------

Co-authored-by: Niels Laute <niels.laute@live.nl>
Co-authored-by: Kai Tao (from Dev Box) <kaitao@microsoft.com>
Co-authored-by: Gordon Lam (SH) <yeelam@microsoft.com>
This commit is contained in:
Mike Hall
2025-11-05 03:28:25 -08:00
committed by GitHub
parent 1ad468641b
commit cd988b798b
31 changed files with 1888 additions and 9 deletions

View File

@@ -29,7 +29,9 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
private MousePointerCrosshairsSettings MousePointerCrosshairsSettingsConfig { get; set; }
public MouseUtilsViewModel(ISettingsUtils settingsUtils, ISettingsRepository<GeneralSettings> settingsRepository, ISettingsRepository<FindMyMouseSettings> findMyMouseSettingsRepository, ISettingsRepository<MouseHighlighterSettings> mouseHighlighterSettingsRepository, ISettingsRepository<MouseJumpSettings> mouseJumpSettingsRepository, ISettingsRepository<MousePointerCrosshairsSettings> mousePointerCrosshairsSettingsRepository, Func<string, int> ipcMSGCallBackFunc)
private CursorWrapSettings CursorWrapSettingsConfig { get; set; }
public MouseUtilsViewModel(ISettingsUtils settingsUtils, ISettingsRepository<GeneralSettings> settingsRepository, ISettingsRepository<FindMyMouseSettings> findMyMouseSettingsRepository, ISettingsRepository<MouseHighlighterSettings> mouseHighlighterSettingsRepository, ISettingsRepository<MouseJumpSettings> mouseJumpSettingsRepository, ISettingsRepository<MousePointerCrosshairsSettings> mousePointerCrosshairsSettingsRepository, ISettingsRepository<CursorWrapSettings> cursorWrapSettingsRepository, Func<string, int> ipcMSGCallBackFunc)
{
SettingsUtils = settingsUtils;
@@ -103,6 +105,14 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
_mousePointerCrosshairsOrientation = MousePointerCrosshairsSettingsConfig.Properties.CrosshairsOrientation.Value;
_mousePointerCrosshairsAutoActivate = MousePointerCrosshairsSettingsConfig.Properties.AutoActivate.Value;
ArgumentNullException.ThrowIfNull(cursorWrapSettingsRepository);
CursorWrapSettingsConfig = cursorWrapSettingsRepository.SettingsConfig;
_cursorWrapAutoActivate = CursorWrapSettingsConfig.Properties.AutoActivate.Value;
// Null-safe access in case property wasn't upgraded yet - default to TRUE
_cursorWrapDisableWrapDuringDrag = CursorWrapSettingsConfig.Properties.DisableWrapDuringDrag?.Value ?? true;
int isEnabled = 0;
Utilities.NativeMethods.SystemParametersInfo(Utilities.NativeMethods.SPI_GETCLIENTAREAANIMATION, 0, ref isEnabled, 0);
@@ -144,13 +154,25 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
if (_mousePointerCrosshairsEnabledGpoRuleConfiguration == GpoRuleConfigured.Disabled || _mousePointerCrosshairsEnabledGpoRuleConfiguration == GpoRuleConfigured.Enabled)
{
// Get the enabled state from GPO.
_mousePointerCrosshairsEnabledStateIsGPOConfigured = true;
_mousePointerCrosshairsEnabledStateGPOConfigured = true;
_isMousePointerCrosshairsEnabled = _mousePointerCrosshairsEnabledGpoRuleConfiguration == GpoRuleConfigured.Enabled;
}
else
{
_isMousePointerCrosshairsEnabled = GeneralSettingsConfig.Enabled.MousePointerCrosshairs;
}
_cursorWrapEnabledGpoRuleConfiguration = GPOWrapper.GetConfiguredCursorWrapEnabledValue();
if (_cursorWrapEnabledGpoRuleConfiguration == GpoRuleConfigured.Disabled || _cursorWrapEnabledGpoRuleConfiguration == GpoRuleConfigured.Enabled)
{
// Get the enabled state from GPO.
_cursorWrapEnabledStateIsGPOConfigured = true;
_isCursorWrapEnabled = _cursorWrapEnabledGpoRuleConfiguration == GpoRuleConfigured.Enabled;
}
else
{
_isCursorWrapEnabled = GeneralSettingsConfig.Enabled.CursorWrap;
}
}
public override Dictionary<string, HotkeySettings[]> GetAllHotkeySettings()
@@ -163,6 +185,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
MousePointerCrosshairsActivationShortcut,
GlidingCursorActivationShortcut],
[MouseJumpSettings.ModuleName] = [MouseJumpActivationShortcut],
[CursorWrapSettings.ModuleName] = [CursorWrapActivationShortcut],
};
return hotkeysDict;
@@ -663,7 +686,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
get => _isMousePointerCrosshairsEnabled;
set
{
if (_mousePointerCrosshairsEnabledStateIsGPOConfigured)
if (_mousePointerCrosshairsEnabledStateGPOConfigured)
{
// If it's GPO configured, shouldn't be able to change this state.
return;
@@ -686,7 +709,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
public bool IsMousePointerCrosshairsEnabledGpoConfigured
{
get => _mousePointerCrosshairsEnabledStateIsGPOConfigured;
get => _mousePointerCrosshairsEnabledStateGPOConfigured;
}
public HotkeySettings MousePointerCrosshairsActivationShortcut
@@ -959,6 +982,110 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
SettingsUtils.SaveSettings(MousePointerCrosshairsSettingsConfig.ToJsonString(), MousePointerCrosshairsSettings.ModuleName);
}
public bool IsCursorWrapEnabled
{
get => _isCursorWrapEnabled;
set
{
if (_cursorWrapEnabledStateIsGPOConfigured)
{
// If it's GPO configured, shouldn't be able to change this state.
return;
}
if (_isCursorWrapEnabled != value)
{
_isCursorWrapEnabled = value;
GeneralSettingsConfig.Enabled.CursorWrap = value;
OnPropertyChanged(nameof(IsCursorWrapEnabled));
OutGoingGeneralSettings outgoing = new OutGoingGeneralSettings(GeneralSettingsConfig);
SendConfigMSG(outgoing.ToString());
NotifyCursorWrapPropertyChanged();
}
}
}
public bool IsCursorWrapEnabledGpoConfigured
{
get => _cursorWrapEnabledStateIsGPOConfigured;
}
public HotkeySettings CursorWrapActivationShortcut
{
get
{
return CursorWrapSettingsConfig.Properties.ActivationShortcut;
}
set
{
if (CursorWrapSettingsConfig.Properties.ActivationShortcut != value)
{
CursorWrapSettingsConfig.Properties.ActivationShortcut = value ?? CursorWrapSettingsConfig.Properties.DefaultActivationShortcut;
NotifyCursorWrapPropertyChanged();
}
}
}
public bool CursorWrapAutoActivate
{
get
{
return _cursorWrapAutoActivate;
}
set
{
if (value != _cursorWrapAutoActivate)
{
_cursorWrapAutoActivate = value;
CursorWrapSettingsConfig.Properties.AutoActivate.Value = value;
NotifyCursorWrapPropertyChanged();
}
}
}
public bool CursorWrapDisableWrapDuringDrag
{
get
{
return _cursorWrapDisableWrapDuringDrag;
}
set
{
if (value != _cursorWrapDisableWrapDuringDrag)
{
_cursorWrapDisableWrapDuringDrag = value;
// Ensure the property exists before setting value
if (CursorWrapSettingsConfig.Properties.DisableWrapDuringDrag == null)
{
CursorWrapSettingsConfig.Properties.DisableWrapDuringDrag = new BoolProperty(value);
}
else
{
CursorWrapSettingsConfig.Properties.DisableWrapDuringDrag.Value = value;
}
NotifyCursorWrapPropertyChanged();
}
}
}
public void NotifyCursorWrapPropertyChanged([CallerMemberName] string propertyName = null)
{
OnPropertyChanged(propertyName);
SndCursorWrapSettings outsettings = new SndCursorWrapSettings(CursorWrapSettingsConfig);
SndModuleSettings<SndCursorWrapSettings> ipcMessage = new SndModuleSettings<SndCursorWrapSettings>(outsettings);
SendConfigMSG(ipcMessage.ToJsonString());
SettingsUtils.SaveSettings(CursorWrapSettingsConfig.ToJsonString(), CursorWrapSettings.ModuleName);
}
public void RefreshEnabledState()
{
InitializeEnabledValues();
@@ -966,6 +1093,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
OnPropertyChanged(nameof(IsMouseHighlighterEnabled));
OnPropertyChanged(nameof(IsMouseJumpEnabled));
OnPropertyChanged(nameof(IsMousePointerCrosshairsEnabled));
OnPropertyChanged(nameof(IsCursorWrapEnabled));
}
private Func<string, int> SendConfigMSG { get; }
@@ -999,7 +1127,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
private bool _highlighterAutoActivate;
private GpoRuleConfigured _mousePointerCrosshairsEnabledGpoRuleConfiguration;
private bool _mousePointerCrosshairsEnabledStateIsGPOConfigured;
private bool _mousePointerCrosshairsEnabledStateGPOConfigured;
private bool _isMousePointerCrosshairsEnabled;
private string _mousePointerCrosshairsColor;
private int _mousePointerCrosshairsOpacity;
@@ -1013,5 +1141,11 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
private int _mousePointerCrosshairsOrientation;
private bool _mousePointerCrosshairsAutoActivate;
private bool _isAnimationEnabledBySystem;
private GpoRuleConfigured _cursorWrapEnabledGpoRuleConfiguration;
private bool _cursorWrapEnabledStateIsGPOConfigured;
private bool _isCursorWrapEnabled;
private bool _cursorWrapAutoActivate;
private bool _cursorWrapDisableWrapDuringDrag; // Will be initialized in constructor from settings
}
}