Enable switching to and from MousePointerCrosshairs and Gliding Cursor (#42105)

## Summary of the Pull Request
This PR enables a user to switch between Mouse Pointer Crosshairs and
Gliding Cursor (or the other way round!). The primary change is to the
underlying state machine that's shared between Mouse Pointer Crosshairs
and Gliding Cursor, both are implemented in the same Mouse Module.

## PR Checklist

- [ ] Closes: #xxx
- [x] **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
See above - this is primarily a change to the shared state machine
between Mouse Pointer Crosshairs and Gliding Cursor - this change
enables transition between Mouse Pointer Crosshairs and Gliding Cursor,
the underlying state is reset when a user transitions from Gliding to
Mouse Pointer and back again.

## Validation Steps Performed
Validation on a Windows Surface Laptop 7 Pro for the following states.
- Mouse Pointer Crosshairs and Gliding Cursor NOT active -
enable/disable Mouse Pointer Crosshairs
- Mouse Pointer Crosshairs and Gliding Cursor NOT active - enable/step
states for Gliding Cursor
- Activate and disable Mouse Pointer Crosshairs
- Activate and step through Gliding Cursor
- Mouse Pointer Crosshairs Active - Switch to Gliding Cursor
- Gliding Cursor Active - Switch to Mouse Pointer Crosshairs
This commit is contained in:
Mike Hall
2025-10-23 11:21:01 +01:00
committed by GitHub
parent c26dfef81b
commit d64f06906c

View File

@@ -14,6 +14,9 @@ extern void InclusiveCrosshairsRequestUpdatePosition();
extern void InclusiveCrosshairsEnsureOn(); extern void InclusiveCrosshairsEnsureOn();
extern void InclusiveCrosshairsEnsureOff(); extern void InclusiveCrosshairsEnsureOff();
extern void InclusiveCrosshairsSetExternalControl(bool enabled); extern void InclusiveCrosshairsSetExternalControl(bool enabled);
extern void InclusiveCrosshairsSetOrientation(CrosshairsOrientation orientation);
extern bool InclusiveCrosshairsIsEnabled();
extern void InclusiveCrosshairsSwitch();
// Non-Localizable strings // Non-Localizable strings
namespace namespace
@@ -244,12 +247,19 @@ public:
return false; return false;
} }
if (hotkeyId == 0) if (hotkeyId == 0) // Crosshairs activation
{ {
// If gliding cursor is active, cancel it and activate crosshairs
if (m_glideState.load() != 0)
{
CancelGliding(true /*activateCrosshairs*/);
return true;
}
// Otherwise, normal crosshairs toggle
InclusiveCrosshairsSwitch(); InclusiveCrosshairsSwitch();
return true; return true;
} }
if (hotkeyId == 1) if (hotkeyId == 1) // Gliding cursor activation
{ {
HandleGlidingHotkey(); HandleGlidingHotkey();
return true; return true;
@@ -268,25 +278,44 @@ private:
SendInput(2, inputs, sizeof(INPUT)); SendInput(2, inputs, sizeof(INPUT));
} }
// Cancel gliding without performing the final click (Escape handling) // Cancel gliding with option to activate crosshairs in user's preferred orientation
void CancelGliding() void CancelGliding(bool activateCrosshairs)
{ {
int state = m_glideState.load(); int state = m_glideState.load();
if (state == 0) if (state == 0)
{ {
return; // nothing to cancel return; // nothing to cancel
} }
// Stop all gliding operations
StopXTimer(); StopXTimer();
StopYTimer(); StopYTimer();
m_glideState = 0; m_glideState = 0;
InclusiveCrosshairsEnsureOff(); UninstallKeyboardHook();
// Reset crosshairs control and restore user settings
InclusiveCrosshairsSetExternalControl(false); InclusiveCrosshairsSetExternalControl(false);
InclusiveCrosshairsSetOrientation(m_inclusiveCrosshairsSettings.crosshairsOrientation);
if (activateCrosshairs)
{
// User is switching to crosshairs mode - enable with their settings
InclusiveCrosshairsEnsureOn();
}
else
{
// User canceled (Escape) - turn off crosshairs completely
InclusiveCrosshairsEnsureOff();
}
// Reset gliding state
if (auto s = m_state) if (auto s = m_state)
{ {
s->xFraction = 0.0; s->xFraction = 0.0;
s->yFraction = 0.0; s->yFraction = 0.0;
} }
Logger::debug("Gliding cursor cancelled via Escape key");
Logger::debug("Gliding cursor cancelled (activateCrosshairs={})", activateCrosshairs ? 1 : 0);
} }
// Stateless helpers operating on shared State // Stateless helpers operating on shared State
@@ -425,21 +454,22 @@ private:
{ {
return; return;
} }
// Simulate the AHK state machine
int state = m_glideState.load(); int state = m_glideState.load();
switch (state) switch (state)
{ {
case 0: case 0: // Starting gliding
{ {
// For detect for cancel key // Install keyboard hook for Escape cancellation
InstallKeyboardHook(); InstallKeyboardHook();
// Ensure crosshairs on (do not toggle off if already on)
InclusiveCrosshairsEnsureOn();
// Disable internal mouse hook so we control position updates explicitly
InclusiveCrosshairsSetExternalControl(true);
// Override crosshairs to show both for Gliding Cursor
InclusiveCrosshairsSetOrientation(CrosshairsOrientation::Both);
// Force crosshairs visible in BOTH orientation for gliding, regardless of user setting
// Set external control before enabling to prevent internal movement hook from attaching
InclusiveCrosshairsSetExternalControl(true);
InclusiveCrosshairsSetOrientation(CrosshairsOrientation::Both);
InclusiveCrosshairsEnsureOn(); // Always ensure they are visible
// Initialize gliding state
s->currentXPos = 0; s->currentXPos = 0;
s->currentXSpeed = s->fastHSpeed; s->currentXSpeed = s->fastHSpeed;
s->xFraction = 0.0; s->xFraction = 0.0;
@@ -447,20 +477,17 @@ private:
int y = GetSystemMetrics(SM_CYVIRTUALSCREEN) / 2; int y = GetSystemMetrics(SM_CYVIRTUALSCREEN) / 2;
SetCursorPos(0, y); SetCursorPos(0, y);
InclusiveCrosshairsRequestUpdatePosition(); InclusiveCrosshairsRequestUpdatePosition();
m_glideState = 1; m_glideState = 1;
StartXTimer(); StartXTimer();
break; break;
} }
case 1: case 1: // Slow horizontal
{
// Slow horizontal
s->currentXSpeed = s->slowHSpeed; s->currentXSpeed = s->slowHSpeed;
m_glideState = 2; m_glideState = 2;
break; break;
} case 2: // Switch to vertical fast
case 2:
{ {
// Stop horizontal, start vertical (fast)
StopXTimer(); StopXTimer();
s->currentYSpeed = s->fastVSpeed; s->currentYSpeed = s->fastVSpeed;
s->currentYPos = 0; s->currentYPos = 0;
@@ -471,33 +498,37 @@ private:
StartYTimer(); StartYTimer();
break; break;
} }
case 3: case 3: // Slow vertical
{
// Slow vertical
s->currentYSpeed = s->slowVSpeed; s->currentYSpeed = s->slowVSpeed;
m_glideState = 4; m_glideState = 4;
break; break;
} case 4: // Finalize (click and end)
case 4:
default: default:
{ {
UninstallKeyboardHook(); // Complete the gliding sequence
// Stop vertical, click, turn crosshairs off, re-enable internal tracking, reset state
StopYTimer(); StopYTimer();
m_glideState = 0; m_glideState = 0;
LeftClick(); LeftClick();
InclusiveCrosshairsEnsureOff();
// Restore normal crosshairs operation and turn them off
InclusiveCrosshairsSetExternalControl(false); InclusiveCrosshairsSetExternalControl(false);
// Restore original crosshairs orientation setting
InclusiveCrosshairsSetOrientation(m_inclusiveCrosshairsSettings.crosshairsOrientation); InclusiveCrosshairsSetOrientation(m_inclusiveCrosshairsSettings.crosshairsOrientation);
s->xFraction = 0.0; InclusiveCrosshairsEnsureOff();
s->yFraction = 0.0;
UninstallKeyboardHook();
// Reset state
if (auto sp = m_state)
{
sp->xFraction = 0.0;
sp->yFraction = 0.0;
}
break; break;
} }
} }
} }
// Low-level keyboard hook procedures // Low-level keyboard hook for Escape cancellation
static LRESULT CALLBACK LowLevelKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) static LRESULT CALLBACK LowLevelKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam)
{ {
if (nCode == HC_ACTION) if (nCode == HC_ACTION)
@@ -509,14 +540,11 @@ private:
{ {
if (inst->m_enabled && inst->m_glideState.load() != 0) if (inst->m_enabled && inst->m_glideState.load() != 0)
{ {
inst->UninstallKeyboardHook(); inst->CancelGliding(false); // Escape cancels without activating crosshairs
inst->CancelGliding();
} }
} }
} }
} }
// Do not swallow Escape; pass it through
return CallNextHookEx(nullptr, nCode, wParam, lParam); return CallNextHookEx(nullptr, nCode, wParam, lParam);
} }