Compare commits

...

3 Commits

Author SHA1 Message Date
vanzue
ecb37d3e9e poc for grow my cursor 2026-01-07 00:42:50 +08:00
vanzue
3e58c10258 Merge branch 'main' into dev/vanzue/grow-mouse 2026-01-06 11:37:28 +08:00
vanzue
6b33c78555 grow mouse 2026-01-06 11:36:55 +08:00
12 changed files with 785 additions and 4 deletions

View File

@@ -0,0 +1,53 @@
# Find My Mouse Cursor Magnifier Overlay
## Summary
Add a cursor magnifier overlay that draws a scaled copy of the current system cursor while Find My Mouse is active. This provides a "larger cursor" effect without changing the system cursor scheme or managing cursor assets.
## Goals
- Show a visibly larger cursor during the Find My Mouse spotlight effect.
- Avoid global cursor changes and asset maintenance.
- Keep the real system cursor visible and unmodified.
## Non-goals
- Changing the system cursor size globally.
- Replacing cursor assets or updating cursor registry schemes.
- Providing a configurable scale (initial implementation uses a fixed scale).
## Proposed Solution
Create a lightweight, topmost, layered overlay window that:
- Polls the current cursor (`GetCursorInfo`, `GetIconInfo`).
- Renders a scaled cursor into a 32-bit DIB via `DrawIconEx`.
- Composites the result using `UpdateLayeredWindow`.
- Runs at ~60 Hz while the Find My Mouse effect is active.
This overlay is independent of the spotlight window and does not interfere with the system cursor itself.
## Integration Points
- Initialize the overlay in `FindMyMouseMain` after the sonar window is created.
- Show/hide the overlay from `SuperSonar::StartSonar` and `SuperSonar::StopSonar`.
- Terminate the overlay when the module is disabled.
## Implementation Notes
- The overlay window uses `WS_EX_LAYERED | WS_EX_TRANSPARENT | WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE | WS_EX_TOPMOST`.
- The scaled cursor position uses the current cursor hotspot multiplied by the scale factor.
- If the cursor is hidden, the overlay hides itself as well.
- The overlay allocates a DIB buffer sized to the scaled cursor and reuses it until the size changes.
## Risks and Considerations
- Performance: 60 Hz rendering can be expensive on low-end machines; consider throttling or caching if needed.
- DPI/scale: the overlay operates in screen pixels; verify on mixed-DPI setups.
- Z-order: topmost layered window should stay above most content, but might need adjustment if other topmost overlays are present.
## Future Enhancements
- Expose cursor scale as a setting.
- Cache rendered cursor bitmaps per `HCURSOR` to reduce per-frame work.
- Consider a composition-based drawing path for smoother integration with existing visuals.

View File

