From a8596fed3daca60834ebf5238d52762513b1da1d Mon Sep 17 00:00:00 2001 From: Mike Hall Date: Mon, 29 Sep 2025 05:14:16 +0100 Subject: [PATCH] Add key to cancel gliding cursor (#41985) ## Summary of the Pull Request Add low level keyboard hook to Gliding Cursor, this checks for 'Esc' being pressed when gliding is active and cancels gliding, the mouse hook passed keys down the hook chain. ## PR Checklist - [x] Closes: #41972 - [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 - [x] **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 No changes to the list of binaries, no new strings for localization. The Gliding Cursor functionality has 5 stages, these are: fast horizontal, slow horizontal, fast vertical, slow vertical, and mouse click - adding the keyboard hook and checking for 'Esc' allows this sequence to be interrupted and reset to ready state. ## Validation Steps Performed Validated Mouse Pointer Crosshairs (which Gliding Cursor is based on), confirmed that Gliding Cursor functionality is unchanged and that the 'Esc' key cancels/resets the gliding cursor state. --------- Co-authored-by: Gordon Lam (SH) --- .../MousePointerCrosshairs.vcxproj | 2 +- .../MousePointerCrosshairs/dllmain.cpp | 91 +++++++++++++++++-- 2 files changed, 86 insertions(+), 7 deletions(-) diff --git a/src/modules/MouseUtils/MousePointerCrosshairs/MousePointerCrosshairs.vcxproj b/src/modules/MouseUtils/MousePointerCrosshairs/MousePointerCrosshairs.vcxproj index 7da54a51e9..58668c663f 100644 --- a/src/modules/MouseUtils/MousePointerCrosshairs/MousePointerCrosshairs.vcxproj +++ b/src/modules/MouseUtils/MousePointerCrosshairs/MousePointerCrosshairs.vcxproj @@ -80,7 +80,7 @@ - $(SolutionDir)src\;$(SolutionDir)src\modules;$(SolutionDir)src\common\Telemetry;%(AdditionalIncludeDirectories) + ..\..\..\;..\..\..\modules;..\..\..\common\Telemetry;%(AdditionalIncludeDirectories) diff --git a/src/modules/MouseUtils/MousePointerCrosshairs/dllmain.cpp b/src/modules/MouseUtils/MousePointerCrosshairs/dllmain.cpp index c58bfc1de2..fd144e807b 100644 --- a/src/modules/MouseUtils/MousePointerCrosshairs/dllmain.cpp +++ b/src/modules/MouseUtils/MousePointerCrosshairs/dllmain.cpp @@ -64,6 +64,9 @@ const static wchar_t* MODULE_NAME = L"MousePointerCrosshairs"; // Add a description that will we shown in the module settings page. const static wchar_t* MODULE_DESC = L""; +class MousePointerCrosshairs; // fwd +static std::atomic g_instance{ nullptr }; // for hook callback + // Implement the PowerToy Module Interface and all the required methods. class MousePointerCrosshairs : public PowertoyModuleIface { @@ -72,8 +75,11 @@ private: bool m_enabled = false; // Additional hotkeys (legacy API) to support multiple shortcuts - Hotkey m_activationHotkey{}; // Crosshairs toggle - Hotkey m_glidingHotkey{}; // Gliding cursor state machine + Hotkey m_activationHotkey{}; // Crosshairs toggle + Hotkey m_glidingHotkey{}; // Gliding cursor state machine + + // Low-level keyboard hook (Escape to cancel gliding) + HHOOK m_keyboardHook = nullptr; // Shared state for worker threads (decoupled from this lifetime) struct State @@ -86,7 +92,7 @@ private: int currentYPos{ 0 }; int currentXSpeed{ 0 }; // pixels per base window int currentYSpeed{ 0 }; // pixels per base window - int xPosSnapshot{ 0 }; // xPos captured at end of horizontal scan + int xPosSnapshot{ 0 }; // xPos captured at end of horizontal scan // Fractional accumulators to spread movement across 10ms ticks double xFraction{ 0.0 }; @@ -94,9 +100,9 @@ private: // Speeds represent pixels per 200ms (min 5, max 60 enforced by UI/settings) int fastHSpeed{ 30 }; // pixels per base window - int slowHSpeed{ 5 }; // pixels per base window + int slowHSpeed{ 5 }; // pixels per base window int fastVSpeed{ 30 }; // pixels per base window - int slowVSpeed{ 5 }; // pixels per base window + int slowVSpeed{ 5 }; // pixels per base window }; std::shared_ptr m_state; @@ -122,13 +128,16 @@ public: LoggerHelpers::init_logger(MODULE_NAME, L"ModuleInterface", LogSettings::mousePointerCrosshairsLoggerName); m_state = std::make_shared(); init_settings(); + g_instance.store(this, std::memory_order_release); }; // Destroy the powertoy and free memory virtual void destroy() override { + UninstallKeyboardHook(); StopXTimer(); StopYTimer(); + g_instance.store(nullptr, std::memory_order_release); // Release shared state so worker threads (if any) exit when weak_ptr lock fails m_state.reset(); delete this; @@ -198,6 +207,7 @@ public: { m_enabled = false; Trace::EnableMousePointerCrosshairs(false); + UninstallKeyboardHook(); StopXTimer(); StopYTimer(); m_glideState = 0; @@ -222,7 +232,7 @@ public: if (buffer && buffer_size >= 2) { buffer[0] = m_activationHotkey; // Crosshairs toggle - buffer[1] = m_glidingHotkey; // Gliding cursor toggle + buffer[1] = m_glidingHotkey; // Gliding cursor toggle } return 2; } @@ -258,6 +268,27 @@ private: SendInput(2, inputs, sizeof(INPUT)); } + // Cancel gliding without performing the final click (Escape handling) + void CancelGliding() + { + int state = m_glideState.load(); + if (state == 0) + { + return; // nothing to cancel + } + StopXTimer(); + StopYTimer(); + m_glideState = 0; + InclusiveCrosshairsEnsureOff(); + InclusiveCrosshairsSetExternalControl(false); + if (auto s = m_state) + { + s->xFraction = 0.0; + s->yFraction = 0.0; + } + Logger::debug("Gliding cursor cancelled via Escape key"); + } + // Stateless helpers operating on shared State static void PositionCursorX(const std::shared_ptr& s) { @@ -400,6 +431,8 @@ private: { case 0: { + // For detect for cancel key + InstallKeyboardHook(); // Ensure crosshairs on (do not toggle off if already on) InclusiveCrosshairsEnsureOn(); // Disable internal mouse hook so we control position updates explicitly @@ -448,6 +481,7 @@ private: case 4: default: { + UninstallKeyboardHook(); // Stop vertical, click, turn crosshairs off, re-enable internal tracking, reset state StopYTimer(); m_glideState = 0; @@ -463,6 +497,51 @@ private: } } + // Low-level keyboard hook procedures + static LRESULT CALLBACK LowLevelKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) + { + if (nCode == HC_ACTION) + { + const KBDLLHOOKSTRUCT* kb = reinterpret_cast(lParam); + if (kb && kb->vkCode == VK_ESCAPE && (wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN)) + { + if (auto inst = g_instance.load(std::memory_order_acquire)) + { + if (inst->m_enabled && inst->m_glideState.load() != 0) + { + inst->UninstallKeyboardHook(); + inst->CancelGliding(); + } + } + } + } + + // Do not swallow Escape; pass it through + return CallNextHookEx(nullptr, nCode, wParam, lParam); + } + + void InstallKeyboardHook() + { + if (m_keyboardHook) + { + return; // already installed + } + m_keyboardHook = SetWindowsHookEx(WH_KEYBOARD_LL, LowLevelKeyboardProc, m_hModule, 0); + if (!m_keyboardHook) + { + Logger::error("Failed to install low-level keyboard hook for MousePointerCrosshairs (Escape cancel). GetLastError={}.", GetLastError()); + } + } + + void UninstallKeyboardHook() + { + if (m_keyboardHook) + { + UnhookWindowsHookEx(m_keyboardHook); + m_keyboardHook = nullptr; + } + } + // Load the settings file. void init_settings() {