From df08d98a815d0f3a2b8dba7ebe15643d8fecb881 Mon Sep 17 00:00:00 2001 From: Mike Hall Date: Thu, 21 Aug 2025 06:53:20 +0100 Subject: [PATCH] Implement "Gliding cursor" accessibility feature (#41221) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary of the Pull Request Added '[Gliding Cursor](https://github.com/microsoft/PowerToys/issues/37097)' functionality to Mouse Pointer Crosshairs, this enables a single hotkey/Microsoft Adaptive Hub + button to control cursor movement and clicking. This is implemented as an extension to the existing Mouse Pointer Crosshairs module. Testing has been manual, ensuring that the existing Mouse Pointer Crosshairs functionality is unchanged, and that the new Gliding Cursor functionality works alongside Mouse Pointer Crosshairs. ![FlowPointer2](https://github.com/user-attachments/assets/ede40fe5-d749-45d1-bd8d-627dda2927a3) image To test this functionality: - Open Mouse Crosshair settings and make sure the feature is enabled. - Press the shortcut to start the gliding cursor — a vertical line appears. - Press the shortcut again to slow the vertical line. - Press once more to fix the vertical line; a horizontal line begins moving. - Press again to slow the horizontal line. - When the lines meet at your target, press the shortcut to perform the click. ## PR Checklist - [x] Closes: #37097 - [ ] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [x] **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 The PR includes these changes: * Updated Mouse Pointer Crosshairs XAML to include a new hotkey to start the gliding cursor experience * Added two sliders for fast/slow cursor movement * mapped the new hotkey/XAML sliders through to the existing MousePointerHotkeys project, dllmain.cpp * Added a 10ms tick for Gliding cursor for crosshairs/cursor movement * Added state for gliding functionality - horiz fast, horiz slow, vert fast, vert slow, click * added gates around the existing mouse movement hook to prevent mouse movement when gliding ## Validation Steps Performed Manual testing has been completed on several PCs to confirm the following: * Existing Mouse Pointer Crosshairs functionality is unchanged * Gliding cursor settings are persisted/used by the gliding cursor code * Gliding cursor restores Mouse Pointer Crosshairs state after the final click has completed. --------- Signed-off-by: Shawn Yuan Co-authored-by: Niels Laute Co-authored-by: Shawn Yuan --- .github/actions/spell-check/expect.txt | 9 + doc/images/icons/Mouse Crosshairs.png | Bin 7618 -> 18721 bytes .../InclusiveCrosshairs.cpp | 95 +++- .../InclusiveCrosshairs.h | 4 + .../MousePointerCrosshairs/dllmain.cpp | 441 ++++++++++++++++-- .../MousePointerCrosshairsProperties.cs | 15 + .../MousePointerCrosshairsSettings.cs | 4 + .../Assets/Settings/Icons/MouseCrosshairs.png | Bin 1714 -> 1577 bytes .../SettingsXAML/Views/MouseUtilsPage.xaml | 21 + .../Settings.UI/Strings/en-us/Resources.resw | 21 +- .../ViewModels/MouseUtilsViewModel.cs | 47 +- 11 files changed, 613 insertions(+), 44 deletions(-) diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index 9911ff6d81..c275d6725f 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -32,6 +32,7 @@ AFeature affordances AFX AGGREGATABLE +AHK AHybrid akv ALarger @@ -667,6 +668,7 @@ HROW hsb HSCROLL hsi +HSpeed HTCLIENT hthumbnail HTOUCHINPUT @@ -1862,6 +1864,7 @@ VSINSTALLDIR VSM vso vsonline +VSpeed vstemplate vstest VSTHRD @@ -1998,10 +2001,13 @@ XNamespace Xoshiro XPels XPixel +XPos XResource xsi +XSpeed XStr xstyler +XTimer XUP XVIRTUALSCREEN xxxxxx @@ -2011,7 +2017,10 @@ YIncrement yinle yinyue YPels +YPos YResolution +YSpeed +YTimer YStr YVIRTUALSCREEN ZEROINIT diff --git a/doc/images/icons/Mouse Crosshairs.png b/doc/images/icons/Mouse Crosshairs.png index 6b1dcb9c1654bc82cb9d893d80ccf58b7b08c61a..a2c64a72a4596c36319c2f501194edf175082475 100644 GIT binary patch literal 18721 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 Eg1ndiRsaA1 literal 7618 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 GzW6^v$d+XQ diff --git a/src/modules/MouseUtils/MousePointerCrosshairs/InclusiveCrosshairs.cpp b/src/modules/MouseUtils/MousePointerCrosshairs/InclusiveCrosshairs.cpp index 937e9bfca3..61e292d7ee 100644 --- a/src/modules/MouseUtils/MousePointerCrosshairs/InclusiveCrosshairs.cpp +++ b/src/modules/MouseUtils/MousePointerCrosshairs/InclusiveCrosshairs.cpp @@ -27,6 +27,73 @@ struct InclusiveCrosshairs void SwitchActivationMode(); void ApplySettings(InclusiveCrosshairsSettings& settings, bool applyToRuntimeObjects); +public: + // Allow external callers to request a position update (thread-safe enqueue) + static void RequestUpdatePosition() + { + if (instance != nullptr) + { + auto dispatcherQueue = instance->m_dispatcherQueueController.DispatcherQueue(); + dispatcherQueue.TryEnqueue([]() { + if (instance != nullptr) + { + instance->UpdateCrosshairsPosition(); + } + }); + } + } + + static void EnsureOn() + { + if (instance != nullptr) + { + auto dispatcherQueue = instance->m_dispatcherQueueController.DispatcherQueue(); + dispatcherQueue.TryEnqueue([]() { + if (instance != nullptr && !instance->m_drawing) + { + instance->StartDrawing(); + } + }); + } + } + + static void EnsureOff() + { + if (instance != nullptr) + { + auto dispatcherQueue = instance->m_dispatcherQueueController.DispatcherQueue(); + dispatcherQueue.TryEnqueue([]() { + if (instance != nullptr && instance->m_drawing) + { + instance->StopDrawing(); + } + }); + } + } + + static void SetExternalControl(bool enabled) + { + if (instance != nullptr) + { + auto dispatcherQueue = instance->m_dispatcherQueueController.DispatcherQueue(); + dispatcherQueue.TryEnqueue([enabled]() { + if (instance != nullptr) + { + instance->m_externalControl = enabled; + if (enabled && instance->m_mouseHook) + { + UnhookWindowsHookEx(instance->m_mouseHook); + instance->m_mouseHook = NULL; + } + else if (!enabled && instance->m_drawing && !instance->m_mouseHook) + { + instance->m_mouseHook = SetWindowsHookEx(WH_MOUSE_LL, MouseHookProc, instance->m_hinstance, 0); + } + } + }); + } + } + private: enum class MouseButton { @@ -69,6 +136,7 @@ private: bool m_drawing = false; bool m_destroyed = false; bool m_hiddenCursor = false; + bool m_externalControl = false; void SetAutoHideTimer() noexcept; // Configurable Settings @@ -264,9 +332,12 @@ LRESULT CALLBACK InclusiveCrosshairs::MouseHookProc(int nCode, WPARAM wParam, LP if (nCode >= 0) { MSLLHOOKSTRUCT* hookData = reinterpret_cast(lParam); - if (wParam == WM_MOUSEMOVE) + if (instance && !instance->m_externalControl) { - instance->UpdateCrosshairsPosition(); + if (wParam == WM_MOUSEMOVE) + { + instance->UpdateCrosshairsPosition(); + } } } return CallNextHookEx(0, nCode, wParam, lParam); @@ -527,6 +598,26 @@ bool InclusiveCrosshairsIsEnabled() return (InclusiveCrosshairs::instance != nullptr); } +void InclusiveCrosshairsRequestUpdatePosition() +{ + InclusiveCrosshairs::RequestUpdatePosition(); +} + +void InclusiveCrosshairsEnsureOn() +{ + InclusiveCrosshairs::EnsureOn(); +} + +void InclusiveCrosshairsEnsureOff() +{ + InclusiveCrosshairs::EnsureOff(); +} + +void InclusiveCrosshairsSetExternalControl(bool enabled) +{ + InclusiveCrosshairs::SetExternalControl(enabled); +} + int InclusiveCrosshairsMain(HINSTANCE hInstance, InclusiveCrosshairsSettings& settings) { Logger::info("Starting a crosshairs instance."); diff --git a/src/modules/MouseUtils/MousePointerCrosshairs/InclusiveCrosshairs.h b/src/modules/MouseUtils/MousePointerCrosshairs/InclusiveCrosshairs.h index 43456a4326..a6618d85bf 100644 --- a/src/modules/MouseUtils/MousePointerCrosshairs/InclusiveCrosshairs.h +++ b/src/modules/MouseUtils/MousePointerCrosshairs/InclusiveCrosshairs.h @@ -31,3 +31,7 @@ void InclusiveCrosshairsDisable(); bool InclusiveCrosshairsIsEnabled(); void InclusiveCrosshairsSwitch(); void InclusiveCrosshairsApplySettings(InclusiveCrosshairsSettings& settings); +void InclusiveCrosshairsRequestUpdatePosition(); +void InclusiveCrosshairsEnsureOn(); +void InclusiveCrosshairsEnsureOff(); +void InclusiveCrosshairsSetExternalControl(bool enabled); diff --git a/src/modules/MouseUtils/MousePointerCrosshairs/dllmain.cpp b/src/modules/MouseUtils/MousePointerCrosshairs/dllmain.cpp index d2273c7efd..3dcee0d6a4 100644 --- a/src/modules/MouseUtils/MousePointerCrosshairs/dllmain.cpp +++ b/src/modules/MouseUtils/MousePointerCrosshairs/dllmain.cpp @@ -4,6 +4,15 @@ #include "trace.h" #include "InclusiveCrosshairs.h" #include "common/utils/color.h" +#include +#include +#include +#include + +extern void InclusiveCrosshairsRequestUpdatePosition(); +extern void InclusiveCrosshairsEnsureOn(); +extern void InclusiveCrosshairsEnsureOff(); +extern void InclusiveCrosshairsSetExternalControl(bool enabled); // Non-Localizable strings namespace @@ -11,6 +20,7 @@ namespace const wchar_t JSON_KEY_PROPERTIES[] = L"properties"; const wchar_t JSON_KEY_VALUE[] = L"value"; const wchar_t JSON_KEY_ACTIVATION_SHORTCUT[] = L"activation_shortcut"; + const wchar_t JSON_KEY_GLIDING_ACTIVATION_SHORTCUT[] = L"gliding_cursor_activation_shortcut"; const wchar_t JSON_KEY_CROSSHAIRS_COLOR[] = L"crosshairs_color"; const wchar_t JSON_KEY_CROSSHAIRS_OPACITY[] = L"crosshairs_opacity"; const wchar_t JSON_KEY_CROSSHAIRS_RADIUS[] = L"crosshairs_radius"; @@ -21,13 +31,15 @@ namespace const wchar_t JSON_KEY_CROSSHAIRS_IS_FIXED_LENGTH_ENABLED[] = L"crosshairs_is_fixed_length_enabled"; const wchar_t JSON_KEY_CROSSHAIRS_FIXED_LENGTH[] = L"crosshairs_fixed_length"; const wchar_t JSON_KEY_AUTO_ACTIVATE[] = L"auto_activate"; + const wchar_t JSON_KEY_GLIDE_TRAVEL_SPEED[] = L"gliding_travel_speed"; + const wchar_t JSON_KEY_GLIDE_DELAY_SPEED[] = L"gliding_delay_speed"; } extern "C" IMAGE_DOS_HEADER __ImageBase; HMODULE m_hModule; -BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) +BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID /*lpReserved*/) { m_hModule = hModule; switch (ul_reason_for_call) @@ -57,8 +69,46 @@ private: // The PowerToy state. bool m_enabled = false; - // Hotkey to invoke the module - HotkeyEx m_hotkey; + // Additional hotkeys (legacy API) to support multiple shortcuts + Hotkey m_activationHotkey{}; // Crosshairs toggle + Hotkey m_glidingHotkey{}; // Gliding cursor state machine + + // Shared state for worker threads (decoupled from this lifetime) + struct State + { + std::atomic stopX{ false }; + std::atomic stopY{ false }; + + // positions and speeds + int currentXPos{ 0 }; + 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 + + // Fractional accumulators to spread movement across 10ms ticks + double xFraction{ 0.0 }; + double yFraction{ 0.0 }; + + // 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 fastVSpeed{ 30 }; // pixels per base window + int slowVSpeed{ 5 }; // pixels per base window + }; + + std::shared_ptr m_state; + + // Worker threads + std::thread m_xThread; + std::thread m_yThread; + + // Gliding cursor state machine + std::atomic m_glideState{ 0 }; // 0..4 like the AHK script + + // Timer configuration: 10ms tick, speeds are defined per 200ms base window + static constexpr int kTimerTickMs = 10; + static constexpr int kBaseSpeedTickMs = 200; // mapping period for configured pixel counts // Mouse Pointer Crosshairs specific settings InclusiveCrosshairsSettings m_inclusiveCrosshairsSettings; @@ -68,12 +118,17 @@ public: MousePointerCrosshairs() { LoggerHelpers::init_logger(MODULE_NAME, L"ModuleInterface", LogSettings::mousePointerCrosshairsLoggerName); + m_state = std::make_shared(); init_settings(); }; // Destroy the powertoy and free memory virtual void destroy() override { + StopXTimer(); + StopYTimer(); + // Release shared state so worker threads (if any) exit when weak_ptr lock fails + m_state.reset(); delete this; } @@ -107,9 +162,7 @@ public: // Signal from the Settings editor to call a custom action. // This can be used to spawn more complex editors. - virtual void call_custom_action(const wchar_t* action) override - { - } + virtual void call_custom_action(const wchar_t* /*action*/) override {} // Called by the runner to pass the updated settings values as a serialized JSON. virtual void set_config(const wchar_t* config) override @@ -143,6 +196,9 @@ public: { m_enabled = false; Trace::EnableMousePointerCrosshairs(false); + StopXTimer(); + StopYTimer(); + m_glideState = 0; InclusiveCrosshairsDisable(); } @@ -158,15 +214,249 @@ public: return false; } - virtual std::optional GetHotkeyEx() override + // Legacy multi-hotkey support (like CropAndLock) + virtual size_t get_hotkeys(Hotkey* buffer, size_t buffer_size) override { - return m_hotkey; + if (buffer && buffer_size >= 2) + { + buffer[0] = m_activationHotkey; // Crosshairs toggle + buffer[1] = m_glidingHotkey; // Gliding cursor toggle + } + return 2; } - virtual void OnHotkeyEx() override + virtual bool on_hotkey(size_t hotkeyId) override { - InclusiveCrosshairsSwitch(); + if (!m_enabled) + { + return false; + } + + if (hotkeyId == 0) + { + InclusiveCrosshairsSwitch(); + return true; + } + if (hotkeyId == 1) + { + HandleGlidingHotkey(); + return true; + } + return false; } + +private: + static void LeftClick() + { + INPUT inputs[2]{}; + inputs[0].type = INPUT_MOUSE; + inputs[0].mi.dwFlags = MOUSEEVENTF_LEFTDOWN; + inputs[1].type = INPUT_MOUSE; + inputs[1].mi.dwFlags = MOUSEEVENTF_LEFTUP; + SendInput(2, inputs, sizeof(INPUT)); + } + + // Stateless helpers operating on shared State + static void PositionCursorX(const std::shared_ptr& s) + { + int screenW = GetSystemMetrics(SM_CXVIRTUALSCREEN); + int screenH = GetSystemMetrics(SM_CYVIRTUALSCREEN); + s->currentYPos = screenH / 2; + + // Distribute movement over 10ms ticks to match pixels-per-base-window speeds + const double perTick = (static_cast(s->currentXSpeed) * kTimerTickMs) / static_cast(kBaseSpeedTickMs); + s->xFraction += perTick; + int step = static_cast(s->xFraction); + if (step > 0) + { + s->xFraction -= step; + s->currentXPos += step; + } + + s->xPosSnapshot = s->currentXPos; + if (s->currentXPos >= screenW) + { + s->currentXPos = 0; + s->currentXSpeed = s->fastHSpeed; + s->xPosSnapshot = 0; + s->xFraction = 0.0; // reset fractional remainder on wrap + } + SetCursorPos(s->currentXPos, s->currentYPos); + // Ensure overlay crosshairs follow immediately + InclusiveCrosshairsRequestUpdatePosition(); + } + + static void PositionCursorY(const std::shared_ptr& s) + { + int screenH = GetSystemMetrics(SM_CYVIRTUALSCREEN); + // Keep X at snapshot + // Use s->xPosSnapshot captured during X pass + + // Distribute movement over 10ms ticks to match pixels-per-base-window speeds + const double perTick = (static_cast(s->currentYSpeed) * kTimerTickMs) / static_cast(kBaseSpeedTickMs); + s->yFraction += perTick; + int step = static_cast(s->yFraction); + if (step > 0) + { + s->yFraction -= step; + s->currentYPos += step; + } + + if (s->currentYPos >= screenH) + { + s->currentYPos = 0; + s->currentYSpeed = s->fastVSpeed; + s->yFraction = 0.0; // reset fractional remainder on wrap + } + SetCursorPos(s->xPosSnapshot, s->currentYPos); + // Ensure overlay crosshairs follow immediately + InclusiveCrosshairsRequestUpdatePosition(); + } + + void StartXTimer() + { + auto s = m_state; + if (!s) + { + return; + } + s->stopX = false; + std::weak_ptr wp = s; + m_xThread = std::thread([wp]() { + while (true) + { + auto sp = wp.lock(); + if (!sp || sp->stopX.load()) + { + break; + } + PositionCursorX(sp); + std::this_thread::sleep_for(std::chrono::milliseconds(kTimerTickMs)); + } + }); + } + + void StopXTimer() + { + auto s = m_state; + if (s) + { + s->stopX = true; + } + if (m_xThread.joinable()) + { + m_xThread.join(); + } + } + + void StartYTimer() + { + auto s = m_state; + if (!s) + { + return; + } + s->stopY = false; + std::weak_ptr wp = s; + m_yThread = std::thread([wp]() { + while (true) + { + auto sp = wp.lock(); + if (!sp || sp->stopY.load()) + { + break; + } + PositionCursorY(sp); + std::this_thread::sleep_for(std::chrono::milliseconds(kTimerTickMs)); + } + }); + } + + void StopYTimer() + { + auto s = m_state; + if (s) + { + s->stopY = true; + } + if (m_yThread.joinable()) + { + m_yThread.join(); + } + } + + void HandleGlidingHotkey() + { + auto s = m_state; + if (!s) + { + return; + } + // Simulate the AHK state machine + int state = m_glideState.load(); + switch (state) + { + case 0: + { + // Ensure crosshairs on (do not toggle off if already on) + InclusiveCrosshairsEnsureOn(); + // Disable internal mouse hook so we control position updates explicitly + InclusiveCrosshairsSetExternalControl(true); + + s->currentXPos = 0; + s->currentXSpeed = s->fastHSpeed; + s->xFraction = 0.0; + s->yFraction = 0.0; + int y = GetSystemMetrics(SM_CYVIRTUALSCREEN) / 2; + SetCursorPos(0, y); + InclusiveCrosshairsRequestUpdatePosition(); + m_glideState = 1; + StartXTimer(); + break; + } + case 1: + { + // Slow horizontal + s->currentXSpeed = s->slowHSpeed; + m_glideState = 2; + break; + } + case 2: + { + // Stop horizontal, start vertical (fast) + StopXTimer(); + s->currentYSpeed = s->fastVSpeed; + s->currentYPos = 0; + s->yFraction = 0.0; + SetCursorPos(s->xPosSnapshot, s->currentYPos); + InclusiveCrosshairsRequestUpdatePosition(); + m_glideState = 3; + StartYTimer(); + break; + } + case 3: + { + // Slow vertical + s->currentYSpeed = s->slowVSpeed; + m_glideState = 4; + break; + } + case 4: + default: + { + // Stop vertical, click, turn crosshairs off, re-enable internal tracking, reset state + StopYTimer(); + m_glideState = 0; + LeftClick(); + InclusiveCrosshairsEnsureOff(); + InclusiveCrosshairsSetExternalControl(false); + s->xFraction = 0.0; + s->yFraction = 0.0; + break; + } + } + } + // Load the settings file. void init_settings() { @@ -192,37 +482,44 @@ public: { try { - // Parse HotKey + // Parse primary activation HotKey (for centralized hook) auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_ACTIVATION_SHORTCUT); auto hotkey = PowerToysSettings::HotkeyObject::from_json(jsonPropertiesObject); - m_hotkey = HotkeyEx(); - if (hotkey.win_pressed()) - { - m_hotkey.modifiersMask |= MOD_WIN; - } - if (hotkey.ctrl_pressed()) - { - m_hotkey.modifiersMask |= MOD_CONTROL; - } - - if (hotkey.shift_pressed()) - { - m_hotkey.modifiersMask |= MOD_SHIFT; - } - - if (hotkey.alt_pressed()) - { - m_hotkey.modifiersMask |= MOD_ALT; - } - - m_hotkey.vkCode = hotkey.get_code(); + // Map to legacy Hotkey for multi-hotkey API + m_activationHotkey.win = hotkey.win_pressed(); + m_activationHotkey.ctrl = hotkey.ctrl_pressed(); + m_activationHotkey.shift = hotkey.shift_pressed(); + m_activationHotkey.alt = hotkey.alt_pressed(); + m_activationHotkey.key = static_cast(hotkey.get_code()); } catch (...) { Logger::warn("Failed to initialize Mouse Pointer Crosshairs activation shortcut"); } try + { + // Parse Gliding Cursor HotKey + auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_GLIDING_ACTIVATION_SHORTCUT); + auto hotkey = PowerToysSettings::HotkeyObject::from_json(jsonPropertiesObject); + m_glidingHotkey.win = hotkey.win_pressed(); + m_glidingHotkey.ctrl = hotkey.ctrl_pressed(); + m_glidingHotkey.shift = hotkey.shift_pressed(); + m_glidingHotkey.alt = hotkey.alt_pressed(); + m_glidingHotkey.key = static_cast(hotkey.get_code()); + } + catch (...) + { + // note that this is also defined in src\settings-ui\Settings.UI.Library\MousePointerCrosshairsProperties.cs, DefaultGlidingCursorActivationShortcut + // both need to be kept in sync! + Logger::warn("Failed to initialize Gliding Cursor activation shortcut. Using default Win+Alt+."); + m_glidingHotkey.win = true; + m_glidingHotkey.alt = true; + m_glidingHotkey.ctrl = false; + m_glidingHotkey.shift = false; + m_glidingHotkey.key = VK_OEM_PERIOD; + } + try { // Parse Opacity auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_CROSSHAIRS_OPACITY); @@ -272,7 +569,6 @@ public: { throw std::runtime_error("Invalid Radius value"); } - } catch (...) { @@ -291,7 +587,6 @@ public: { throw std::runtime_error("Invalid Thickness value"); } - } catch (...) { @@ -320,7 +615,7 @@ public: { // Parse border size auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_CROSSHAIRS_BORDER_SIZE); - int value = static_cast (jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE)); + int value = static_cast(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE)); if (value >= 0) { inclusiveCrosshairsSettings.crosshairsBorderSize = value; @@ -383,20 +678,86 @@ public: { Logger::warn("Failed to initialize auto activate from settings. Will use default value"); } + try + { + // Parse Travel speed (fast speed mapping) + auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_GLIDE_TRAVEL_SPEED); + int value = static_cast(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE)); + if (value >= 5 && value <= 60) + { + m_state->fastHSpeed = value; + m_state->fastVSpeed = value; + } + else if (value < 5) + { + m_state->fastHSpeed = 5; m_state->fastVSpeed = 5; + } + else + { + m_state->fastHSpeed = 60; m_state->fastVSpeed = 60; + } + } + catch (...) + { + Logger::warn("Failed to initialize gliding travel speed from settings. Using default 25."); + if (m_state) + { + m_state->fastHSpeed = 25; + m_state->fastVSpeed = 25; + } + } + try + { + // Parse Delay speed (slow speed mapping) + auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_GLIDE_DELAY_SPEED); + int value = static_cast(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE)); + if (value >= 5 && value <= 60) + { + m_state->slowHSpeed = value; + m_state->slowVSpeed = value; + } + else if (value < 5) + { + m_state->slowHSpeed = 5; m_state->slowVSpeed = 5; + } + else + { + m_state->slowHSpeed = 60; m_state->slowVSpeed = 60; + } + } + catch (...) + { + Logger::warn("Failed to initialize gliding delay speed from settings. Using default 5."); + if (m_state) + { + m_state->slowHSpeed = 5; + m_state->slowVSpeed = 5; + } + } } else { Logger::info("Mouse Pointer Crosshairs settings are empty"); } - if (!m_hotkey.modifiersMask) + + if (m_activationHotkey.key == 0) { - Logger::info("Mouse Pointer Crosshairs is going to use default shortcut"); - m_hotkey.modifiersMask = MOD_WIN | MOD_ALT; - m_hotkey.vkCode = 0x50; // P key + m_activationHotkey.win = true; + m_activationHotkey.alt = true; + m_activationHotkey.ctrl = false; + m_activationHotkey.shift = false; + m_activationHotkey.key = 'P'; + } + if (m_glidingHotkey.key == 0) + { + m_glidingHotkey.win = true; + m_glidingHotkey.alt = true; + m_glidingHotkey.ctrl = false; + m_glidingHotkey.shift = false; + m_glidingHotkey.key = VK_OEM_PERIOD; } m_inclusiveCrosshairsSettings = inclusiveCrosshairsSettings; } - }; extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create() diff --git a/src/settings-ui/Settings.UI.Library/MousePointerCrosshairsProperties.cs b/src/settings-ui/Settings.UI.Library/MousePointerCrosshairsProperties.cs index 9b0e530a2a..54542194c0 100644 --- a/src/settings-ui/Settings.UI.Library/MousePointerCrosshairsProperties.cs +++ b/src/settings-ui/Settings.UI.Library/MousePointerCrosshairsProperties.cs @@ -13,9 +13,15 @@ namespace Microsoft.PowerToys.Settings.UI.Library [CmdConfigureIgnore] public HotkeySettings DefaultActivationShortcut => new HotkeySettings(true, false, true, false, 0x50); // Win + Alt + P + [CmdConfigureIgnore] + public HotkeySettings DefaultGlidingCursorActivationShortcut => new HotkeySettings(true, false, true, false, 0xBE); // Win + Alt + . + [JsonPropertyName("activation_shortcut")] public HotkeySettings ActivationShortcut { get; set; } + [JsonPropertyName("gliding_cursor_activation_shortcut")] + public HotkeySettings GlidingCursorActivationShortcut { get; set; } + [JsonPropertyName("crosshairs_color")] public StringProperty CrosshairsColor { get; set; } @@ -46,9 +52,16 @@ namespace Microsoft.PowerToys.Settings.UI.Library [JsonPropertyName("auto_activate")] public BoolProperty AutoActivate { get; set; } + [JsonPropertyName("gliding_travel_speed")] + public IntProperty GlidingTravelSpeed { get; set; } + + [JsonPropertyName("gliding_delay_speed")] + public IntProperty GlidingDelaySpeed { get; set; } + public MousePointerCrosshairsProperties() { ActivationShortcut = DefaultActivationShortcut; + GlidingCursorActivationShortcut = DefaultGlidingCursorActivationShortcut; CrosshairsColor = new StringProperty("#FF0000"); CrosshairsOpacity = new IntProperty(75); CrosshairsRadius = new IntProperty(20); @@ -59,6 +72,8 @@ namespace Microsoft.PowerToys.Settings.UI.Library CrosshairsIsFixedLengthEnabled = new BoolProperty(false); CrosshairsFixedLength = new IntProperty(1); AutoActivate = new BoolProperty(false); + GlidingTravelSpeed = new IntProperty(25); + GlidingDelaySpeed = new IntProperty(5); } } } diff --git a/src/settings-ui/Settings.UI.Library/MousePointerCrosshairsSettings.cs b/src/settings-ui/Settings.UI.Library/MousePointerCrosshairsSettings.cs index 81b3eadca4..d814f115a1 100644 --- a/src/settings-ui/Settings.UI.Library/MousePointerCrosshairsSettings.cs +++ b/src/settings-ui/Settings.UI.Library/MousePointerCrosshairsSettings.cs @@ -39,6 +39,10 @@ namespace Microsoft.PowerToys.Settings.UI.Library () => Properties.ActivationShortcut, value => Properties.ActivationShortcut = value ?? Properties.DefaultActivationShortcut, "MouseUtils_MousePointerCrosshairs_ActivationShortcut"), + new HotkeyAccessor( + () => Properties.GlidingCursorActivationShortcut, + value => Properties.GlidingCursorActivationShortcut = value ?? Properties.DefaultGlidingCursorActivationShortcut, + "MouseUtils_GlidingCursor"), }; return hotkeyAccessors.ToArray(); diff --git a/src/settings-ui/Settings.UI/Assets/Settings/Icons/MouseCrosshairs.png b/src/settings-ui/Settings.UI/Assets/Settings/Icons/MouseCrosshairs.png index ae940629b028503379c87a8b3f182f261af7f30d..dea5a249b53cb8c64f402490b6f8d47cb566414f 100644 GIT binary patch delta 1561 zcmV+!2Il#)4XF%}BYyx1a7bBm000XU000XU0RWnu7ytkO0drDELIAGL9O(c600d`2 zO+f$vv5yPl?%G}6RGG%>8kG!`*!jOZJ*!I(aPtr~pcfvwS$(3ss8h<`uvr!g_a7}ITyv7|t+ zq=eSpJI6D3?#!LLceg*7@Wg57-s$|EZ_YX2nYjhHBQpzwTM#>SCIRup9uB+#5E6)J z`>Q#Cm~)X0a5q0R*0Cc8^OSi3Iy{JbN5(E6JpaS5Nimgxc7ygW@)OwZ8m+CWr3tJ35ekt zQqV{n*RC+dcNmkdV`f{}bv{^<_IpzO4bw2$>~!pNV1GVe2F>v{kMAO+< z?$6E$=zr}CI82Q;+y)WsJF=CnG222eJ2Z&cud$vxs$+$+-*LH`GJj!b*5{^9tyFZ4)f7tpF8AsCUQ0E;QGW zpnsw+BhU%dqs){Kq4O#LMGJ9Gm4a?tb$2xkv_1weH?EY2D_7^8vN_Xslg*L{6ya+k zfaDBTWMcHKhY!+s=eMkeZw_yTRrN7_>!ja-sjlE-AjC2{WIC}3y^KN4z*1UoqaRwGuPdArS zTlO=^p)^3ReYLOlwbNp4+)#~%ubdlsIezjcY-{fYhY5GYy#%WU1{0yPaAIf5f$~xb zv&i+2cC;@#(3h=>$GRc7Z-2{RW)=9^;2+GnHV#9%t1xnM#-YT)rd!NB#p%6B6B?P>*A+00000 LNkvXXu0mjfbS~v0 delta 1699 zcmV;U23+~646+T7BYy^YNklM0xE}KjGTY~R)(h&U{c`wUh-`50i z^|)|3mzbegV}BCXBoF`yAp^>MfTfKKVnW_bN&*>Vk>Fqq^C}V+wi9OOQMUuq20#W# zsENdh!QG260+|bzyL|*YMunjmvBnR|$WSXlJ%;B2XHbmfa|`4-LL z#@aO&Au}*hR2h@)N$jY-cxtevr4v$bwQc5;+4Ssgn}3|fCTn!`0<{^v%m9IjsVxPj z8YC{MoKb2Lq&~=MLE+FYP|?Ds#%J;-jgg`&%eOh{%wQ1(2OKa!{Y4-b>y@+743rxr zzd=xXHpw3Zl$b$NVbb?$>D_9=4+(-C5F<1qNC3GY04BT^*`De}nBLl}6Kg9q^V2!< z1n%Ivi+?J+OE%Z}gi`)Q>NW%i7s3E-9wZY1V`8bq6i^niMo(v{^VVJl2TW${+&<3F z|FMhNZ4;QQ532*3U425)D>AHl9Klg?0#k^b^$M(42!zThdmZw8h4yN?PJ19DxfFN!>uM>apYPrGJM`WGhS*%VedH+dl_Vuc3*1X{Nh0 zMg2UNW623}&f2?7)$8$*hhCF=+-Q!aqjsSI$~>V6sR^&CsHn$ttks6(BY=!Sc3ogN zv+Gi5vUPmM*s+W4ywkd$hZ?zqJGm04?d<^KThG2JcfavGOGloCBuoN#OzBEZZCB9D z6n|D^APp~nxJd@(0Os~=L(h}ar6gf4Btc3jv!+R_+d^0?Fij8z66W5A$-!&9NqqbH zx8miOI*_-0=NQ_PEdqUb*$vmted=gM9Vn%1_Rlw%J90H9dmiwC06^u7H0e^L2r;4* zQ4>)-cTFL6%8=@S3q18F$5Wzd5`-2C8GpY$@JMbvSF>|Ir`N5)9a6%|s<0TLN>{@p zq%^qf2J^I5W$sNRd8DC?#-l>4i4` zIN~T`YB+J%D))ZHk2zztVZ4<=ax6Jjksnnz30#-P``6L`t!L_--;&?d@mB)m0Dl+& zrvo40LYWdiHc#cnzL0v~$Nesn2ohVT+kEhchw;2Oev?nW?p{3Z?l?V` zbh$Pm&mJ8lB!P?R#cjO*yQcug^vs`yQB{;b>fb{+#&3+R&jPOM(|7k_-X1|K33 zp8PSwzC}uJLb?*3P;#dJZxJc&a@VWCKa2-xN{9uJM*2#evzg4m^%XpQMKN9vY{D!^ zb+0jxfA&x$%;=}#{w1IGDS-(9-0&c<2v$PgB23NyHOx$d8NP1`E$xP;fjS|t!McD0 zU4#R+d63tUY+{0`tu06il#n%UPz6LKFC*KnED$gkQruxjS3QwWq*XSgt`Xz^)LXE z9Fl`1_M6Z^gzXO$GK4@vbqx|=?7u!paz@T<@cZIGW)L!{)=*~^%=#OxFyG4gMcF<8Tk&#nYIj@CPFRVzdNixB|i%f=tjfSg(Py1ujNS#%bjK tcDZJQ_>V>}dc1002ovPDHLkV1joaERg^J diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/MouseUtilsPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/MouseUtilsPage.xaml index 0ba74ca164..01e9f8e740 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/MouseUtilsPage.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/MouseUtilsPage.xaml @@ -363,6 +363,27 @@ + + + + + + + + + + + + diff --git a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw index 17bb9267b6..7eede397b3 100644 --- a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw +++ b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw @@ -2845,7 +2845,26 @@ Right-click to remove the key combination, thereby deactivating the shortcut.Crosshairs fixed length (px) px = pixels - + + Gliding cursor + + + An accessibility feature that lets you control the mouse with a single button using guided horizontal and vertical lines + + + Initial line speed + + + Speed of the horizontal or vertical line when it begins moving + + + Reduced line speed + + + Speed after slowing down the line with a second shortcut press + + + Custom colors diff --git a/src/settings-ui/Settings.UI/ViewModels/MouseUtilsViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/MouseUtilsViewModel.cs index a3adc16e62..3d845a662b 100644 --- a/src/settings-ui/Settings.UI/ViewModels/MouseUtilsViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/MouseUtilsViewModel.cs @@ -159,7 +159,9 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels { [FindMyMouseSettings.ModuleName] = [FindMyMouseActivationShortcut], [MouseHighlighterSettings.ModuleName] = [MouseHighlighterActivationShortcut], - [MousePointerCrosshairsSettings.ModuleName] = [MousePointerCrosshairsActivationShortcut], + [MousePointerCrosshairsSettings.ModuleName] = [ + MousePointerCrosshairsActivationShortcut, + GlidingCursorActivationShortcut], [MouseJumpSettings.ModuleName] = [MouseJumpActivationShortcut], }; @@ -904,6 +906,49 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels } } + public int GlidingCursorTravelSpeed + { + get => MousePointerCrosshairsSettingsConfig.Properties.GlidingTravelSpeed.Value; + set + { + if (MousePointerCrosshairsSettingsConfig.Properties.GlidingTravelSpeed.Value != value) + { + MousePointerCrosshairsSettingsConfig.Properties.GlidingTravelSpeed.Value = value; + NotifyMousePointerCrosshairsPropertyChanged(); + } + } + } + + public int GlidingCursorDelaySpeed + { + get => MousePointerCrosshairsSettingsConfig.Properties.GlidingDelaySpeed.Value; + set + { + if (MousePointerCrosshairsSettingsConfig.Properties.GlidingDelaySpeed.Value != value) + { + MousePointerCrosshairsSettingsConfig.Properties.GlidingDelaySpeed.Value = value; + NotifyMousePointerCrosshairsPropertyChanged(); + } + } + } + + public HotkeySettings GlidingCursorActivationShortcut + { + get + { + return MousePointerCrosshairsSettingsConfig.Properties.GlidingCursorActivationShortcut; + } + + set + { + if (MousePointerCrosshairsSettingsConfig.Properties.GlidingCursorActivationShortcut != value) + { + MousePointerCrosshairsSettingsConfig.Properties.GlidingCursorActivationShortcut = value ?? MousePointerCrosshairsSettingsConfig.Properties.DefaultGlidingCursorActivationShortcut; + NotifyMousePointerCrosshairsPropertyChanged(); + } + } + } + public void NotifyMousePointerCrosshairsPropertyChanged([CallerMemberName] string propertyName = null) { OnPropertyChanged(propertyName);