@@ -0,0 +1,525 @@
#include "pch.h"
#include "CursorMagnifierOverlay.h"
#include <algorithm>
#include <cmath>
#include <cstring>
#include <vector>
namespace
{
// System cursor IDs (same values as OCR_* when OEMRESOURCE is defined).
static constexpr UINT kCursorIdNormal = 32512;
static const UINT kCursorIds[] = {
kCursorIdNormal, // OCR_NORMAL
32513, // OCR_IBEAM
32514, // OCR_WAIT
32515, // OCR_CROSS
32516, // OCR_UP
32642, // OCR_SIZENWSE
32643, // OCR_SIZENESW
32644, // OCR_SIZEWE
32645, // OCR_SIZENS
32646, // OCR_SIZEALL
32648, // OCR_NO
32649, // OCR_HAND
32650, // OCR_APPSTARTING
32651, // OCR_HELP
};
}
CursorMagnifierOverlay::~CursorMagnifierOverlay()
{
DestroyWindowInternal();
}
bool CursorMagnifierOverlay::Initialize(HINSTANCE instance)
{
if (m_hwnd)
{
return true;
}
m_instance = instance;
WNDCLASS wc{};
if (!GetClassInfoW(instance, kWindowClassName, &wc))
{
wc.lpfnWndProc = WndProc;
wc.hInstance = instance;
wc.hCursor = LoadCursor(nullptr, IDC_ARROW);
wc.hbrBackground = static_cast<HBRUSH>(GetStockObject(NULL_BRUSH));
wc.lpszClassName = kWindowClassName;
if (!RegisterClassW(&wc))
{
Logger::error("RegisterClassW failed for cursor magnifier. GetLastError={}", GetLastError());
return false;
}
}
DWORD exStyle = WS_EX_LAYERED | WS_EX_TRANSPARENT | WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE | WS_EX_TOPMOST;
m_hwnd = CreateWindowExW(
exStyle,
kWindowClassName,
L"PowerToys FindMyMouse Cursor Magnifier",
WS_POPUP,
0,
0,
0,
0,
nullptr,
nullptr,
instance,
this);
if (!m_hwnd)
{
Logger::error("CreateWindowExW failed for cursor magnifier. GetLastError={}", GetLastError());
return false;
}
return true;
}
void CursorMagnifierOverlay::Terminate()
{
if (!m_hwnd)
{
return;
}
PostMessage(m_hwnd, WM_CLOSE, 0, 0);
}
void CursorMagnifierOverlay::SetVisible(bool visible)
{
if (!m_hwnd || m_visible == visible)
{
return;
}
m_visible = visible;
if (visible)
{
BeginScaleAnimation();
HideSystemCursors();
SetTimer(m_hwnd, kTimerId, kFrameIntervalMs, nullptr);
ShowWindow(m_hwnd, SW_SHOWNOACTIVATE);
SetWindowPos(m_hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_SHOWWINDOW);
Render();
}
else
{
KillTimer(m_hwnd, kTimerId);
ShowWindow(m_hwnd, SW_HIDE);
ResetCursorMetrics();
m_animationStartTick = 0;
RestoreSystemCursors();
}
}
void CursorMagnifierOverlay::SetScale(float scale)
{
if (scale > 0.0f && m_targetScale != scale)
{
m_targetScale = scale;
if (m_visible)
{
m_startScale = m_currentScale;
m_animationStartTick = GetTickCount64();
}
}
}
void CursorMagnifierOverlay::SetAnimationDurationMs(int durationMs)
{
if (durationMs > 0)
{
m_animationDurationMs = static_cast<DWORD>(durationMs);
}
else
{
m_animationDurationMs = 1;
}
}
LRESULT CALLBACK CursorMagnifierOverlay::WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) noexcept
{
CursorMagnifierOverlay* self = nullptr;
if (message == WM_NCCREATE)
{
auto create = reinterpret_cast<LPCREATESTRUCT>(lParam);
self = static_cast<CursorMagnifierOverlay*>(create->lpCreateParams);
SetWindowLongPtr(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(self));
self->m_hwnd = hwnd;
}
else
{
self = reinterpret_cast<CursorMagnifierOverlay*>(GetWindowLongPtr(hwnd, GWLP_USERDATA));
}
if (!self)
{
return DefWindowProc(hwnd, message, wParam, lParam);
}
switch (message)
{
case WM_TIMER:
if (wParam == kTimerId)
{
self->OnTimer();
}
return 0;
case WM_NCHITTEST:
return HTTRANSPARENT;
case WM_DESTROY:
self->CleanupResources();
return 0;
}
return DefWindowProc(hwnd, message, wParam, lParam);
}
void CursorMagnifierOverlay::OnTimer()
{
if (!m_visible)
{
return;
}
Render();
}
void CursorMagnifierOverlay::Render()
{
if (!m_hwnd)
{
return;
}
CURSORINFO ci{};
ci.cbSize = sizeof(ci);
if (!GetCursorInfo(&ci) || (ci.flags & CURSOR_SHOWING) == 0)
{
ShowWindow(m_hwnd, SW_HIDE);
return;
}
HCURSOR cursorToDraw = ci.hCursor;
if (m_systemCursorsHidden)
{
auto hiddenIt = m_hiddenCursorIds.find(ci.hCursor);
if (hiddenIt != m_hiddenCursorIds.end())
{
auto originalIt = m_originalCursors.find(hiddenIt->second);
if (originalIt != m_originalCursors.end() && originalIt->second)
{
cursorToDraw = originalIt->second;
}
}
}
UpdateCursorMetrics(cursorToDraw);
const float scale = GetAnimatedScale();
int srcW = m_cursorSize.cx;
int srcH = m_cursorSize.cy;
if (srcW <= 0 || srcH <= 0)
{
srcW = GetSystemMetrics(SM_CXCURSOR);
srcH = GetSystemMetrics(SM_CYCURSOR);
}
const int dstW = (std::max)(1, static_cast<int>(std::lround(srcW * scale)));
const int dstH = (std::max)(1, static_cast<int>(std::lround(srcH * scale)));
EnsureResources(dstW, dstH);
if (!m_memDc || !m_bits)
{
return;
}
std::memset(m_bits, 0, static_cast<size_t>(dstW) * static_cast<size_t>(dstH) * 4);
if (cursorToDraw)
{
DrawIconEx(m_memDc, 0, 0, cursorToDraw, dstW, dstH, 0, nullptr, DI_NORMAL);
}
const int x = static_cast<int>(std::lround(ci.ptScreenPos.x - m_hotspot.x * scale));
const int y = static_cast<int>(std::lround(ci.ptScreenPos.y - m_hotspot.y * scale));
POINT ptDst{ x, y };
POINT ptSrc{ 0, 0 };
SIZE size{ dstW, dstH };
BLENDFUNCTION blend{};
blend.BlendOp = AC_SRC_OVER;
blend.SourceConstantAlpha = 255;
blend.AlphaFormat = AC_SRC_ALPHA;
UpdateLayeredWindow(m_hwnd, nullptr, &ptDst, &size, m_memDc, &ptSrc, 0, &blend, ULW_ALPHA);
ShowWindow(m_hwnd, SW_SHOWNOACTIVATE);
}
void CursorMagnifierOverlay::BeginScaleAnimation()
{
m_startScale = (m_targetScale >= 1.0f) ? 1.0f : m_targetScale;
m_currentScale = m_startScale;
m_animationStartTick = GetTickCount64();
}
float CursorMagnifierOverlay::GetAnimatedScale()
{
if (m_animationDurationMs <= 1 || !m_visible)
{
m_currentScale = m_targetScale;
return m_currentScale;
}
if (m_animationStartTick == 0)
{
m_animationStartTick = GetTickCount64();
}
const ULONGLONG now = GetTickCount64();
const ULONGLONG elapsed = now - m_animationStartTick;
if (elapsed >= m_animationDurationMs)
{
m_currentScale = m_targetScale;
return m_currentScale;
}
const float t = static_cast<float>(elapsed) / static_cast<float>(m_animationDurationMs);
m_currentScale = m_startScale + (m_targetScale - m_startScale) * t;
return m_currentScale;
}
bool CursorMagnifierOverlay::HideSystemCursors()
{
if (m_systemCursorsHidden)
{
return true;
}
m_hiddenCursorIds.clear();
ReleaseOriginalCursors();
bool anyHidden = false;
bool normalHidden = false;
for (UINT cursorId : kCursorIds)
{
HCURSOR systemCursor = LoadCursor(nullptr, MAKEINTRESOURCE(cursorId));
if (systemCursor)
{
HCURSOR copy = CopyIcon(systemCursor);
if (copy)
{
m_originalCursors[cursorId] = copy;
}
}
HCURSOR transparent = CreateTransparentCursor();
if (!transparent)
{
Logger::warn("CreateTransparentCursor failed for cursor id {}. GetLastError={}", cursorId, GetLastError());
continue;
}
if (!SetSystemCursor(transparent, cursorId))
{
Logger::warn("SetSystemCursor failed for cursor id {}. GetLastError={}", cursorId, GetLastError());
DestroyCursor(transparent);
continue;
}
DestroyCursor(transparent);
HCURSOR replacedCursor = LoadCursor(nullptr, MAKEINTRESOURCE(cursorId));
if (replacedCursor)
{
m_hiddenCursorIds[replacedCursor] = cursorId;
}
anyHidden = true;
if (cursorId == kCursorIdNormal)
{
normalHidden = true;
}
}
if (!anyHidden)
{
m_hiddenCursorIds.clear();
ReleaseOriginalCursors();
return false;
}
if (!normalHidden)
{
Logger::warn("Failed to hide OCR_NORMAL; cursor may remain visible during magnifier.");
}
m_systemCursorsHidden = true;
return true;
}
void CursorMagnifierOverlay::RestoreSystemCursors()
{
if (!m_systemCursorsHidden)
{
return;
}
SystemParametersInfoW(SPI_SETCURSORS, 0, nullptr, 0);
m_systemCursorsHidden = false;
m_hiddenCursorIds.clear();
ReleaseOriginalCursors();
}
HCURSOR CursorMagnifierOverlay::CreateTransparentCursor() const
{
const int width = GetSystemMetrics(SM_CXCURSOR);
const int height = GetSystemMetrics(SM_CYCURSOR);
if (width <= 0 || height <= 0)
{
return nullptr;
}
const int bytesPerRow = (width + 7) / 8;
const size_t maskSize = static_cast<size_t>(bytesPerRow) * static_cast<size_t>(height);
std::vector<BYTE> andMask(maskSize, 0xFF);
std::vector<BYTE> xorMask(maskSize, 0x00);
return CreateCursor(m_instance, 0, 0, width, height, andMask.data(), xorMask.data());
}
void CursorMagnifierOverlay::ReleaseOriginalCursors()
{
for (auto& entry : m_originalCursors)
{
if (entry.second)
{
DestroyIcon(entry.second);
}
}
m_originalCursors.clear();
}
void CursorMagnifierOverlay::UpdateCursorMetrics(HCURSOR cursor)
{
if (!cursor || cursor == m_cachedCursor)
{
return;
}
m_cursorSize = { 0, 0 };
m_hotspot = { 0, 0 };
ICONINFO ii{};
if (GetIconInfo(cursor, &ii))
{
if (ii.hbmColor)
{
BITMAP bm{};
GetObject(ii.hbmColor, sizeof(bm), &bm);
m_cursorSize = { bm.bmWidth, bm.bmHeight };
}
else if (ii.hbmMask)
{
BITMAP bm{};
GetObject(ii.hbmMask, sizeof(bm), &bm);
m_cursorSize = { bm.bmWidth, bm.bmHeight / 2 };
}
m_hotspot = { static_cast<LONG>(ii.xHotspot), static_cast<LONG>(ii.yHotspot) };
if (ii.hbmColor)
{
DeleteObject(ii.hbmColor);
}
if (ii.hbmMask)
{
DeleteObject(ii.hbmMask);
}
}
m_cachedCursor = cursor;
if (m_cursorSize.cx <= 0 || m_cursorSize.cy <= 0)
{
m_cursorSize = { GetSystemMetrics(SM_CXCURSOR), GetSystemMetrics(SM_CYCURSOR) };
}
}
void CursorMagnifierOverlay::ResetCursorMetrics()
{
m_cachedCursor = nullptr;
m_cursorSize = { 0, 0 };
m_hotspot = { 0, 0 };
}
void CursorMagnifierOverlay::EnsureResources(int width, int height)
{
if (width <= 0 || height <= 0)
{
return;
}
if (m_dib && m_dibSize.cx == width && m_dibSize.cy == height)
{
return;
}
CleanupResources();
m_memDc = CreateCompatibleDC(nullptr);
if (!m_memDc)
{
return;
}
BITMAPINFO bmi{};
bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
bmi.bmiHeader.biWidth = width;
bmi.bmiHeader.biHeight = -height; // top-down DIB
bmi.bmiHeader.biPlanes = 1;
bmi.bmiHeader.biBitCount = 32;
bmi.bmiHeader.biCompression = BI_RGB;
m_dib = CreateDIBSection(m_memDc, &bmi, DIB_RGB_COLORS, &m_bits, nullptr, 0);
if (!m_dib || !m_bits)
{
CleanupResources();
return;
}
SelectObject(m_memDc, m_dib);
m_dibSize = { width, height };
}
void CursorMagnifierOverlay::CleanupResources()
{
if (m_dib)
{
DeleteObject(m_dib);
m_dib = nullptr;
}
if (m_memDc)
{
DeleteDC(m_memDc);
m_memDc = nullptr;
}
m_bits = nullptr;
m_dibSize = { 0, 0 };
}
void CursorMagnifierOverlay::DestroyWindowInternal()
{
if (m_hwnd)
{
DestroyWindow(m_hwnd);
m_hwnd = nullptr;
}
CleanupResources();
ResetCursorMetrics();
RestoreSystemCursors();
}

