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