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 6b1dcb9c16..a2c64a72a4 100644 Binary files a/doc/images/icons/Mouse Crosshairs.png and b/doc/images/icons/Mouse Crosshairs.png differ 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 ae940629b0..dea5a249b5 100644 Binary files a/src/settings-ui/Settings.UI/Assets/Settings/Icons/MouseCrosshairs.png and b/src/settings-ui/Settings.UI/Assets/Settings/Icons/MouseCrosshairs.png differ 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);