View File

@@ -0,0 +1,60 @@
#pragma once
#include "pch.h"
#include <unordered_map>
class CursorMagnifierOverlay
{
public:
CursorMagnifierOverlay() = default;
~CursorMagnifierOverlay();
bool Initialize(HINSTANCE instance);
void Terminate();
void SetVisible(bool visible);
void SetScale(float scale);
void SetAnimationDurationMs(int durationMs);
private:
static constexpr wchar_t kWindowClassName[] = L"FindMyMouseCursorMagnifier";
static constexpr UINT_PTR kTimerId = 1;
static constexpr UINT kFrameIntervalMs = 16;
static LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) noexcept;
void OnTimer();
void Render();
void EnsureResources(int width, int height);
void CleanupResources();
bool HideSystemCursors();
void RestoreSystemCursors();
HCURSOR CreateTransparentCursor() const;
void ReleaseOriginalCursors();
void UpdateCursorMetrics(HCURSOR cursor);
void ResetCursorMetrics();
void DestroyWindowInternal();
void BeginScaleAnimation();
float GetAnimatedScale();
HWND m_hwnd = nullptr;
HINSTANCE m_instance = nullptr;
bool m_visible = false;
float m_targetScale = 2.0f;
float m_startScale = 1.0f;
float m_currentScale = 1.0f;
ULONGLONG m_animationStartTick = 0;
DWORD m_animationDurationMs = 500;
bool m_systemCursorsHidden = false;
std::unordered_map<HCURSOR, UINT> m_hiddenCursorIds;
std::unordered_map<UINT, HCURSOR> m_originalCursors;
HCURSOR m_cachedCursor = nullptr;
SIZE m_cursorSize{ 0, 0 };
POINT m_hotspot{ 0, 0 };
HDC m_memDc = nullptr;
HBITMAP m_dib = nullptr;
void* m_bits = nullptr;
SIZE m_dibSize{ 0, 0 };
};

