Compare commits

...

4 Commits

Author SHA1 Message Date
Shawn Yuan (from Dev Box)
b71bce9c6c Prevent from winuiEx crash 2026-02-06 15:22:08 +08:00
Jiří Polášek
753689309e CmdPal: Fix alias UI clearing when toggling Direct/Indirect combobox (#45381)
## Summary of the Pull Request

This PR fixes the settings UI for the top-level command alias.



<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist


https://github.com/user-attachments/assets/0d1e1392-0293-4482-97cb-e8e8c0ed0dd5


- [x] Closes: #41301
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [ ] **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

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2026-02-05 16:47:45 -06:00
Thanh Nguyen
2be4c4eb46 Fix CursorWrap "Automatically activate on utility startup" setting not persisting (#45210)
## Summary of the Pull Request

Fixes #45185 - CursorWrap "Automatically activate on utility startup"
setting cannot be disabled, and prevents spurious activation on startup.

## PR Checklist

- [x] Closes: #45185
- [x] **Communication:** Issue was reported by community; fix follows
established patterns from MousePointerCrosshairs
- [x] **Tests:** Manual validation performed by contributor (video
available)
- [x] **Localization:** No new user-facing strings added
- [ ] **Dev docs:** N/A - bug fix only
- [ ] **New binaries:** N/A - no new binaries
- [ ] **Documentation updated:** N/A - bug fix only

## Detailed Description of the Pull Request / Additional comments

### Problem

Users reported that disabling the "Automatically activate on utility
startup" setting for CursorWrap does not work - the mouse hook always
starts automatically regardless of the setting value.

### Root Causes

1. **`dllmain.cpp` `enable()` method**: `StartMouseHook()` was always
called unconditionally, ignoring `m_autoActivate`.
2. **`MouseUtilsViewModel.cs` `IsCursorWrapEnabled` setter**: enabling
CursorWrap forced `AutoActivate = true`, overriding the user's
preference.
3. **Startup edge case**: the trigger event could remain signaled from a
previous session, immediately toggling CursorWrap on startup even when
AutoActivate is off.

### Solution

1. **`src/modules/MouseUtils/CursorWrap/dllmain.cpp`**: only start the
mouse hook if `m_autoActivate` is true.
2. **`src/settings-ui/Settings.UI/ViewModels/MouseUtilsViewModel.cs`**:
remove the line that forced `AutoActivate = true` when enabling
CursorWrap.
3. **`src/modules/MouseUtils/CursorWrap/dllmain.cpp`**: reset the
trigger event on enable to avoid immediate activation on startup.

### Pattern Reference

This fix follows the same pattern used by **MousePointerCrosshairs**
module which has a similar `AutoActivate` setting that works correctly.

## Validation Steps Performed

### Build

- `tools\build\build.ps1 -Platform x64 -Configuration Debug`

### Manual validation (contributor)

#### Test Case 1: AutoActivate = false (should NOT auto-start mouse
hook)

1. Open PowerToys Settings → Mouse Utilities → Cursor Wrap
2. Enable Cursor Wrap
3. **Disable** "Automatically activate on utility startup"
4. Close PowerToys completely (right-click tray icon → Exit)
5. Restart PowerToys
6. **Expected Result**: CursorWrap module is loaded but mouse hook is
NOT active - cursor does NOT wrap at screen edges
7. Press activation hotkey (default: `Win+Alt+U`)
8. **Expected Result**: Mouse hook activates, cursor now wraps at screen
edges
9. **Actual Result**:  Works as expected

#### Test Case 2: AutoActivate = true (should auto-start mouse hook)

1. Open PowerToys Settings → Mouse Utilities → Cursor Wrap
2. Enable Cursor Wrap
3. **Enable** "Automatically activate on utility startup"
4. Close PowerToys completely
5. Restart PowerToys
6. **Expected Result**: Mouse hook is immediately active, cursor wraps
at screen edges without pressing hotkey
7. **Actual Result**:  Works as expected

#### Test Case 3: Setting persistence after restart

1. Set AutoActivate = false, restart PowerToys
2. Open Settings and verify AutoActivate is still false
3. Set AutoActivate = true, restart PowerToys
4. Open Settings and verify AutoActivate is still true
5. **Actual Result**:  Setting persists correctly

#### Test Case 4: Hotkey toggle works correctly

1. With AutoActivate = false, restart PowerToys
2. Press hotkey → cursor should start wrapping
3. Press hotkey again → cursor should stop wrapping
4. **Actual Result**:  Hotkey toggle works correctly

---

**Note**: Video demonstration available from contributor.
2026-02-05 19:58:49 +08:00
Mike Hall
731532fdd8 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.

<!-- Please review the items on the PR checklist before submitting-->
## 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 <niels.laute@live.nl>
2026-02-05 18:37:10 +08:00
13 changed files with 117 additions and 15 deletions

View File

@@ -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))
{

View File

@@ -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<MonitorInfo>& GetMonitors() const { return m_monitors; }
size_t GetMonitorCount() const { return m_monitors.size(); }
const MonitorTopology& GetTopology() const { return m_topology; }
private:

View File

@@ -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
@@ -196,6 +198,10 @@ public:
// Start listening for external trigger event so we can invoke the same logic as the activation hotkey.
m_triggerEventHandle = CreateEventW(nullptr, false, false, CommonSharedConstants::CURSOR_WRAP_TRIGGER_EVENT);
m_terminateEventHandle = CreateEventW(nullptr, false, false, nullptr);
if (m_triggerEventHandle)
{
ResetEvent(m_triggerEventHandle);
}
if (m_triggerEventHandle && m_terminateEventHandle)
{
m_listening = true;
@@ -210,8 +216,16 @@ public:
// Create message window for display change notifications
RegisterForDisplayChanges();
StartMouseHook();
Logger::info("CursorWrap enabled - mouse hook started");
// Only start the mouse hook automatically if auto-activate is enabled
if (m_autoActivate)
{
StartMouseHook();
Logger::info("CursorWrap enabled - mouse hook started (auto-activate on)");
}
else
{
Logger::info("CursorWrap enabled - waiting for activation hotkey (auto-activate off)");
}
while (m_listening)
{
@@ -415,6 +429,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 +675,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)
{

View File

@@ -107,7 +107,7 @@ public partial class AliasManager : ObservableObject
}
// Look for the alias belonging to another command, and remove it
if (newAlias is not null && kv.Value.Alias == newAlias.Alias)
if (newAlias is not null && kv.Value.Alias == newAlias.Alias && kv.Value.CommandId != commandId)
{
toRemove.Add(kv.Value);

View File

@@ -8,7 +8,6 @@
IsMaximizable="False"
IsMinimizable="False"
IsResizable="False"
IsShownInSwitchers="False"
IsTitleBarVisible="False"
mc:Ignorable="d">
<Grid Background="#1A000000">

View File

@@ -2,6 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Threading.Tasks;
using Microsoft.UI.Windowing;
using Windows.Graphics;
@@ -24,6 +25,17 @@ namespace PowerDisplay.PowerDisplayXAML
{
InitializeComponent();
NumberText.Text = displayText;
try
{
this.SetIsShownInSwitchers(false);
}
catch (NotImplementedException)
{
// WinUI will throw if explorer is not running, safely ignore
}
catch (Exception)
{
}
// Configure window style
ConfigureWindow();

View File

@@ -15,7 +15,6 @@
IsMaximizable="False"
IsMinimizable="False"
IsResizable="False"
IsShownInSwitchers="False"
IsTitleBarVisible="False">
<winuiex:WindowEx.SystemBackdrop>
<DesktopAcrylicBackdrop />

View File

@@ -71,6 +71,10 @@ namespace PowerDisplay
_hotkeyService.Initialize(this);
Logger.LogTrace("MainWindow constructor: HotkeyService initialized");
Logger.LogTrace("MainWindow constructor: Setting IsShownInSwitchers property");
this.SetIsShownInSwitchers(false);
Logger.LogTrace("MainWindow constructor: Set IsShownInSwitchers property successfully");
// Note: ViewModel handles all async initialization internally.
// We listen to InitializationCompleted event to know when data is ready.
// No duplicate initialization here - single responsibility in ViewModel.

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -54,6 +54,9 @@
<ComboBoxItem x:Uid="MouseUtils_CursorWrap_WrapMode_HorizontalOnly" />
</ComboBox>
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard ContentAlignment="Left" IsEnabled="{x:Bind ViewModel.IsCursorWrapEnabled, Mode=OneWay}">
<CheckBox x:Uid="MouseUtils_CursorWrap_DisableOnSingleMonitor" IsChecked="{x:Bind ViewModel.CursorWrapDisableOnSingleMonitor, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
</tkcontrols:SettingsExpander.Items>
</tkcontrols:SettingsExpander>
</controls:SettingsGroup>

View File

@@ -2726,6 +2726,9 @@ From there, simply click on one of the supported files in the File Explorer and
<data name="MouseUtils_CursorWrap_DisableWrapDuringDrag.Content" xml:space="preserve">
<value>Disable wrapping while dragging</value>
</data>
<data name="MouseUtils_CursorWrap_DisableOnSingleMonitor.Content" xml:space="preserve">
<value>Disable wrapping when using a single monitor</value>
</data>
<data name="MouseUtils_CursorWrap_AutoActivate.Header" xml:space="preserve">
<value>Auto-activate on startup</value>
</data>

View File

@@ -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);
@@ -1003,13 +1006,6 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
GeneralSettingsConfig.Enabled.CursorWrap = value;
OnPropertyChanged(nameof(IsCursorWrapEnabled));
// Auto-enable the AutoActivate setting when CursorWrap is enabled
// This ensures cursor wrapping is active immediately after enabling
if (value && !_cursorWrapAutoActivate)
{
CursorWrapAutoActivate = true;
}
OutGoingGeneralSettings outgoing = new OutGoingGeneralSettings(GeneralSettingsConfig);
SendConfigMSG(outgoing.ToString());
@@ -1114,6 +1110,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 +1210,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
}
}