mirror of
https://github.com/microsoft/PowerToys
synced 2025-08-22 01:58:04 +00:00
Implement "Gliding cursor" accessibility feature (#41221)
## 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.  <img width="857" height="438" alt="image" src="https://github.com/user-attachments/assets/b9e7ee72-dfeb-4d20-93a5-a34e8b10d703" /> 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 <shuaiyuan@microsoft.com> Co-authored-by: Niels Laute <niels.laute@live.nl> Co-authored-by: Shawn Yuan <shuaiyuan@microsoft.com>
This commit is contained in:
parent
44d34e45c0
commit
df08d98a81
9
.github/actions/spell-check/expect.txt
vendored
9
.github/actions/spell-check/expect.txt
vendored
@ -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
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 18 KiB |
@ -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,11 +332,14 @@ LRESULT CALLBACK InclusiveCrosshairs::MouseHookProc(int nCode, WPARAM wParam, LP
|
||||
if (nCode >= 0)
|
||||
{
|
||||
MSLLHOOKSTRUCT* hookData = reinterpret_cast<MSLLHOOKSTRUCT*>(lParam);
|
||||
if (instance && !instance->m_externalControl)
|
||||
{
|
||||
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.");
|
||||
|
@ -31,3 +31,7 @@ void InclusiveCrosshairsDisable();
|
||||
bool InclusiveCrosshairsIsEnabled();
|
||||
void InclusiveCrosshairsSwitch();
|
||||
void InclusiveCrosshairsApplySettings(InclusiveCrosshairsSettings& settings);
|
||||
void InclusiveCrosshairsRequestUpdatePosition();
|
||||
void InclusiveCrosshairsEnsureOn();
|
||||
void InclusiveCrosshairsEnsureOff();
|
||||
void InclusiveCrosshairsSetExternalControl(bool enabled);
|
||||
|
@ -4,6 +4,15 @@
|
||||
#include "trace.h"
|
||||
#include "InclusiveCrosshairs.h"
|
||||
#include "common/utils/color.h"
|
||||
#include <atomic>
|
||||
#include <thread>
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
|
||||
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<bool> stopX{ false };
|
||||
std::atomic<bool> 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<State> m_state;
|
||||
|
||||
// Worker threads
|
||||
std::thread m_xThread;
|
||||
std::thread m_yThread;
|
||||
|
||||
// Gliding cursor state machine
|
||||
std::atomic<int> 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<State>();
|
||||
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<HotkeyEx> 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
|
||||
{
|
||||
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<State>& 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<double>(s->currentXSpeed) * kTimerTickMs) / static_cast<double>(kBaseSpeedTickMs);
|
||||
s->xFraction += perTick;
|
||||
int step = static_cast<int>(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<State>& 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<double>(s->currentYSpeed) * kTimerTickMs) / static_cast<double>(kBaseSpeedTickMs);
|
||||
s->yFraction += perTick;
|
||||
int step = static_cast<int>(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<State> 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<State> 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<unsigned char>(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<unsigned char>(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 <int>(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
|
||||
int value = static_cast<int>(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<int>(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<int>(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()
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.5 KiB |
@ -363,6 +363,27 @@
|
||||
</tkcontrols:SettingsCard>
|
||||
</tkcontrols:SettingsExpander.Items>
|
||||
</tkcontrols:SettingsExpander>
|
||||
|
||||
<tkcontrols:SettingsExpander
|
||||
x:Uid="MouseUtils_GlidingCursor"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}"
|
||||
IsEnabled="{x:Bind ViewModel.IsMousePointerCrosshairsEnabled, Mode=OneWay}">
|
||||
<controls:ShortcutControl MinWidth="{StaticResource SettingActionControlMinWidth}" HotkeySettings="{x:Bind Path=ViewModel.GlidingCursorActivationShortcut, Mode=TwoWay}" />
|
||||
<tkcontrols:SettingsExpander.Items>
|
||||
<tkcontrols:SettingsCard x:Uid="MouseUtils_GlidingCursor_InitialSpeed">
|
||||
<Slider
|
||||
Maximum="60"
|
||||
Minimum="5"
|
||||
Value="{x:Bind ViewModel.GlidingCursorTravelSpeed, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard x:Uid="MouseUtils_GlidingCursor_DelaySpeed">
|
||||
<Slider
|
||||
Maximum="60"
|
||||
Minimum="5"
|
||||
Value="{x:Bind ViewModel.GlidingCursorDelaySpeed, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
</tkcontrols:SettingsExpander.Items>
|
||||
</tkcontrols:SettingsExpander>
|
||||
</controls:SettingsGroup>
|
||||
</StackPanel>
|
||||
</controls:SettingsPageControl.ModuleContent>
|
||||
|
@ -2845,6 +2845,25 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
||||
<value>Crosshairs fixed length (px)</value>
|
||||
<comment>px = pixels</comment>
|
||||
</data>
|
||||
<data name="MouseUtils_GlidingCursor.Header" xml:space="preserve">
|
||||
<value>Gliding cursor</value>
|
||||
</data>
|
||||
<data name="MouseUtils_GlidingCursor.Description" xml:space="preserve">
|
||||
<value>An accessibility feature that lets you control the mouse with a single button using guided horizontal and vertical lines</value>
|
||||
</data>
|
||||
<data name="MouseUtils_GlidingCursor_InitialSpeed.Header" xml:space="preserve">
|
||||
<value>Initial line speed</value>
|
||||
</data>
|
||||
<data name="MouseUtils_GlidingCursor_InitialSpeed.Description" xml:space="preserve">
|
||||
<value>Speed of the horizontal or vertical line when it begins moving</value>
|
||||
</data>
|
||||
<data name="MouseUtils_GlidingCursor_DelaySpeed.Header" xml:space="preserve">
|
||||
<value>Reduced line speed</value>
|
||||
</data>
|
||||
<data name="MouseUtils_GlidingCursor_DelaySpeed.Description" xml:space="preserve">
|
||||
<value>Speed after slowing down the line with a second shortcut press</value>
|
||||
</data>
|
||||
|
||||
<data name="FancyZones_Radio_Custom_Colors.Content" xml:space="preserve">
|
||||
<value>Custom colors</value>
|
||||
</data>
|
||||
|
@ -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);
|
||||
|
Loading…
x
Reference in New Issue
Block a user