mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-01-12 23:36:19 +01:00
Compare commits
3 Commits
main
...
dev/vanzue
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ecb37d3e9e | ||
|
|
3e58c10258 | ||
|
|
6b33c78555 |
@@ -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.
|
||||
525
src/modules/MouseUtils/FindMyMouse/CursorMagnifierOverlay.cpp
Normal file
525
src/modules/MouseUtils/FindMyMouse/CursorMagnifierOverlay.cpp
Normal 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();
|
||||
}
|
||||
60
src/modules/MouseUtils/FindMyMouse/CursorMagnifierOverlay.h
Normal file
60
src/modules/MouseUtils/FindMyMouse/CursorMagnifierOverlay.h
Normal 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 };
|
||||
};
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -143,6 +143,12 @@
|
||||
HeaderIcon="{ui:FontIcon Glyph=}"
|
||||
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}" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user