2021-10-22 13:30:18 +01:00
|
|
|
// FindMyMouse.cpp : Based on Raymond Chen's SuperSonar.cpp
|
|
|
|
|
//
|
|
|
|
|
#include "pch.h"
|
|
|
|
|
#include "FindMyMouse.h"
|
2023-09-04 17:58:37 +02:00
|
|
|
#include "WinHookEventIDs.h"
|
2021-10-22 13:30:18 +01:00
|
|
|
#include "trace.h"
|
2021-10-25 19:39:48 +01:00
|
|
|
#include "common/utils/game_mode.h"
|
2022-02-14 18:22:05 +00:00
|
|
|
#include "common/utils/process_path.h"
|
|
|
|
|
#include "common/utils/excluded_apps.h"
|
2023-09-05 15:25:24 +02:00
|
|
|
#include "common/utils/MsWindowsSettings.h"
|
2025-09-26 13:03:00 +08:00
|
|
|
#include <winrt/Windows.Graphics.h>
|
|
|
|
|
|
|
|
|
|
#include <winrt/Microsoft.UI.Composition.Interop.h>
|
|
|
|
|
#include <winrt/Microsoft.UI.Dispatching.h>
|
|
|
|
|
#include <winrt/Microsoft.UI.Xaml.h>
|
|
|
|
|
#include <winrt/Microsoft.UI.Xaml.Controls.h>
|
|
|
|
|
#include <winrt/Microsoft.UI.Xaml.Media.h>
|
|
|
|
|
#include <winrt/Microsoft.UI.Xaml.Hosting.h>
|
|
|
|
|
#include <winrt/Microsoft.UI.Interop.h>
|
|
|
|
|
#include <winrt/Microsoft.UI.Content.h>
|
|
|
|
|
|
2022-02-11 22:52:57 +00:00
|
|
|
#include <vector>
|
2021-10-22 13:30:18 +01:00
|
|
|
|
|
|
|
|
namespace winrt
|
|
|
|
|
{
|
|
|
|
|
using namespace winrt::Windows::System;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-26 13:03:00 +08:00
|
|
|
namespace muxc = winrt::Microsoft::UI::Composition;
|
|
|
|
|
namespace muxx = winrt::Microsoft::UI::Xaml;
|
|
|
|
|
namespace muxxc = winrt::Microsoft::UI::Xaml::Controls;
|
|
|
|
|
namespace muxxh = winrt::Microsoft::UI::Xaml::Hosting;
|
2021-10-22 13:30:18 +01:00
|
|
|
|
|
|
|
|
#pragma region Super_Sonar_Base_Code
|
|
|
|
|
|
|
|
|
|
template<typename D>
|
|
|
|
|
struct SuperSonar
|
|
|
|
|
{
|
|
|
|
|
bool Initialize(HINSTANCE hinst);
|
|
|
|
|
void Terminate();
|
|
|
|
|
|
|
|
|
|
protected:
|
|
|
|
|
// You are expected to override these, as appropriate.
|
|
|
|
|
|
|
|
|
|
DWORD GetExtendedStyle()
|
|
|
|
|
{
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
LRESULT WndProc(UINT message, WPARAM wParam, LPARAM lParam) noexcept
|
|
|
|
|
{
|
|
|
|
|
return BaseWndProc(message, wParam, lParam);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void BeforeMoveSonar() {}
|
|
|
|
|
void AfterMoveSonar() {}
|
|
|
|
|
void SetSonarVisibility(bool visible) = delete;
|
2022-02-11 22:52:57 +00:00
|
|
|
void UpdateMouseSnooping();
|
2022-02-14 18:22:05 +00:00
|
|
|
bool IsForegroundAppExcluded();
|
2021-10-22 13:30:18 +01:00
|
|
|
|
|
|
|
|
protected:
|
|
|
|
|
// Base class members you can access.
|
|
|
|
|
D* Shim() { return static_cast<D*>(this); }
|
|
|
|
|
LRESULT BaseWndProc(UINT message, WPARAM wParam, LPARAM lParam) noexcept;
|
|
|
|
|
|
2023-05-02 06:56:55 -04:00
|
|
|
HWND m_hwnd{};
|
2021-10-22 13:30:18 +01:00
|
|
|
POINT m_sonarPos = ptNowhere;
|
|
|
|
|
|
2021-10-29 12:45:04 +01:00
|
|
|
// Only consider double left control click if at least 100ms passed between the clicks, to avoid keyboards that might be sending rapid clicks.
|
2021-11-08 11:36:38 +00:00
|
|
|
// At actual check, time a fifth of the current double click setting might be used instead to take into account users who might have low values.
|
2021-10-29 12:45:04 +01:00
|
|
|
static const int MIN_DOUBLE_CLICK_TIME = 100;
|
|
|
|
|
|
2021-11-23 16:52:17 +00:00
|
|
|
bool m_destroyed = false;
|
2022-02-11 22:52:57 +00:00
|
|
|
FindMyMouseActivationMethod m_activationMethod = FIND_MY_MOUSE_DEFAULT_ACTIVATION_METHOD;
|
2024-06-03 07:44:11 -04:00
|
|
|
bool m_includeWinKey = FIND_MY_MOUSE_DEFAULT_INCLUDE_WIN_KEY;
|
2022-02-11 22:52:57 +00:00
|
|
|
bool m_doNotActivateOnGameMode = FIND_MY_MOUSE_DEFAULT_DO_NOT_ACTIVATE_ON_GAME_MODE;
|
2021-11-23 16:52:17 +00:00
|
|
|
int m_sonarRadius = FIND_MY_MOUSE_DEFAULT_SPOTLIGHT_RADIUS;
|
|
|
|
|
int m_sonarZoomFactor = FIND_MY_MOUSE_DEFAULT_SPOTLIGHT_INITIAL_ZOOM;
|
|
|
|
|
DWORD m_fadeDuration = FIND_MY_MOUSE_DEFAULT_ANIMATION_DURATION_MS;
|
2022-02-14 18:22:05 +00:00
|
|
|
std::vector<std::wstring> m_excludedApps;
|
2022-03-04 12:28:11 +00:00
|
|
|
int m_shakeMinimumDistance = FIND_MY_MOUSE_DEFAULT_SHAKE_MINIMUM_DISTANCE;
|
2025-09-26 13:03:00 +08:00
|
|
|
winrt::Microsoft::UI::Dispatching::DispatcherQueueController m_dispatcherQueueController{ nullptr };
|
2021-10-22 13:30:18 +01:00
|
|
|
|
2024-01-16 17:35:54 +00:00
|
|
|
// Don't consider movements started past these milliseconds to detect shaking.
|
|
|
|
|
int m_shakeIntervalMs = FIND_MY_MOUSE_DEFAULT_SHAKE_INTERVAL_MS;
|
|
|
|
|
// By which factor must travelled distance be than the diagonal of the rectangle containing the movements. (value in percent)
|
|
|
|
|
int m_shakeFactor = FIND_MY_MOUSE_DEFAULT_SHAKE_FACTOR;
|
|
|
|
|
|
2021-10-22 13:30:18 +01:00
|
|
|
private:
|
2022-02-11 22:52:57 +00:00
|
|
|
// Save the mouse movement that occurred in any direction.
|
|
|
|
|
struct PointerRecentMovement
|
|
|
|
|
{
|
|
|
|
|
POINT diff;
|
|
|
|
|
ULONGLONG tick;
|
|
|
|
|
};
|
|
|
|
|
std::vector<PointerRecentMovement> m_movementHistory;
|
|
|
|
|
// Raw Input may give relative or absolute values. Need to take each case into account.
|
|
|
|
|
bool m_seenAnAbsoluteMousePosition = false;
|
|
|
|
|
POINT m_lastAbsolutePosition = { 0, 0 };
|
|
|
|
|
|
|
|
|
|
static inline byte GetSign(LONG const& num)
|
|
|
|
|
{
|
|
|
|
|
if (num > 0)
|
|
|
|
|
return 1;
|
|
|
|
|
if (num < 0)
|
|
|
|
|
return -1;
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
2021-10-22 13:30:18 +01:00
|
|
|
static bool IsEqual(POINT const& p1, POINT const& p2)
|
|
|
|
|
{
|
|
|
|
|
return p1.x == p2.x && p1.y == p2.y;
|
|
|
|
|
}
|
|
|
|
|
|
2022-07-12 12:58:16 +01:00
|
|
|
static constexpr POINT ptNowhere = { LONG_MIN, LONG_MIN };
|
2021-10-22 13:30:18 +01:00
|
|
|
static constexpr DWORD TIMER_ID_TRACK = 100;
|
|
|
|
|
static constexpr DWORD IdlePeriod = 1000;
|
|
|
|
|
|
|
|
|
|
// Activate sonar: Hit LeftControl twice.
|
|
|
|
|
enum class SonarState
|
|
|
|
|
{
|
|
|
|
|
Idle,
|
|
|
|
|
ControlDown1,
|
|
|
|
|
ControlUp1,
|
|
|
|
|
ControlDown2,
|
|
|
|
|
ControlUp2,
|
|
|
|
|
};
|
|
|
|
|
|
2023-05-02 06:56:55 -04:00
|
|
|
HWND m_hwndOwner{};
|
2021-10-22 13:30:18 +01:00
|
|
|
SonarState m_sonarState = SonarState::Idle;
|
|
|
|
|
POINT m_lastKeyPos{};
|
2022-02-11 22:52:57 +00:00
|
|
|
ULONGLONG m_lastKeyTime{};
|
2021-10-22 13:30:18 +01:00
|
|
|
|
|
|
|
|
static constexpr DWORD NoSonar = 0;
|
|
|
|
|
static constexpr DWORD SonarWaitingForMouseMove = 1;
|
2022-02-11 22:52:57 +00:00
|
|
|
ULONGLONG m_sonarStart = NoSonar;
|
2021-10-22 13:30:18 +01:00
|
|
|
bool m_isSnoopingMouse = false;
|
|
|
|
|
|
|
|
|
|
private:
|
|
|
|
|
static constexpr auto className = L"FindMyMouse";
|
|
|
|
|
|
2021-12-09 11:08:53 +00:00
|
|
|
static constexpr auto windowTitle = L"PowerToys Find My Mouse";
|
2021-10-22 13:30:18 +01:00
|
|
|
|
|
|
|
|
static LRESULT CALLBACK s_WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam);
|
|
|
|
|
|
|
|
|
|
BOOL OnSonarCreate();
|
|
|
|
|
void OnSonarDestroy();
|
|
|
|
|
void OnSonarInput(WPARAM flags, HRAWINPUT hInput);
|
|
|
|
|
void OnSonarKeyboardInput(RAWINPUT const& input);
|
|
|
|
|
void OnSonarMouseInput(RAWINPUT const& input);
|
|
|
|
|
void OnMouseTimer();
|
|
|
|
|
|
2022-02-11 22:52:57 +00:00
|
|
|
void DetectShake();
|
2024-06-03 07:44:11 -04:00
|
|
|
bool KeyboardInputCanActivate();
|
2022-02-11 22:52:57 +00:00
|
|
|
|
2026-01-05 11:12:25 +08:00
|
|
|
void StartSonar(FindMyMouseActivationMethod activationMethod);
|
2021-10-22 13:30:18 +01:00
|
|
|
void StopSonar();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
template<typename D>
|
|
|
|
|
bool SuperSonar<D>::Initialize(HINSTANCE hinst)
|
|
|
|
|
{
|
|
|
|
|
SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
|
|
|
|
|
|
|
|
|
|
WNDCLASS wc{};
|
|
|
|
|
if (!GetClassInfoW(hinst, className, &wc))
|
|
|
|
|
{
|
|
|
|
|
wc.lpfnWndProc = s_WndProc;
|
|
|
|
|
wc.hInstance = hinst;
|
|
|
|
|
wc.hIcon = LoadIcon(hinst, IDI_APPLICATION);
|
|
|
|
|
wc.hCursor = LoadCursor(nullptr, IDC_ARROW);
|
2023-02-08 11:01:35 +00:00
|
|
|
wc.hbrBackground = static_cast<HBRUSH>(GetStockObject(NULL_BRUSH));
|
2021-10-22 13:30:18 +01:00
|
|
|
wc.lpszClassName = className;
|
|
|
|
|
|
|
|
|
|
if (!RegisterClassW(&wc))
|
|
|
|
|
{
|
2025-09-26 13:03:00 +08:00
|
|
|
Logger::error("RegisterClassW failed. GetLastError={}", GetLastError());
|
2021-10-22 13:30:18 +01:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-09-26 13:03:00 +08:00
|
|
|
// else: class already registered
|
2021-10-22 13:30:18 +01:00
|
|
|
|
|
|
|
|
m_hwndOwner = CreateWindow(L"static", nullptr, WS_POPUP, 0, 0, 0, 0, nullptr, nullptr, hinst, nullptr);
|
2025-09-26 13:03:00 +08:00
|
|
|
if (!m_hwndOwner)
|
|
|
|
|
{
|
|
|
|
|
Logger::error("Failed to create owner window. GetLastError={}", GetLastError());
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2021-10-22 13:30:18 +01:00
|
|
|
|
2025-10-27 09:33:26 +08:00
|
|
|
DWORD exStyle = WS_EX_TRANSPARENT | WS_EX_LAYERED | WS_EX_TOOLWINDOW | Shim()->GetExtendedStyle();
|
2025-09-26 13:03:00 +08:00
|
|
|
HWND created = CreateWindowExW(exStyle, className, windowTitle, WS_POPUP, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, m_hwndOwner, nullptr, hinst, this);
|
|
|
|
|
if (!created)
|
|
|
|
|
{
|
|
|
|
|
Logger::error("CreateWindowExW failed. GetLastError={}", GetLastError());
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
2021-10-22 13:30:18 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
template<typename D>
|
|
|
|
|
void SuperSonar<D>::Terminate()
|
|
|
|
|
{
|
|
|
|
|
auto dispatcherQueue = m_dispatcherQueueController.DispatcherQueue();
|
|
|
|
|
bool enqueueSucceeded = dispatcherQueue.TryEnqueue([=]() {
|
2021-11-23 16:52:17 +00:00
|
|
|
m_destroyed = true;
|
2021-10-22 13:30:18 +01:00
|
|
|
DestroyWindow(m_hwndOwner);
|
|
|
|
|
});
|
|
|
|
|
if (!enqueueSucceeded)
|
|
|
|
|
{
|
|
|
|
|
Logger::error("Couldn't enqueue message to destroy the sonar Window.");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
template<typename D>
|
|
|
|
|
LRESULT SuperSonar<D>::s_WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
|
|
|
|
|
{
|
|
|
|
|
SuperSonar* self;
|
|
|
|
|
if (message == WM_NCCREATE)
|
|
|
|
|
{
|
2023-02-08 11:01:35 +00:00
|
|
|
auto info = reinterpret_cast<LPCREATESTRUCT>(lParam);
|
|
|
|
|
SetWindowLongPtr(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(info->lpCreateParams));
|
|
|
|
|
self = static_cast<SuperSonar*>(info->lpCreateParams);
|
2021-10-22 13:30:18 +01:00
|
|
|
self->m_hwnd = hwnd;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2023-02-08 11:01:35 +00:00
|
|
|
self = reinterpret_cast<SuperSonar*>(GetWindowLongPtr(hwnd, GWLP_USERDATA));
|
2021-10-22 13:30:18 +01:00
|
|
|
}
|
|
|
|
|
if (self)
|
|
|
|
|
{
|
|
|
|
|
return self->Shim()->WndProc(message, wParam, lParam);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
return DefWindowProc(hwnd, message, wParam, lParam);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
template<typename D>
|
|
|
|
|
LRESULT SuperSonar<D>::BaseWndProc(UINT message, WPARAM wParam, LPARAM lParam) noexcept
|
|
|
|
|
{
|
|
|
|
|
switch (message)
|
|
|
|
|
{
|
|
|
|
|
case WM_CREATE:
|
2025-09-26 13:03:00 +08:00
|
|
|
if (!OnSonarCreate())
|
|
|
|
|
return -1;
|
2022-02-11 22:52:57 +00:00
|
|
|
UpdateMouseSnooping();
|
|
|
|
|
return 0;
|
2021-10-22 13:30:18 +01:00
|
|
|
|
|
|
|
|
case WM_DESTROY:
|
|
|
|
|
OnSonarDestroy();
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case WM_INPUT:
|
2023-02-08 11:01:35 +00:00
|
|
|
OnSonarInput(wParam, reinterpret_cast<HRAWINPUT>(lParam));
|
2021-10-22 13:30:18 +01:00
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case WM_TIMER:
|
|
|
|
|
switch (wParam)
|
|
|
|
|
{
|
|
|
|
|
case TIMER_ID_TRACK:
|
|
|
|
|
OnMouseTimer();
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case WM_NCHITTEST:
|
|
|
|
|
return HTTRANSPARENT;
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-04 17:58:37 +02:00
|
|
|
if (message == WM_PRIV_SHORTCUT)
|
|
|
|
|
{
|
|
|
|
|
if (m_sonarStart == NoSonar)
|
|
|
|
|
{
|
2026-01-05 11:12:25 +08:00
|
|
|
StartSonar(FindMyMouseActivationMethod::Shortcut);
|
2023-09-04 17:58:37 +02:00
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
StopSonar();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-10-22 13:30:18 +01:00
|
|
|
return DefWindowProc(m_hwnd, message, wParam, lParam);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
template<typename D>
|
|
|
|
|
BOOL SuperSonar<D>::OnSonarCreate()
|
|
|
|
|
{
|
|
|
|
|
RAWINPUTDEVICE keyboard{};
|
|
|
|
|
keyboard.usUsagePage = HID_USAGE_PAGE_GENERIC;
|
|
|
|
|
keyboard.usUsage = HID_USAGE_GENERIC_KEYBOARD;
|
|
|
|
|
keyboard.dwFlags = RIDEV_INPUTSINK;
|
|
|
|
|
keyboard.hwndTarget = m_hwnd;
|
|
|
|
|
return RegisterRawInputDevices(&keyboard, 1, sizeof(keyboard));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
template<typename D>
|
|
|
|
|
void SuperSonar<D>::OnSonarDestroy()
|
|
|
|
|
{
|
|
|
|
|
PostQuitMessage(0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
template<typename D>
|
|
|
|
|
void SuperSonar<D>::OnSonarInput(WPARAM flags, HRAWINPUT hInput)
|
|
|
|
|
{
|
|
|
|
|
RAWINPUT input;
|
|
|
|
|
UINT size = sizeof(input);
|
|
|
|
|
auto result = GetRawInputData(hInput, RID_INPUT, &input, &size, sizeof(RAWINPUTHEADER));
|
2023-02-08 11:01:35 +00:00
|
|
|
if (result < sizeof(RAWINPUTHEADER))
|
2021-10-22 13:30:18 +01:00
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch (input.header.dwType)
|
|
|
|
|
{
|
|
|
|
|
case RIM_TYPEKEYBOARD:
|
|
|
|
|
OnSonarKeyboardInput(input);
|
|
|
|
|
break;
|
|
|
|
|
case RIM_TYPEMOUSE:
|
|
|
|
|
OnSonarMouseInput(input);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
template<typename D>
|
|
|
|
|
void SuperSonar<D>::OnSonarKeyboardInput(RAWINPUT const& input)
|
|
|
|
|
{
|
2023-09-04 17:58:37 +02:00
|
|
|
// Don't stop the sonar when the shortcut is released
|
|
|
|
|
if (m_activationMethod == FindMyMouseActivationMethod::Shortcut && (input.data.keyboard.Flags & RI_KEY_BREAK) != 0)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-26 13:03:00 +08:00
|
|
|
if ((m_activationMethod != FindMyMouseActivationMethod::DoubleRightControlKey && m_activationMethod != FindMyMouseActivationMethod::DoubleLeftControlKey) || input.data.keyboard.VKey != VK_CONTROL)
|
2021-10-22 13:30:18 +01:00
|
|
|
{
|
|
|
|
|
StopSonar();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool pressed = (input.data.keyboard.Flags & RI_KEY_BREAK) == 0;
|
|
|
|
|
|
2023-09-04 17:58:37 +02:00
|
|
|
bool leftCtrlPressed = (input.data.keyboard.Flags & RI_KEY_E0) == 0;
|
|
|
|
|
bool rightCtrlPressed = (input.data.keyboard.Flags & RI_KEY_E0) != 0;
|
|
|
|
|
|
2025-09-26 13:03:00 +08:00
|
|
|
if ((m_activationMethod == FindMyMouseActivationMethod::DoubleRightControlKey && !rightCtrlPressed) || (m_activationMethod == FindMyMouseActivationMethod::DoubleLeftControlKey && !leftCtrlPressed))
|
2021-10-22 13:30:18 +01:00
|
|
|
{
|
|
|
|
|
StopSonar();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch (m_sonarState)
|
|
|
|
|
{
|
|
|
|
|
case SonarState::Idle:
|
|
|
|
|
if (pressed)
|
|
|
|
|
{
|
|
|
|
|
m_sonarState = SonarState::ControlDown1;
|
2022-02-11 22:52:57 +00:00
|
|
|
m_lastKeyTime = GetTickCount64();
|
2021-10-22 13:30:18 +01:00
|
|
|
m_lastKeyPos = {};
|
|
|
|
|
GetCursorPos(&m_lastKeyPos);
|
|
|
|
|
UpdateMouseSnooping();
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case SonarState::ControlDown1:
|
|
|
|
|
if (!pressed)
|
|
|
|
|
{
|
|
|
|
|
m_sonarState = SonarState::ControlUp1;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case SonarState::ControlUp1:
|
2024-06-03 07:44:11 -04:00
|
|
|
if (pressed && KeyboardInputCanActivate())
|
2021-10-22 13:30:18 +01:00
|
|
|
{
|
2022-02-11 22:52:57 +00:00
|
|
|
auto now = GetTickCount64();
|
2021-10-29 12:45:04 +01:00
|
|
|
auto doubleClickInterval = now - m_lastKeyTime;
|
2021-10-22 13:30:18 +01:00
|
|
|
POINT ptCursor{};
|
2021-11-08 11:36:38 +00:00
|
|
|
auto doubleClickTimeSetting = GetDoubleClickTime();
|
2021-10-22 13:30:18 +01:00
|
|
|
if (GetCursorPos(&ptCursor) &&
|
2021-11-08 11:36:38 +00:00
|
|
|
doubleClickInterval >= min(MIN_DOUBLE_CLICK_TIME, doubleClickTimeSetting / 5) &&
|
|
|
|
|
doubleClickInterval <= doubleClickTimeSetting &&
|
2021-10-22 13:30:18 +01:00
|
|
|
IsEqual(m_lastKeyPos, ptCursor))
|
|
|
|
|
{
|
2021-10-29 12:45:04 +01:00
|
|
|
m_sonarState = SonarState::ControlDown2;
|
2026-01-05 11:12:25 +08:00
|
|
|
StartSonar(m_activationMethod);
|
2021-10-22 13:30:18 +01:00
|
|
|
}
|
2021-10-29 12:45:04 +01:00
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
m_sonarState = SonarState::ControlDown1;
|
2022-02-11 22:52:57 +00:00
|
|
|
m_lastKeyTime = GetTickCount64();
|
2021-10-29 12:45:04 +01:00
|
|
|
m_lastKeyPos = {};
|
|
|
|
|
GetCursorPos(&m_lastKeyPos);
|
|
|
|
|
UpdateMouseSnooping();
|
|
|
|
|
}
|
2021-10-22 13:30:18 +01:00
|
|
|
m_lastKeyTime = now;
|
|
|
|
|
m_lastKeyPos = ptCursor;
|
|
|
|
|
}
|
|
|
|
|
break;
|
2021-10-29 12:45:04 +01:00
|
|
|
case SonarState::ControlUp2:
|
|
|
|
|
// Also deactivate sonar with left control.
|
|
|
|
|
if (pressed)
|
|
|
|
|
{
|
|
|
|
|
StopSonar();
|
|
|
|
|
}
|
|
|
|
|
break;
|
2021-10-22 13:30:18 +01:00
|
|
|
case SonarState::ControlDown2:
|
|
|
|
|
if (!pressed)
|
|
|
|
|
{
|
|
|
|
|
m_sonarState = SonarState::ControlUp2;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-02-11 22:52:57 +00:00
|
|
|
// Shaking detection algorithm is: Has distance travelled been much greater than the diagonal of the rectangle containing the movement?
|
|
|
|
|
template<typename D>
|
|
|
|
|
void SuperSonar<D>::DetectShake()
|
|
|
|
|
{
|
2024-01-16 17:35:54 +00:00
|
|
|
ULONGLONG shakeStartTick = GetTickCount64() - m_shakeIntervalMs;
|
2025-09-26 13:03:00 +08:00
|
|
|
|
2022-02-11 22:52:57 +00:00
|
|
|
// Prune the story of movements for those movements that started too long ago.
|
|
|
|
|
std::erase_if(m_movementHistory, [shakeStartTick](const PointerRecentMovement& movement) { return movement.tick < shakeStartTick; });
|
2025-09-26 13:03:00 +08:00
|
|
|
|
2022-02-11 22:52:57 +00:00
|
|
|
double distanceTravelled = 0;
|
2025-09-26 13:03:00 +08:00
|
|
|
LONGLONG currentX = 0, minX = 0, maxX = 0;
|
|
|
|
|
LONGLONG currentY = 0, minY = 0, maxY = 0;
|
2022-02-11 22:52:57 +00:00
|
|
|
|
|
|
|
|
for (const PointerRecentMovement& movement : m_movementHistory)
|
|
|
|
|
{
|
|
|
|
|
currentX += movement.diff.x;
|
|
|
|
|
currentY += movement.diff.y;
|
2023-02-08 11:01:35 +00:00
|
|
|
distanceTravelled += sqrt(static_cast<double>(movement.diff.x) * movement.diff.x + static_cast<double>(movement.diff.y) * movement.diff.y); // Pythagorean theorem
|
2022-02-11 22:52:57 +00:00
|
|
|
minX = min(currentX, minX);
|
|
|
|
|
maxX = max(currentX, maxX);
|
|
|
|
|
minY = min(currentY, minY);
|
|
|
|
|
maxY = max(currentY, maxY);
|
|
|
|
|
}
|
2025-09-26 13:03:00 +08:00
|
|
|
|
2022-03-04 12:28:11 +00:00
|
|
|
if (distanceTravelled < m_shakeMinimumDistance)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
Updates for check-spelling v0.0.25 (#40386)
## Summary of the Pull Request
- #39572 updated check-spelling but ignored:
> 🐣 Breaking Changes
[Code Scanning action requires a Code Scanning
Ruleset](https://github.com/check-spelling/check-spelling/wiki/Breaking-Change:-Code-Scanning-action-requires-a-Code-Scanning-Ruleset)
If you use SARIF reporting, then instead of the workflow yielding an ❌
when it fails, it will rely on [github-advanced-security
🤖](https://github.com/apps/github-advanced-security) to report the
failure. You will need to adjust your checks for PRs.
This means that check-spelling hasn't been properly doing its job 😦.
I'm sorry, I should have pushed a thing to this repo earlier,...
Anyway, as with most refreshes, this comes with a number of fixes, some
are fixes for typos that snuck in before the 0.0.25 upgrade, some are
for things that snuck in after, some are based on new rules in
spell-check-this, and some are hand written patterns based on running
through this repository a few times.
About the 🐣 **breaking change**: someone needs to create a ruleset for
this repository (see [Code Scanning action requires a Code Scanning
Ruleset: Sample ruleset
](https://github.com/check-spelling/check-spelling/wiki/Breaking-Change:-Code-Scanning-action-requires-a-Code-Scanning-Ruleset#sample-ruleset)).
The alternative to adding a ruleset is to change the condition to not
use sarif for this repository. In general, I think the github
integration from sarif is prettier/more helpful, so I think that it's
the better choice.
You can see an example of it working in:
- https://github.com/check-spelling-sandbox/PowerToys/pull/23
---------
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
Co-authored-by: Mike Griese <migrie@microsoft.com>
Co-authored-by: Dustin L. Howett <dustin@howett.net>
2025-07-08 18:16:52 -04:00
|
|
|
// Size of the rectangle that the pointer moved in.
|
2025-09-26 13:03:00 +08:00
|
|
|
double rectangleWidth = static_cast<double>(maxX) - minX;
|
|
|
|
|
double rectangleHeight = static_cast<double>(maxY) - minY;
|
2022-02-11 22:52:57 +00:00
|
|
|
|
|
|
|
|
double diagonal = sqrt(rectangleWidth * rectangleWidth + rectangleHeight * rectangleHeight);
|
2025-09-26 13:03:00 +08:00
|
|
|
if (diagonal > 0 && distanceTravelled / diagonal > (m_shakeFactor / 100.f))
|
2022-02-11 22:52:57 +00:00
|
|
|
{
|
|
|
|
|
m_movementHistory.clear();
|
2026-01-05 11:12:25 +08:00
|
|
|
StartSonar(m_activationMethod);
|
2022-02-11 22:52:57 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-03 07:44:11 -04:00
|
|
|
template<typename D>
|
|
|
|
|
bool SuperSonar<D>::KeyboardInputCanActivate()
|
|
|
|
|
{
|
|
|
|
|
return !m_includeWinKey || (GetAsyncKeyState(VK_LWIN) & 0x8000) || (GetAsyncKeyState(VK_RWIN) & 0x8000);
|
|
|
|
|
}
|
|
|
|
|
|
2021-10-22 13:30:18 +01:00
|
|
|
template<typename D>
|
|
|
|
|
void SuperSonar<D>::OnSonarMouseInput(RAWINPUT const& input)
|
|
|
|
|
{
|
2022-02-11 22:52:57 +00:00
|
|
|
if (m_activationMethod == FindMyMouseActivationMethod::ShakeMouse)
|
|
|
|
|
{
|
|
|
|
|
LONG relativeX = 0;
|
|
|
|
|
LONG relativeY = 0;
|
2025-09-26 13:03:00 +08:00
|
|
|
if ((input.data.mouse.usFlags & MOUSE_MOVE_ABSOLUTE) == MOUSE_MOVE_ABSOLUTE && (input.data.mouse.lLastX != 0 || input.data.mouse.lLastY != 0))
|
2022-02-11 22:52:57 +00:00
|
|
|
{
|
|
|
|
|
// Getting absolute mouse coordinates. Likely inside a VM / RDP session.
|
|
|
|
|
if (m_seenAnAbsoluteMousePosition)
|
|
|
|
|
{
|
|
|
|
|
relativeX = input.data.mouse.lLastX - m_lastAbsolutePosition.x;
|
|
|
|
|
relativeY = input.data.mouse.lLastY - m_lastAbsolutePosition.y;
|
|
|
|
|
m_lastAbsolutePosition.x = input.data.mouse.lLastX;
|
|
|
|
|
m_lastAbsolutePosition.y = input.data.mouse.lLastY;
|
|
|
|
|
}
|
|
|
|
|
m_seenAnAbsoluteMousePosition = true;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
relativeX = input.data.mouse.lLastX;
|
|
|
|
|
relativeY = input.data.mouse.lLastY;
|
|
|
|
|
}
|
|
|
|
|
if (m_movementHistory.size() > 0)
|
|
|
|
|
{
|
|
|
|
|
PointerRecentMovement& lastMovement = m_movementHistory.back();
|
|
|
|
|
// If the pointer is still moving in the same direction, just add to that movement instead of adding a new movement.
|
|
|
|
|
// This helps in keeping the list of movements smaller even in cases where a high number of messages is sent.
|
|
|
|
|
if (GetSign(lastMovement.diff.x) == GetSign(relativeX) && GetSign(lastMovement.diff.y) == GetSign(relativeY))
|
|
|
|
|
{
|
|
|
|
|
lastMovement.diff.x += relativeX;
|
|
|
|
|
lastMovement.diff.y += relativeY;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2025-09-26 13:03:00 +08:00
|
|
|
m_movementHistory.push_back({ .diff = { .x = relativeX, .y = relativeY }, .tick = GetTickCount64() });
|
2022-02-11 22:52:57 +00:00
|
|
|
// Mouse movement changed directions. Take the opportunity do detect shake.
|
|
|
|
|
DetectShake();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
m_movementHistory.push_back({ .diff = { .x = relativeX, .y = relativeY }, .tick = GetTickCount64() });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-10-22 13:30:18 +01:00
|
|
|
if (input.data.mouse.usButtonFlags)
|
|
|
|
|
{
|
|
|
|
|
StopSonar();
|
|
|
|
|
}
|
|
|
|
|
else if (m_sonarStart != NoSonar)
|
|
|
|
|
{
|
|
|
|
|
OnMouseTimer();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
template<typename D>
|
2026-01-05 11:12:25 +08:00
|
|
|
void SuperSonar<D>::StartSonar(FindMyMouseActivationMethod activationMethod)
|
2021-10-22 13:30:18 +01:00
|
|
|
{
|
2022-02-11 22:52:57 +00:00
|
|
|
// Don't activate if game mode is on.
|
|
|
|
|
if (m_doNotActivateOnGameMode && detect_game_mode())
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2022-02-14 18:22:05 +00:00
|
|
|
if (IsForegroundAppExcluded())
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-05 11:12:25 +08:00
|
|
|
Trace::MousePointerFocused(static_cast<int>(activationMethod));
|
2021-10-22 13:30:18 +01:00
|
|
|
// Cover the entire virtual screen.
|
Updates for check-spelling v0.0.25 (#40386)
## Summary of the Pull Request
- #39572 updated check-spelling but ignored:
> 🐣 Breaking Changes
[Code Scanning action requires a Code Scanning
Ruleset](https://github.com/check-spelling/check-spelling/wiki/Breaking-Change:-Code-Scanning-action-requires-a-Code-Scanning-Ruleset)
If you use SARIF reporting, then instead of the workflow yielding an ❌
when it fails, it will rely on [github-advanced-security
🤖](https://github.com/apps/github-advanced-security) to report the
failure. You will need to adjust your checks for PRs.
This means that check-spelling hasn't been properly doing its job 😦.
I'm sorry, I should have pushed a thing to this repo earlier,...
Anyway, as with most refreshes, this comes with a number of fixes, some
are fixes for typos that snuck in before the 0.0.25 upgrade, some are
for things that snuck in after, some are based on new rules in
spell-check-this, and some are hand written patterns based on running
through this repository a few times.
About the 🐣 **breaking change**: someone needs to create a ruleset for
this repository (see [Code Scanning action requires a Code Scanning
Ruleset: Sample ruleset
](https://github.com/check-spelling/check-spelling/wiki/Breaking-Change:-Code-Scanning-action-requires-a-Code-Scanning-Ruleset#sample-ruleset)).
The alternative to adding a ruleset is to change the condition to not
use sarif for this repository. In general, I think the github
integration from sarif is prettier/more helpful, so I think that it's
the better choice.
You can see an example of it working in:
- https://github.com/check-spelling-sandbox/PowerToys/pull/23
---------
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
Co-authored-by: Mike Griese <migrie@microsoft.com>
Co-authored-by: Dustin L. Howett <dustin@howett.net>
2025-07-08 18:16:52 -04:00
|
|
|
// HACK: Draw with 1 pixel off. Otherwise, Windows glitches the task bar transparency when a transparent window fill the whole screen.
|
2025-10-27 09:33:26 +08:00
|
|
|
SetWindowPos(m_hwnd, HWND_TOPMOST, GetSystemMetrics(SM_XVIRTUALSCREEN) + 1, GetSystemMetrics(SM_YVIRTUALSCREEN) + 1, GetSystemMetrics(SM_CXVIRTUALSCREEN) - 2, GetSystemMetrics(SM_CYVIRTUALSCREEN) - 2, 0);
|
2021-10-22 13:30:18 +01:00
|
|
|
m_sonarPos = ptNowhere;
|
|
|
|
|
OnMouseTimer();
|
|
|
|
|
UpdateMouseSnooping();
|
|
|
|
|
Shim()->SetSonarVisibility(true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
template<typename D>
|
|
|
|
|
void SuperSonar<D>::StopSonar()
|
|
|
|
|
{
|
|
|
|
|
if (m_sonarStart != NoSonar)
|
|
|
|
|
{
|
|
|
|
|
m_sonarStart = NoSonar;
|
|
|
|
|
Shim()->SetSonarVisibility(false);
|
|
|
|
|
KillTimer(m_hwnd, TIMER_ID_TRACK);
|
|
|
|
|
}
|
|
|
|
|
m_sonarState = SonarState::Idle;
|
|
|
|
|
UpdateMouseSnooping();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
template<typename D>
|
|
|
|
|
void SuperSonar<D>::OnMouseTimer()
|
|
|
|
|
{
|
2022-02-11 22:52:57 +00:00
|
|
|
auto now = GetTickCount64();
|
2021-10-22 13:30:18 +01:00
|
|
|
|
|
|
|
|
// If mouse has moved, then reset the sonar timer.
|
|
|
|
|
POINT ptCursor{};
|
|
|
|
|
if (!GetCursorPos(&ptCursor))
|
|
|
|
|
{
|
|
|
|
|
// We are no longer the active desktop - done.
|
|
|
|
|
StopSonar();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
ScreenToClient(m_hwnd, &ptCursor);
|
|
|
|
|
|
|
|
|
|
if (IsEqual(m_sonarPos, ptCursor))
|
|
|
|
|
{
|
|
|
|
|
// Mouse is stationary.
|
|
|
|
|
if (m_sonarStart != SonarWaitingForMouseMove && now - m_sonarStart >= IdlePeriod)
|
|
|
|
|
{
|
|
|
|
|
StopSonar();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
// Mouse has moved.
|
|
|
|
|
if (IsEqual(m_sonarPos, ptNowhere))
|
|
|
|
|
{
|
|
|
|
|
// Initial call, mark sonar as active but waiting for first mouse-move.
|
|
|
|
|
now = SonarWaitingForMouseMove;
|
|
|
|
|
}
|
|
|
|
|
SetTimer(m_hwnd, TIMER_ID_TRACK, IdlePeriod, nullptr);
|
|
|
|
|
Shim()->BeforeMoveSonar();
|
|
|
|
|
m_sonarPos = ptCursor;
|
|
|
|
|
m_sonarStart = now;
|
|
|
|
|
Shim()->AfterMoveSonar();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
template<typename D>
|
|
|
|
|
void SuperSonar<D>::UpdateMouseSnooping()
|
|
|
|
|
{
|
2022-02-11 22:52:57 +00:00
|
|
|
bool wantSnoopingMouse = m_sonarStart != NoSonar || m_sonarState != SonarState::Idle || m_activationMethod == FindMyMouseActivationMethod::ShakeMouse;
|
2021-10-22 13:30:18 +01:00
|
|
|
if (m_isSnoopingMouse != wantSnoopingMouse)
|
|
|
|
|
{
|
|
|
|
|
m_isSnoopingMouse = wantSnoopingMouse;
|
|
|
|
|
RAWINPUTDEVICE mouse{};
|
|
|
|
|
mouse.usUsagePage = HID_USAGE_PAGE_GENERIC;
|
|
|
|
|
mouse.usUsage = HID_USAGE_GENERIC_MOUSE;
|
|
|
|
|
if (wantSnoopingMouse)
|
|
|
|
|
{
|
|
|
|
|
mouse.dwFlags = RIDEV_INPUTSINK;
|
|
|
|
|
mouse.hwndTarget = m_hwnd;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
mouse.dwFlags = RIDEV_REMOVE;
|
|
|
|
|
mouse.hwndTarget = nullptr;
|
|
|
|
|
}
|
|
|
|
|
RegisterRawInputDevices(&mouse, 1, sizeof(mouse));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-02-14 18:22:05 +00:00
|
|
|
template<typename D>
|
|
|
|
|
bool SuperSonar<D>::IsForegroundAppExcluded()
|
|
|
|
|
{
|
|
|
|
|
if (m_excludedApps.size() < 1)
|
|
|
|
|
{
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
if (HWND foregroundApp{ GetForegroundWindow() })
|
|
|
|
|
{
|
|
|
|
|
auto processPath = get_process_path(foregroundApp);
|
2023-02-08 11:01:35 +00:00
|
|
|
CharUpperBuffW(processPath.data(), static_cast<DWORD>(processPath.length()));
|
2023-06-23 22:53:15 +03:00
|
|
|
|
|
|
|
|
return check_excluded_app(foregroundApp, processPath, m_excludedApps);
|
2022-02-14 18:22:05 +00:00
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-10-22 13:30:18 +01:00
|
|
|
struct CompositionSpotlight : SuperSonar<CompositionSpotlight>
|
|
|
|
|
{
|
|
|
|
|
static constexpr UINT WM_OPACITY_ANIMATION_COMPLETED = WM_APP;
|
2021-11-23 16:52:17 +00:00
|
|
|
float m_sonarRadiusFloat = static_cast<float>(m_sonarRadius);
|
2021-10-22 13:30:18 +01:00
|
|
|
|
|
|
|
|
DWORD GetExtendedStyle()
|
|
|
|
|
{
|
2025-09-26 13:03:00 +08:00
|
|
|
// Remove WS_EX_NOREDIRECTIONBITMAP for Composition/XAML to allow DWM redirection.
|
|
|
|
|
return 0;
|
2021-10-22 13:30:18 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void AfterMoveSonar()
|
|
|
|
|
{
|
2025-09-26 13:03:00 +08:00
|
|
|
const float scale = static_cast<float>(m_surface.XamlRoot().RasterizationScale());
|
|
|
|
|
// Move gradient center
|
|
|
|
|
if (m_spotlightMaskGradient)
|
|
|
|
|
{
|
|
|
|
|
m_spotlightMaskGradient.EllipseCenter({ static_cast<float>(m_sonarPos.x) / scale,
|
|
|
|
|
static_cast<float>(m_sonarPos.y) / scale });
|
|
|
|
|
}
|
|
|
|
|
// Move spotlight visual (color fill) below masked backdrop
|
|
|
|
|
if (m_spotlight)
|
|
|
|
|
{
|
|
|
|
|
m_spotlight.Offset({ static_cast<float>(m_sonarPos.x) / scale,
|
|
|
|
|
static_cast<float>(m_sonarPos.y) / scale,
|
|
|
|
|
0.0f });
|
|
|
|
|
}
|
2021-10-22 13:30:18 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
LRESULT WndProc(UINT message, WPARAM wParam, LPARAM lParam) noexcept
|
|
|
|
|
{
|
|
|
|
|
switch (message)
|
|
|
|
|
{
|
|
|
|
|
case WM_CREATE:
|
2025-09-26 13:03:00 +08:00
|
|
|
if (!OnCompositionCreate())
|
|
|
|
|
return -1;
|
|
|
|
|
return BaseWndProc(message, wParam, lParam);
|
2021-10-22 13:30:18 +01:00
|
|
|
|
|
|
|
|
case WM_OPACITY_ANIMATION_COMPLETED:
|
|
|
|
|
OnOpacityAnimationCompleted();
|
|
|
|
|
break;
|
2025-09-26 13:03:00 +08:00
|
|
|
case WM_SIZE:
|
|
|
|
|
UpdateIslandSize();
|
|
|
|
|
break;
|
2021-10-22 13:30:18 +01:00
|
|
|
}
|
|
|
|
|
return BaseWndProc(message, wParam, lParam);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void SetSonarVisibility(bool visible)
|
|
|
|
|
{
|
2025-09-26 13:03:00 +08:00
|
|
|
m_batch = m_compositor.GetCommitBatch(muxc::CompositionBatchTypes::Animation);
|
2023-09-05 15:25:24 +02:00
|
|
|
BOOL isEnabledAnimations = GetAnimationsEnabled();
|
|
|
|
|
m_animation.Duration(std::chrono::milliseconds{ isEnabledAnimations ? m_fadeDuration : 1 });
|
2021-10-22 13:30:18 +01:00
|
|
|
m_batch.Completed([hwnd = m_hwnd](auto&&, auto&&) {
|
|
|
|
|
PostMessage(hwnd, WM_OPACITY_ANIMATION_COMPLETED, 0, 0);
|
|
|
|
|
});
|
2025-09-26 13:03:00 +08:00
|
|
|
m_root.Opacity(visible ? 1.0f : 0.0f);
|
2021-10-22 13:30:18 +01:00
|
|
|
if (visible)
|
|
|
|
|
{
|
|
|
|
|
ShowWindow(m_hwnd, SW_SHOWNOACTIVATE);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-04 17:58:37 +02:00
|
|
|
HWND GetHwnd() noexcept
|
|
|
|
|
{
|
|
|
|
|
return m_hwnd;
|
|
|
|
|
}
|
|
|
|
|
|
2021-10-22 13:30:18 +01:00
|
|
|
private:
|
|
|
|
|
bool OnCompositionCreate()
|
|
|
|
|
try
|
|
|
|
|
{
|
2025-09-26 13:03:00 +08:00
|
|
|
// Creating composition resources
|
|
|
|
|
// Ensure a DispatcherQueue bound to this thread (required by WinAppSDK composition/XAML)
|
|
|
|
|
if (!m_dispatcherQueueController)
|
|
|
|
|
{
|
|
|
|
|
// Ensure COM is initialized
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
winrt::init_apartment(winrt::apartment_type::single_threaded);
|
|
|
|
|
// COM STA initialized
|
|
|
|
|
}
|
|
|
|
|
catch (const winrt::hresult_error& e)
|
|
|
|
|
{
|
|
|
|
|
Logger::error("Failed to initialize COM apartment: {}", winrt::to_string(e.message()));
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
m_dispatcherQueueController =
|
|
|
|
|
winrt::Microsoft::UI::Dispatching::DispatcherQueueController::CreateOnCurrentThread();
|
|
|
|
|
// DispatcherQueueController created
|
|
|
|
|
}
|
|
|
|
|
catch (const winrt::hresult_error& e)
|
|
|
|
|
{
|
|
|
|
|
Logger::error("Failed to create DispatcherQueueController: {}", winrt::to_string(e.message()));
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 1) Create a XAML island and attach it to this HWND
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
m_island = winrt::Microsoft::UI::Xaml::Hosting::DesktopWindowXamlSource{};
|
|
|
|
|
auto windowId = winrt::Microsoft::UI::GetWindowIdFromWindow(m_hwnd);
|
|
|
|
|
m_island.Initialize(windowId);
|
|
|
|
|
// Xaml source initialized
|
|
|
|
|
}
|
|
|
|
|
catch (const winrt::hresult_error& e)
|
|
|
|
|
{
|
|
|
|
|
Logger::error("Failed to create XAML island: {}", winrt::to_string(e.message()));
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
UpdateIslandSize();
|
|
|
|
|
// Island size set
|
|
|
|
|
|
|
|
|
|
// 2) Create a XAML container to host the Composition child visual
|
|
|
|
|
m_surface = winrt::Microsoft::UI::Xaml::Controls::Grid{};
|
|
|
|
|
|
|
|
|
|
// A transparent background keeps hit-testing consistent vs. null brush
|
|
|
|
|
m_surface.Background(winrt::Microsoft::UI::Xaml::Media::SolidColorBrush{
|
|
|
|
|
winrt::Microsoft::UI::Colors::Transparent() });
|
|
|
|
|
m_surface.HorizontalAlignment(muxx::HorizontalAlignment::Stretch);
|
|
|
|
|
m_surface.VerticalAlignment(muxx::VerticalAlignment::Stretch);
|
|
|
|
|
|
|
|
|
|
m_island.Content(m_surface);
|
|
|
|
|
|
|
|
|
|
// 3) Get the compositor from the XAML visual tree (pure MUXC path)
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
auto elementVisual =
|
|
|
|
|
winrt::Microsoft::UI::Xaml::Hosting::ElementCompositionPreview::GetElementVisual(m_surface);
|
|
|
|
|
m_compositor = elementVisual.Compositor();
|
|
|
|
|
// Compositor acquired
|
|
|
|
|
}
|
|
|
|
|
catch (const winrt::hresult_error& e)
|
|
|
|
|
{
|
|
|
|
|
Logger::error("Failed to get compositor: {}", winrt::to_string(e.message()));
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 4) Build the composition tree
|
2021-10-22 13:30:18 +01:00
|
|
|
//
|
2025-09-26 13:03:00 +08:00
|
|
|
// [root] ContainerVisual (fills host)
|
|
|
|
|
// \ LayerVisual
|
|
|
|
|
// \ [backdrop dim * radial gradient mask (hole)]
|
2021-10-22 13:30:18 +01:00
|
|
|
m_root = m_compositor.CreateContainerVisual();
|
2025-09-26 13:03:00 +08:00
|
|
|
m_root.RelativeSizeAdjustment({ 1.0f, 1.0f });
|
2021-10-22 13:30:18 +01:00
|
|
|
m_root.Opacity(0.0f);
|
2025-09-26 13:03:00 +08:00
|
|
|
|
|
|
|
|
// Insert our root as a hand-in Visual under the XAML element
|
|
|
|
|
winrt::Microsoft::UI::Xaml::Hosting::ElementCompositionPreview::SetElementChildVisual(m_surface, m_root);
|
2021-10-22 13:30:18 +01:00
|
|
|
|
|
|
|
|
auto layer = m_compositor.CreateLayerVisual();
|
2025-09-26 13:03:00 +08:00
|
|
|
layer.RelativeSizeAdjustment({ 1.0f, 1.0f });
|
2021-10-22 13:30:18 +01:00
|
|
|
m_root.Children().InsertAtTop(layer);
|
|
|
|
|
|
2025-09-26 13:03:00 +08:00
|
|
|
const float scale = static_cast<float>(m_surface.XamlRoot().RasterizationScale());
|
|
|
|
|
const float rDip = m_sonarRadiusFloat / scale;
|
|
|
|
|
const float zoom = static_cast<float>(m_sonarZoomFactor);
|
2021-10-22 13:30:18 +01:00
|
|
|
|
2025-09-26 13:03:00 +08:00
|
|
|
// Spotlight shape (below backdrop, visible through hole)
|
|
|
|
|
m_circleGeometry = m_compositor.CreateEllipseGeometry();
|
2021-11-23 16:52:17 +00:00
|
|
|
m_circleShape = m_compositor.CreateSpriteShape(m_circleGeometry);
|
|
|
|
|
m_circleShape.FillBrush(m_compositor.CreateColorBrush(m_spotlightColor));
|
2025-09-26 13:03:00 +08:00
|
|
|
m_circleShape.Offset({ rDip * zoom, rDip * zoom });
|
2021-10-22 13:30:18 +01:00
|
|
|
m_spotlight = m_compositor.CreateShapeVisual();
|
2025-09-26 13:03:00 +08:00
|
|
|
m_spotlight.Size({ rDip * 2 * zoom, rDip * 2 * zoom });
|
2021-10-22 13:30:18 +01:00
|
|
|
m_spotlight.AnchorPoint({ 0.5f, 0.5f });
|
2021-11-23 16:52:17 +00:00
|
|
|
m_spotlight.Shapes().Append(m_circleShape);
|
2021-10-22 13:30:18 +01:00
|
|
|
layer.Children().InsertAtTop(m_spotlight);
|
|
|
|
|
|
2025-09-26 13:03:00 +08:00
|
|
|
// Dim color (source)
|
|
|
|
|
m_dimColorBrush = m_compositor.CreateColorBrush(m_backgroundColor);
|
|
|
|
|
// Radial gradient mask (center transparent, outer opaque)
|
2026-01-05 22:41:01 +08:00
|
|
|
// Fixed feather width: 1px for radius < 300, 2px for radius >= 300
|
|
|
|
|
const float featherPixels = (m_sonarRadius >= 300) ? 2.0f : 1.0f;
|
|
|
|
|
const float featherOffset = 1.0f - featherPixels / (rDip * zoom);
|
2025-09-26 13:03:00 +08:00
|
|
|
m_spotlightMaskGradient = m_compositor.CreateRadialGradientBrush();
|
|
|
|
|
m_spotlightMaskGradient.MappingMode(muxc::CompositionMappingMode::Absolute);
|
|
|
|
|
m_maskStopCenter = m_compositor.CreateColorGradientStop();
|
|
|
|
|
m_maskStopCenter.Offset(0.0f);
|
|
|
|
|
m_maskStopCenter.Color(winrt::Windows::UI::ColorHelper::FromArgb(0, 0, 0, 0));
|
|
|
|
|
m_maskStopInner = m_compositor.CreateColorGradientStop();
|
2026-01-05 22:41:01 +08:00
|
|
|
m_maskStopInner.Offset(featherOffset);
|
2025-09-26 13:03:00 +08:00
|
|
|
m_maskStopInner.Color(winrt::Windows::UI::ColorHelper::FromArgb(0, 0, 0, 0));
|
|
|
|
|
m_maskStopOuter = m_compositor.CreateColorGradientStop();
|
|
|
|
|
m_maskStopOuter.Offset(1.0f);
|
|
|
|
|
m_maskStopOuter.Color(winrt::Windows::UI::ColorHelper::FromArgb(255, 255, 255, 255));
|
|
|
|
|
m_spotlightMaskGradient.ColorStops().Append(m_maskStopCenter);
|
|
|
|
|
m_spotlightMaskGradient.ColorStops().Append(m_maskStopInner);
|
|
|
|
|
m_spotlightMaskGradient.ColorStops().Append(m_maskStopOuter);
|
|
|
|
|
m_spotlightMaskGradient.EllipseCenter({ rDip * zoom, rDip * zoom });
|
|
|
|
|
m_spotlightMaskGradient.EllipseRadius({ rDip * zoom, rDip * zoom });
|
|
|
|
|
|
|
|
|
|
m_maskBrush = m_compositor.CreateMaskBrush();
|
|
|
|
|
m_maskBrush.Source(m_dimColorBrush);
|
|
|
|
|
m_maskBrush.Mask(m_spotlightMaskGradient);
|
|
|
|
|
|
|
|
|
|
m_backdrop = m_compositor.CreateSpriteVisual();
|
|
|
|
|
m_backdrop.RelativeSizeAdjustment({ 1.0f, 1.0f });
|
|
|
|
|
m_backdrop.Brush(m_maskBrush);
|
|
|
|
|
layer.Children().InsertAtTop(m_backdrop);
|
|
|
|
|
|
|
|
|
|
// 5) Implicit opacity animation on the root
|
2021-11-23 16:52:17 +00:00
|
|
|
m_animation = m_compositor.CreateScalarKeyFrameAnimation();
|
|
|
|
|
m_animation.Target(L"Opacity");
|
|
|
|
|
m_animation.InsertExpressionKeyFrame(1.0f, L"this.FinalValue");
|
|
|
|
|
m_animation.Duration(std::chrono::milliseconds{ m_fadeDuration });
|
2021-10-22 13:30:18 +01:00
|
|
|
auto collection = m_compositor.CreateImplicitAnimationCollection();
|
2021-11-23 16:52:17 +00:00
|
|
|
collection.Insert(L"Opacity", m_animation);
|
2021-10-22 13:30:18 +01:00
|
|
|
m_root.ImplicitAnimations(collection);
|
|
|
|
|
|
2025-09-26 13:03:00 +08:00
|
|
|
// 6) Spotlight radius shrinks as opacity increases (expression animation)
|
2026-01-05 22:41:01 +08:00
|
|
|
SetupRadiusAnimations(rDip * zoom, rDip, featherPixels);
|
2021-10-22 13:30:18 +01:00
|
|
|
|
2025-09-26 13:03:00 +08:00
|
|
|
// Composition created successfully
|
2021-10-22 13:30:18 +01:00
|
|
|
return true;
|
|
|
|
|
}
|
2025-09-26 13:03:00 +08:00
|
|
|
catch (const winrt::hresult_error& e)
|
2021-10-22 13:30:18 +01:00
|
|
|
{
|
2025-09-26 13:03:00 +08:00
|
|
|
Logger::error("Failed to create FindMyMouse visual: {}", winrt::to_string(e.message()));
|
2021-10-22 13:30:18 +01:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void OnOpacityAnimationCompleted()
|
|
|
|
|
{
|
|
|
|
|
if (m_root.Opacity() < 0.01f)
|
|
|
|
|
{
|
|
|
|
|
ShowWindow(m_hwnd, SW_HIDE);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-05 22:41:01 +08:00
|
|
|
// Helper to setup radius and feather expression animations
|
|
|
|
|
void SetupRadiusAnimations(float startRadiusDip, float endRadiusDip, float featherPixels)
|
|
|
|
|
{
|
|
|
|
|
// Radius expression: shrinks from startRadiusDip to endRadiusDip as opacity goes 0->1
|
|
|
|
|
auto radiusExpression = m_compositor.CreateExpressionAnimation();
|
|
|
|
|
radiusExpression.SetReferenceParameter(L"Root", m_root);
|
|
|
|
|
wchar_t expressionText[256];
|
|
|
|
|
winrt::check_hresult(StringCchPrintfW(
|
|
|
|
|
expressionText, ARRAYSIZE(expressionText),
|
|
|
|
|
L"Lerp(Vector2(%.1f, %.1f), Vector2(%.1f, %.1f), Root.Opacity)",
|
|
|
|
|
startRadiusDip, startRadiusDip, endRadiusDip, endRadiusDip));
|
|
|
|
|
radiusExpression.Expression(expressionText);
|
|
|
|
|
m_spotlightMaskGradient.StartAnimation(L"EllipseRadius", radiusExpression);
|
|
|
|
|
|
|
|
|
|
// Feather expression: maintains fixed pixel width as radius changes
|
|
|
|
|
auto featherExpression = m_compositor.CreateExpressionAnimation();
|
|
|
|
|
featherExpression.SetReferenceParameter(L"Root", m_root);
|
|
|
|
|
wchar_t featherExpressionText[256];
|
|
|
|
|
winrt::check_hresult(StringCchPrintfW(
|
|
|
|
|
featherExpressionText, ARRAYSIZE(featherExpressionText),
|
|
|
|
|
L"1.0f - %.1ff / Lerp(%.1ff, %.1ff, Root.Opacity)",
|
|
|
|
|
featherPixels, startRadiusDip, endRadiusDip));
|
|
|
|
|
featherExpression.Expression(featherExpressionText);
|
|
|
|
|
m_maskStopInner.StartAnimation(L"Offset", featherExpression);
|
|
|
|
|
|
|
|
|
|
// Circle geometry radius for visual consistency
|
|
|
|
|
if (m_circleGeometry)
|
|
|
|
|
{
|
|
|
|
|
auto radiusExpression2 = m_compositor.CreateExpressionAnimation();
|
|
|
|
|
radiusExpression2.SetReferenceParameter(L"Root", m_root);
|
|
|
|
|
radiusExpression2.Expression(expressionText);
|
|
|
|
|
m_circleGeometry.StartAnimation(L"Radius", radiusExpression2);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-26 13:03:00 +08:00
|
|
|
void UpdateIslandSize()
|
|
|
|
|
{
|
|
|
|
|
if (!m_island)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
RECT rc{};
|
|
|
|
|
if (!GetClientRect(m_hwnd, &rc))
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
const int width = rc.right - rc.left;
|
|
|
|
|
const int height = rc.bottom - rc.top;
|
|
|
|
|
|
|
|
|
|
auto bridge = m_island.SiteBridge();
|
|
|
|
|
bridge.MoveAndResize(winrt::Windows::Graphics::RectInt32{ 0, 0, width, height });
|
|
|
|
|
}
|
|
|
|
|
|
2021-11-23 16:52:17 +00:00
|
|
|
public:
|
2025-09-26 13:03:00 +08:00
|
|
|
void ApplySettings(const FindMyMouseSettings& settings, bool applyToRuntimeObjects)
|
|
|
|
|
{
|
2021-11-23 16:52:17 +00:00
|
|
|
if (!applyToRuntimeObjects)
|
|
|
|
|
{
|
|
|
|
|
m_sonarRadius = settings.spotlightRadius;
|
|
|
|
|
m_sonarRadiusFloat = static_cast<float>(m_sonarRadius);
|
|
|
|
|
m_backgroundColor = settings.backgroundColor;
|
|
|
|
|
m_spotlightColor = settings.spotlightColor;
|
2022-02-11 22:52:57 +00:00
|
|
|
m_activationMethod = settings.activationMethod;
|
2024-06-03 07:44:11 -04:00
|
|
|
m_includeWinKey = settings.includeWinKey;
|
2021-11-23 16:52:17 +00:00
|
|
|
m_doNotActivateOnGameMode = settings.doNotActivateOnGameMode;
|
|
|
|
|
m_fadeDuration = settings.animationDurationMs > 0 ? settings.animationDurationMs : 1;
|
|
|
|
|
m_sonarZoomFactor = settings.spotlightInitialZoom;
|
2022-02-14 18:22:05 +00:00
|
|
|
m_excludedApps = settings.excludedApps;
|
2022-03-04 12:28:11 +00:00
|
|
|
m_shakeMinimumDistance = settings.shakeMinimumDistance;
|
2024-01-16 17:35:54 +00:00
|
|
|
m_shakeIntervalMs = settings.shakeIntervalMs;
|
|
|
|
|
m_shakeFactor = settings.shakeFactor;
|
2021-11-23 16:52:17 +00:00
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2022-02-02 11:06:32 +00:00
|
|
|
if (m_dispatcherQueueController == nullptr)
|
|
|
|
|
{
|
|
|
|
|
Logger::warn("Tried accessing the dispatch queue controller before it was initialized.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
2021-11-23 16:52:17 +00:00
|
|
|
auto dispatcherQueue = m_dispatcherQueueController.DispatcherQueue();
|
|
|
|
|
FindMyMouseSettings localSettings = settings;
|
|
|
|
|
bool enqueueSucceeded = dispatcherQueue.TryEnqueue([=]() {
|
|
|
|
|
if (!m_destroyed)
|
|
|
|
|
{
|
|
|
|
|
m_sonarRadius = localSettings.spotlightRadius;
|
|
|
|
|
m_sonarRadiusFloat = static_cast<float>(m_sonarRadius);
|
|
|
|
|
m_backgroundColor = localSettings.backgroundColor;
|
|
|
|
|
m_spotlightColor = localSettings.spotlightColor;
|
2022-02-14 18:22:05 +00:00
|
|
|
m_activationMethod = localSettings.activationMethod;
|
2024-06-03 07:44:11 -04:00
|
|
|
m_includeWinKey = localSettings.includeWinKey;
|
2021-11-23 16:52:17 +00:00
|
|
|
m_doNotActivateOnGameMode = localSettings.doNotActivateOnGameMode;
|
|
|
|
|
m_fadeDuration = localSettings.animationDurationMs > 0 ? localSettings.animationDurationMs : 1;
|
|
|
|
|
m_sonarZoomFactor = localSettings.spotlightInitialZoom;
|
2022-02-14 18:22:05 +00:00
|
|
|
m_excludedApps = localSettings.excludedApps;
|
2022-03-04 12:28:11 +00:00
|
|
|
m_shakeMinimumDistance = localSettings.shakeMinimumDistance;
|
2024-01-16 17:35:54 +00:00
|
|
|
m_shakeIntervalMs = localSettings.shakeIntervalMs;
|
|
|
|
|
m_shakeFactor = localSettings.shakeFactor;
|
2022-02-11 22:52:57 +00:00
|
|
|
UpdateMouseSnooping(); // For the shake mouse activation method
|
|
|
|
|
|
2021-11-23 16:52:17 +00:00
|
|
|
// Apply new settings to runtime composition objects.
|
2025-09-26 13:03:00 +08:00
|
|
|
if (m_dimColorBrush)
|
|
|
|
|
{
|
|
|
|
|
m_dimColorBrush.Color(m_backgroundColor);
|
|
|
|
|
}
|
|
|
|
|
if (m_circleShape)
|
|
|
|
|
{
|
|
|
|
|
if (auto brush = m_circleShape.FillBrush().try_as<muxc::CompositionColorBrush>())
|
|
|
|
|
{
|
|
|
|
|
brush.Color(m_spotlightColor);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const float scale = static_cast<float>(m_surface.XamlRoot().RasterizationScale());
|
|
|
|
|
const float rDip = m_sonarRadiusFloat / scale;
|
|
|
|
|
const float zoom = static_cast<float>(m_sonarZoomFactor);
|
2026-01-05 22:41:01 +08:00
|
|
|
const float featherPixels = (m_sonarRadius >= 300) ? 2.0f : 1.0f;
|
|
|
|
|
const float startRadiusDip = rDip * zoom;
|
2025-09-26 13:03:00 +08:00
|
|
|
m_spotlightMaskGradient.StopAnimation(L"EllipseRadius");
|
2026-01-05 22:41:01 +08:00
|
|
|
m_maskStopInner.StopAnimation(L"Offset");
|
2025-09-26 13:03:00 +08:00
|
|
|
if (m_circleGeometry)
|
|
|
|
|
{
|
|
|
|
|
m_circleGeometry.StopAnimation(L"Radius");
|
|
|
|
|
}
|
2026-01-05 22:41:01 +08:00
|
|
|
m_spotlightMaskGradient.EllipseCenter({ startRadiusDip, startRadiusDip });
|
|
|
|
|
if (m_spotlight)
|
|
|
|
|
{
|
|
|
|
|
m_spotlight.Size({ rDip * 2 * zoom, rDip * 2 * zoom });
|
|
|
|
|
m_circleShape.Offset({ startRadiusDip, startRadiusDip });
|
|
|
|
|
}
|
|
|
|
|
SetupRadiusAnimations(startRadiusDip, rDip, featherPixels);
|
2021-11-23 16:52:17 +00:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
if (!enqueueSucceeded)
|
|
|
|
|
{
|
|
|
|
|
Logger::error("Couldn't enqueue message to update the sonar settings.");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-10-22 13:30:18 +01:00
|
|
|
private:
|
2025-09-26 13:03:00 +08:00
|
|
|
muxc::Compositor m_compositor{ nullptr };
|
|
|
|
|
muxxh::DesktopWindowXamlSource m_island{ nullptr };
|
|
|
|
|
muxxc::Grid m_surface{ nullptr };
|
|
|
|
|
|
|
|
|
|
muxc::ContainerVisual m_root{ nullptr };
|
|
|
|
|
muxc::CompositionCommitBatch m_batch{ nullptr };
|
|
|
|
|
muxc::SpriteVisual m_backdrop{ nullptr };
|
|
|
|
|
// Spotlight shape visuals
|
|
|
|
|
muxc::CompositionEllipseGeometry m_circleGeometry{ nullptr };
|
|
|
|
|
muxc::ShapeVisual m_spotlight{ nullptr };
|
|
|
|
|
muxc::CompositionSpriteShape m_circleShape{ nullptr };
|
|
|
|
|
// Radial gradient mask components
|
|
|
|
|
muxc::CompositionMaskBrush m_maskBrush{ nullptr };
|
|
|
|
|
muxc::CompositionColorBrush m_dimColorBrush{ nullptr };
|
|
|
|
|
muxc::CompositionRadialGradientBrush m_spotlightMaskGradient{ nullptr };
|
|
|
|
|
muxc::CompositionColorGradientStop m_maskStopCenter{ nullptr };
|
|
|
|
|
muxc::CompositionColorGradientStop m_maskStopInner{ nullptr };
|
|
|
|
|
muxc::CompositionColorGradientStop m_maskStopOuter{ nullptr };
|
2021-11-23 16:52:17 +00:00
|
|
|
winrt::Windows::UI::Color m_backgroundColor = FIND_MY_MOUSE_DEFAULT_BACKGROUND_COLOR;
|
|
|
|
|
winrt::Windows::UI::Color m_spotlightColor = FIND_MY_MOUSE_DEFAULT_SPOTLIGHT_COLOR;
|
2025-09-26 13:03:00 +08:00
|
|
|
muxc::ScalarKeyFrameAnimation m_animation{ nullptr };
|
2021-10-22 13:30:18 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
#pragma endregion Super_Sonar_Base_Code
|
|
|
|
|
|
|
|
|
|
#pragma region Super_Sonar_API
|
|
|
|
|
|
|
|
|
|
CompositionSpotlight* m_sonar = nullptr;
|
2021-11-23 16:52:17 +00:00
|
|
|
void FindMyMouseApplySettings(const FindMyMouseSettings& settings)
|
|
|
|
|
{
|
|
|
|
|
if (m_sonar != nullptr)
|
|
|
|
|
{
|
|
|
|
|
m_sonar->ApplySettings(settings, true);
|
|
|
|
|
}
|
|
|
|
|
}
|
2021-10-22 13:30:18 +01:00
|
|
|
|
|
|
|
|
void FindMyMouseDisable()
|
|
|
|
|
{
|
|
|
|
|
if (m_sonar != nullptr)
|
|
|
|
|
{
|
|
|
|
|
m_sonar->Terminate();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool FindMyMouseIsEnabled()
|
|
|
|
|
{
|
|
|
|
|
return (m_sonar != nullptr);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Based on SuperSonar's original wWinMain.
|
2021-11-23 16:52:17 +00:00
|
|
|
int FindMyMouseMain(HINSTANCE hinst, const FindMyMouseSettings& settings)
|
2021-10-22 13:30:18 +01:00
|
|
|
{
|
|
|
|
|
if (m_sonar != nullptr)
|
|
|
|
|
{
|
|
|
|
|
Logger::error("A sonar instance was still working when trying to start a new one.");
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
CompositionSpotlight sonar;
|
2021-11-23 16:52:17 +00:00
|
|
|
sonar.ApplySettings(settings, false);
|
2021-10-22 13:30:18 +01:00
|
|
|
if (!sonar.Initialize(hinst))
|
|
|
|
|
{
|
|
|
|
|
Logger::error("Couldn't initialize a sonar instance.");
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
m_sonar = &sonar;
|
|
|
|
|
|
2023-09-04 17:58:37 +02:00
|
|
|
InitializeWinhookEventIds();
|
|
|
|
|
|
2021-10-22 13:30:18 +01:00
|
|
|
MSG msg;
|
|
|
|
|
|
|
|
|
|
// Main message loop:
|
|
|
|
|
while (GetMessage(&msg, nullptr, 0, 0))
|
|
|
|
|
{
|
|
|
|
|
TranslateMessage(&msg);
|
|
|
|
|
DispatchMessage(&msg);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m_sonar = nullptr;
|
|
|
|
|
|
|
|
|
|
return (int)msg.wParam;
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-04 17:58:37 +02:00
|
|
|
HWND GetSonarHwnd() noexcept
|
|
|
|
|
{
|
|
|
|
|
if (m_sonar != nullptr)
|
|
|
|
|
{
|
|
|
|
|
return m_sonar->GetHwnd();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-05 22:41:01 +08:00
|
|
|
#pragma endregion Super_Sonar_API
|