View File

@@ -2,6 +2,7 @@
//
#include "pch.h"
#include "FindMyMouse.h"
#include "CursorMagnifierOverlay.h"
#include "WinHookEventIDs.h"
#include "trace.h"
#include "common/utils/game_mode.h"
@@ -26,6 +27,11 @@ namespace winrt
using namespace winrt::Windows::System;
}
namespace
{
CursorMagnifierOverlay* g_cursorMagnifier = nullptr;
}
namespace muxc = winrt::Microsoft::UI::Composition;
namespace muxx = winrt::Microsoft::UI::Xaml;
namespace muxxc = winrt::Microsoft::UI::Xaml::Controls;
@@ -56,6 +62,7 @@ protected:
void AfterMoveSonar() {}
void SetSonarVisibility(bool visible) = delete;
void UpdateMouseSnooping();
void UpdateAnimationMethod(FindMyMouseAnimationMethod method);
bool IsForegroundAppExcluded();
protected:
@@ -72,6 +79,7 @@ protected:
bool m_destroyed = false;
FindMyMouseActivationMethod m_activationMethod = FIND_MY_MOUSE_DEFAULT_ACTIVATION_METHOD;
FindMyMouseAnimationMethod m_animationMethod = FIND_MY_MOUSE_DEFAULT_ANIMATION_METHOD;
bool m_includeWinKey = FIND_MY_MOUSE_DEFAULT_INCLUDE_WIN_KEY;
bool m_doNotActivateOnGameMode = FIND_MY_MOUSE_DEFAULT_DO_NOT_ACTIVATE_ON_GAME_MODE;
int m_sonarRadius = FIND_MY_MOUSE_DEFAULT_SPOTLIGHT_RADIUS;
@@ -537,7 +545,15 @@ void SuperSonar<D>::StartSonar(FindMyMouseActivationMethod activationMethod)
m_sonarPos = ptNowhere;
OnMouseTimer();
UpdateMouseSnooping();
Shim()->SetSonarVisibility(true);
const bool showSpotlight = m_animationMethod == FindMyMouseAnimationMethod::Spotlight;
if (showSpotlight)
{
Shim()->SetSonarVisibility(true);
}
if (g_cursorMagnifier)
{
g_cursorMagnifier->SetVisible(m_animationMethod == FindMyMouseAnimationMethod::CursorMagnifier);
}
}
template<typename D>
@@ -549,6 +565,10 @@ void SuperSonar<D>::StopSonar()
Shim()->SetSonarVisibility(false);
KillTimer(m_hwnd, TIMER_ID_TRACK);
}
if (g_cursorMagnifier)
{
g_cursorMagnifier->SetVisible(false);
}
m_sonarState = SonarState::Idle;
UpdateMouseSnooping();
}
@@ -617,6 +637,27 @@ void SuperSonar<D>::UpdateMouseSnooping()
}
}
template<typename D>
void SuperSonar<D>::UpdateAnimationMethod(FindMyMouseAnimationMethod method)
{
if (m_animationMethod == method)
{
return;
}
m_animationMethod = method;
if (m_sonarStart == NoSonar)
{
return;
}
Shim()->SetSonarVisibility(m_animationMethod == FindMyMouseAnimationMethod::Spotlight);
if (g_cursorMagnifier)
{
g_cursorMagnifier->SetVisible(m_animationMethod == FindMyMouseAnimationMethod::CursorMagnifier);
}
}
template<typename D>
bool SuperSonar<D>::IsForegroundAppExcluded()
{
@@ -933,6 +974,7 @@ public:
m_backgroundColor = settings.backgroundColor;
m_spotlightColor = settings.spotlightColor;
m_activationMethod = settings.activationMethod;
m_animationMethod = settings.animationMethod;
m_includeWinKey = settings.includeWinKey;
m_doNotActivateOnGameMode = settings.doNotActivateOnGameMode;
m_fadeDuration = settings.animationDurationMs > 0 ? settings.animationDurationMs : 1;
@@ -941,6 +983,10 @@ public:
m_shakeMinimumDistance = settings.shakeMinimumDistance;
m_shakeIntervalMs = settings.shakeIntervalMs;
m_shakeFactor = settings.shakeFactor;
if (g_cursorMagnifier)
{
g_cursorMagnifier->SetAnimationDurationMs(settings.animationDurationMs);
}
}
else
{
@@ -959,6 +1005,7 @@ public:
m_backgroundColor = localSettings.backgroundColor;
m_spotlightColor = localSettings.spotlightColor;
m_activationMethod = localSettings.activationMethod;
UpdateAnimationMethod(localSettings.animationMethod);
m_includeWinKey = localSettings.includeWinKey;
m_doNotActivateOnGameMode = localSettings.doNotActivateOnGameMode;
m_fadeDuration = localSettings.animationDurationMs > 0 ? localSettings.animationDurationMs : 1;
@@ -968,6 +1015,10 @@ public:
m_shakeIntervalMs = localSettings.shakeIntervalMs;
m_shakeFactor = localSettings.shakeFactor;
UpdateMouseSnooping(); // For the shake mouse activation method
if (g_cursorMagnifier)
{
g_cursorMagnifier->SetAnimationDurationMs(localSettings.animationDurationMs);
}
// Apply new settings to runtime composition objects.
if (m_dimColorBrush)
@@ -1047,6 +1098,10 @@ void FindMyMouseApplySettings(const FindMyMouseSettings& settings)
void FindMyMouseDisable()
{
if (g_cursorMagnifier)
{
g_cursorMagnifier->Terminate();
}
if (m_sonar != nullptr)
{
m_sonar->Terminate();
@@ -1076,6 +1131,18 @@ int FindMyMouseMain(HINSTANCE hinst, const FindMyMouseSettings& settings)
}
m_sonar = &sonar;
CursorMagnifierOverlay cursorMagnifier;
g_cursorMagnifier = &cursorMagnifier;
if (!cursorMagnifier.Initialize(hinst))
{
Logger::warn("Couldn't initialize cursor magnifier overlay.");
g_cursorMagnifier = nullptr;
}
else
{
cursorMagnifier.SetAnimationDurationMs(settings.animationDurationMs);
}
InitializeWinhookEventIds();
MSG msg;
@@ -1088,6 +1155,7 @@ int FindMyMouseMain(HINSTANCE hinst, const FindMyMouseSettings& settings)
}
m_sonar = nullptr;
g_cursorMagnifier = nullptr;
return (int)msg.wParam;
}
@@ -1102,4 +1170,4 @@ HWND GetSonarHwnd() noexcept
return nullptr;
}
#pragma endregion Super_Sonar_API
#pragma endregion Super_Sonar_API

