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