From d64f06906cfe4b116547a3b83747c50b232eeae3 Mon Sep 17 00:00:00 2001 From: Mike Hall Date: Thu, 23 Oct 2025 11:21:01 +0100 Subject: [PATCH] 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 --- .../MousePointerCrosshairs/dllmain.cpp | 98 ++++++++++++------- 1 file changed, 63 insertions(+), 35 deletions(-) diff --git a/src/modules/MouseUtils/MousePointerCrosshairs/dllmain.cpp b/src/modules/MouseUtils/MousePointerCrosshairs/dllmain.cpp index fd144e807b..b460e29643 100644 --- a/src/modules/MouseUtils/MousePointerCrosshairs/dllmain.cpp +++ b/src/modules/MouseUtils/MousePointerCrosshairs/dllmain.cpp @@ -14,6 +14,9 @@ extern void InclusiveCrosshairsRequestUpdatePosition(); extern void InclusiveCrosshairsEnsureOn(); extern void InclusiveCrosshairsEnsureOff(); extern void InclusiveCrosshairsSetExternalControl(bool enabled); +extern void InclusiveCrosshairsSetOrientation(CrosshairsOrientation orientation); +extern bool InclusiveCrosshairsIsEnabled(); +extern void InclusiveCrosshairsSwitch(); // Non-Localizable strings namespace @@ -244,12 +247,19 @@ public: 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(); return true; } - if (hotkeyId == 1) + if (hotkeyId == 1) // Gliding cursor activation { HandleGlidingHotkey(); return true; @@ -268,25 +278,44 @@ private: SendInput(2, inputs, sizeof(INPUT)); } - // Cancel gliding without performing the final click (Escape handling) - void CancelGliding() + // Cancel gliding with option to activate crosshairs in user's preferred orientation + void CancelGliding(bool activateCrosshairs) { int state = m_glideState.load(); if (state == 0) { return; // nothing to cancel } + + // Stop all gliding operations StopXTimer(); StopYTimer(); m_glideState = 0; - InclusiveCrosshairsEnsureOff(); + UninstallKeyboardHook(); + + // Reset crosshairs control and restore user settings 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) { s->xFraction = 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 @@ -425,21 +454,22 @@ private: { return; } - // Simulate the AHK state machine + int state = m_glideState.load(); switch (state) { - case 0: + case 0: // Starting gliding { - // For detect for cancel key + // Install keyboard hook for Escape cancellation InstallKeyboardHook(); - // Ensure crosshairs on (do not toggle off if already on) - InclusiveCrosshairsEnsureOn(); - // Disable internal mouse hook so we control position updates explicitly + + // 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); - // Override crosshairs to show both for Gliding Cursor InclusiveCrosshairsSetOrientation(CrosshairsOrientation::Both); + InclusiveCrosshairsEnsureOn(); // Always ensure they are visible + // Initialize gliding state s->currentXPos = 0; s->currentXSpeed = s->fastHSpeed; s->xFraction = 0.0; @@ -447,20 +477,17 @@ private: int y = GetSystemMetrics(SM_CYVIRTUALSCREEN) / 2; SetCursorPos(0, y); InclusiveCrosshairsRequestUpdatePosition(); + m_glideState = 1; StartXTimer(); break; } - case 1: - { - // Slow horizontal + case 1: // Slow horizontal s->currentXSpeed = s->slowHSpeed; m_glideState = 2; break; - } - case 2: + case 2: // Switch to vertical fast { - // Stop horizontal, start vertical (fast) StopXTimer(); s->currentYSpeed = s->fastVSpeed; s->currentYPos = 0; @@ -471,33 +498,37 @@ private: StartYTimer(); break; } - case 3: - { - // Slow vertical + case 3: // Slow vertical s->currentYSpeed = s->slowVSpeed; m_glideState = 4; break; - } - case 4: + case 4: // Finalize (click and end) default: { - UninstallKeyboardHook(); - // Stop vertical, click, turn crosshairs off, re-enable internal tracking, reset state + // Complete the gliding sequence StopYTimer(); m_glideState = 0; LeftClick(); - InclusiveCrosshairsEnsureOff(); + + // Restore normal crosshairs operation and turn them off InclusiveCrosshairsSetExternalControl(false); - // Restore original crosshairs orientation setting InclusiveCrosshairsSetOrientation(m_inclusiveCrosshairsSettings.crosshairsOrientation); - s->xFraction = 0.0; - s->yFraction = 0.0; + InclusiveCrosshairsEnsureOff(); + + UninstallKeyboardHook(); + + // Reset state + if (auto sp = m_state) + { + sp->xFraction = 0.0; + sp->yFraction = 0.0; + } break; } } } - // Low-level keyboard hook procedures + // Low-level keyboard hook for Escape cancellation static LRESULT CALLBACK LowLevelKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) { if (nCode == HC_ACTION) @@ -509,14 +540,11 @@ private: { if (inst->m_enabled && inst->m_glideState.load() != 0) { - inst->UninstallKeyboardHook(); - inst->CancelGliding(); + inst->CancelGliding(false); // Escape cancels without activating crosshairs } } } } - - // Do not swallow Escape; pass it through return CallNextHookEx(nullptr, nCode, wParam, lParam); }