View File

@@ -10,6 +10,13 @@ enum struct FindMyMouseActivationMethod : int
EnumElements = 4, // number of elements in the enum, not counting this
};
enum struct FindMyMouseAnimationMethod : int
{
Spotlight = 0,
CursorMagnifier = 1,
EnumElements = 2, // number of elements in the enum, not counting this
};
constexpr bool FIND_MY_MOUSE_DEFAULT_DO_NOT_ACTIVATE_ON_GAME_MODE = true;
// Default colors now include full alpha. Opacity is encoded directly in color alpha (legacy overlay_opacity migrated into A channel)
const winrt::Windows::UI::Color FIND_MY_MOUSE_DEFAULT_BACKGROUND_COLOR = winrt::Windows::UI::ColorHelper::FromArgb(128, 0, 0, 0);
@@ -18,6 +25,7 @@ constexpr int FIND_MY_MOUSE_DEFAULT_SPOTLIGHT_RADIUS = 100;
constexpr int FIND_MY_MOUSE_DEFAULT_ANIMATION_DURATION_MS = 500;
constexpr int FIND_MY_MOUSE_DEFAULT_SPOTLIGHT_INITIAL_ZOOM = 9;
constexpr FindMyMouseActivationMethod FIND_MY_MOUSE_DEFAULT_ACTIVATION_METHOD = FindMyMouseActivationMethod::DoubleLeftControlKey;
constexpr FindMyMouseAnimationMethod FIND_MY_MOUSE_DEFAULT_ANIMATION_METHOD = FindMyMouseAnimationMethod::Spotlight;
constexpr bool FIND_MY_MOUSE_DEFAULT_INCLUDE_WIN_KEY = false;
constexpr int FIND_MY_MOUSE_DEFAULT_SHAKE_MINIMUM_DISTANCE = 1000;
constexpr int FIND_MY_MOUSE_DEFAULT_SHAKE_INTERVAL_MS = 1000;
@@ -26,6 +34,7 @@ constexpr int FIND_MY_MOUSE_DEFAULT_SHAKE_FACTOR = 400; // 400 percent
struct FindMyMouseSettings
{
FindMyMouseActivationMethod activationMethod = FIND_MY_MOUSE_DEFAULT_ACTIVATION_METHOD;
FindMyMouseAnimationMethod animationMethod = FIND_MY_MOUSE_DEFAULT_ANIMATION_METHOD;
bool includeWinKey = FIND_MY_MOUSE_DEFAULT_INCLUDE_WIN_KEY;
bool doNotActivateOnGameMode = FIND_MY_MOUSE_DEFAULT_DO_NOT_ACTIVATE_ON_GAME_MODE;
winrt::Windows::UI::Color backgroundColor = FIND_MY_MOUSE_DEFAULT_BACKGROUND_COLOR;

View File

@@ -106,6 +106,7 @@
</ClCompile>
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="CursorMagnifierOverlay.h" />
<ClInclude Include="FindMyMouse.h" />
<ClInclude Include="pch.h" />
<ClInclude Include="resource.h" />
@@ -113,6 +114,7 @@
<ClInclude Include="WinHookEventIDs.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="CursorMagnifierOverlay.cpp" />
<ClCompile Include="dllmain.cpp" />
<ClCompile Include="FindMyMouse.cpp" />
<ClCompile Include="pch.cpp">

View File

@@ -33,6 +33,9 @@
<ClCompile Include="WinHookEventIDs.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="CursorMagnifierOverlay.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="pch.h">
@@ -50,6 +53,9 @@
<ClInclude Include="WinHookEventIDs.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="CursorMagnifierOverlay.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="FindMyMouse.rc">
@@ -59,4 +65,4 @@
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
</Project>
</Project>

View File

@@ -16,6 +16,7 @@ namespace
const wchar_t JSON_KEY_PROPERTIES[] = L"properties";
const wchar_t JSON_KEY_VALUE[] = L"value";
const wchar_t JSON_KEY_ACTIVATION_METHOD[] = L"activation_method";
const wchar_t JSON_KEY_ANIMATION_METHOD[] = L"animation_method";
const wchar_t JSON_KEY_INCLUDE_WIN_KEY[] = L"include_win_key";
const wchar_t JSON_KEY_DO_NOT_ACTIVATE_ON_GAME_MODE[] = L"do_not_activate_on_game_mode";
const wchar_t JSON_KEY_BACKGROUND_COLOR[] = L"background_color";
@@ -267,6 +268,24 @@ void FindMyMouse::parse_settings(PowerToysSettings::PowerToyValues& settings)
Logger::warn("Failed to initialize Activation Method from settings. Will use default value");
}
try
{
// Parse Animation Method
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_ANIMATION_METHOD);
int value = static_cast<int>(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
if (value < static_cast<int>(FindMyMouseAnimationMethod::EnumElements) && value >= 0)
{
findMyMouseSettings.animationMethod = static_cast<FindMyMouseAnimationMethod>(value);
}
else
{
throw std::runtime_error("Invalid Animation Method value");
}
}
catch (...)
{
Logger::warn("Failed to initialize Animation Method from settings. Will use default value");
}
try
{
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_INCLUDE_WIN_KEY);
findMyMouseSettings.includeWinKey = jsonPropertiesObject.GetNamedBoolean(JSON_KEY_VALUE);

View File

@@ -16,6 +16,9 @@ namespace Microsoft.PowerToys.Settings.UI.Library
[JsonPropertyName("activation_method")]
public IntProperty ActivationMethod { get; set; }
[JsonPropertyName("animation_method")]
public IntProperty AnimationMethod { get; set; }
[JsonPropertyName("include_win_key")]
public BoolProperty IncludeWinKey { get; set; }
@@ -55,6 +58,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
public FindMyMouseProperties()
{
ActivationMethod = new IntProperty(0);
AnimationMethod = new IntProperty(0);
IncludeWinKey = new BoolProperty(false);
ActivationShortcut = DefaultActivationShortcut;
DoNotActivateOnGameMode = new BoolProperty(true);

View File

@@ -143,6 +143,12 @@
HeaderIcon="{ui:FontIcon Glyph=&#xEB3C;}"
IsEnabled="{x:Bind ViewModel.IsFindMyMouseEnabled, Mode=OneWay}">
<tkcontrols:SettingsExpander.Items>
<tkcontrols:SettingsCard Name="MouseUtilsFindMyMouseAnimationMethod" x:Uid="MouseUtils_FindMyMouse_AnimationMethod">
<ComboBox MinWidth="{StaticResource SettingActionControlMinWidth}" SelectedIndex="{x:Bind ViewModel.FindMyMouseAnimationMethod, Mode=TwoWay}">
<ComboBoxItem x:Uid="MouseUtils_FindMyMouse_AnimationMethod_Spotlight" />
<ComboBoxItem x:Uid="MouseUtils_FindMyMouse_AnimationMethod_CursorMagnifier" />
</ComboBox>
</tkcontrols:SettingsCard>
<!-- Overlay opacity removed; alpha now encoded in colors -->
<tkcontrols:SettingsCard Name="MouseUtilsFindMyMouseBackgroundColor" x:Uid="MouseUtils_FindMyMouse_BackgroundColor">
<controls:ColorPickerButton IsAlphaEnabled="True" SelectedColor="{x:Bind Path=ViewModel.FindMyMouseBackgroundColor, Mode=TwoWay}" />

View File

@@ -2831,6 +2831,15 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
<data name="MouseUtils_FindMyMouse_ActivationMethod.Header" xml:space="preserve">
<value>Activation method</value>
</data>
<data name="MouseUtils_FindMyMouse_AnimationMethod.Header" xml:space="preserve">
<value>Animation method</value>
</data>
<data name="MouseUtils_FindMyMouse_AnimationMethod_Spotlight.Content" xml:space="preserve">
<value>Spotlight</value>
</data>
<data name="MouseUtils_FindMyMouse_AnimationMethod_CursorMagnifier.Content" xml:space="preserve">
<value>Cursor magnifier</value>
</data>
<data name="MouseUtils_FindMyMouse_ActivationDoubleControlPress.Content" xml:space="preserve">
<value>Press Left Control twice</value>
<comment>Left control is the physical key on the keyboard.</comment>
@@ -5769,4 +5778,4 @@ To record a specific window, enter the hotkey with the Alt key in the opposite m
<data name="LightSwitch_FollowNightLightCardMessage.Text" xml:space="preserve">
<value>Following Night Light settings.</value>
</data>
</root>
</root>

View File

@@ -48,6 +48,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
FindMyMouseSettingsConfig = findMyMouseSettingsRepository.SettingsConfig;
_findMyMouseActivationMethod = FindMyMouseSettingsConfig.Properties.ActivationMethod.Value < 4 ? FindMyMouseSettingsConfig.Properties.ActivationMethod.Value : 0;
_findMyMouseAnimationMethod = FindMyMouseSettingsConfig.Properties.AnimationMethod.Value < 2 ? FindMyMouseSettingsConfig.Properties.AnimationMethod.Value : 0;
_findMyMouseIncludeWinKey = FindMyMouseSettingsConfig.Properties.IncludeWinKey.Value;
_findMyMouseDoNotActivateOnGameMode = FindMyMouseSettingsConfig.Properties.DoNotActivateOnGameMode.Value;
@@ -240,6 +241,24 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
}
}
public int FindMyMouseAnimationMethod
{
get
{
return _findMyMouseAnimationMethod;
}
set
{
if (value != _findMyMouseAnimationMethod)
{
_findMyMouseAnimationMethod = value;
FindMyMouseSettingsConfig.Properties.AnimationMethod.Value = value;
NotifyFindMyMousePropertyChanged();
}
}
}
public bool FindMyMouseIncludeWinKey
{
get
@@ -1109,6 +1128,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
private bool _findMyMouseEnabledStateIsGPOConfigured;
private bool _isFindMyMouseEnabled;
private int _findMyMouseActivationMethod;
private int _findMyMouseAnimationMethod;
private bool _findMyMouseIncludeWinKey;
private bool _findMyMouseDoNotActivateOnGameMode;
private string _findMyMouseBackgroundColor;