Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FindMyMouse] Add setting to activate by shaking mouse #16244

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/actions/spell-check/expect.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1126,6 +1126,7 @@ logon
LOGPIXELSX
LOn
longdate
LONGLONG
lookbehind
lowlevel
LOWORD
Expand Down
155 changes: 135 additions & 20 deletions src/modules/MouseUtils/FindMyMouse/FindMyMouse.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include "FindMyMouse.h"
#include "trace.h"
#include "common/utils/game_mode.h"
#include <vector>

#ifdef COMPOSITION
namespace winrt
Expand Down Expand Up @@ -43,6 +44,7 @@ struct SuperSonar
void BeforeMoveSonar() {}
void AfterMoveSonar() {}
void SetSonarVisibility(bool visible) = delete;
void UpdateMouseSnooping();

protected:
// Base class members you can access.
Expand All @@ -57,7 +59,8 @@ struct SuperSonar
static const int MIN_DOUBLE_CLICK_TIME = 100;

bool m_destroyed = false;
bool m_doNotActivateOnGameMode = true;
FindMyMouseActivationMethod m_activationMethod = FIND_MY_MOUSE_DEFAULT_ACTIVATION_METHOD;
bool m_doNotActivateOnGameMode = FIND_MY_MOUSE_DEFAULT_DO_NOT_ACTIVATE_ON_GAME_MODE;
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;
Expand All @@ -66,13 +69,37 @@ struct SuperSonar
winrt::DispatcherQueueController m_dispatcherQueueController{ nullptr };

private:

// 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 };
// Don't consider movements started past these milliseconds to detect shaking.
static constexpr LONG ShakeIntervalMs = 1000;
// By which factor must travelled distance be than the diagonal of the rectangle containing the movements.
static constexpr float ShakeFactor = 4.0f;

static inline byte GetSign(LONG const& num)
{
if (num > 0)
return 1;
if (num < 0)
return -1;
return 0;
}

static bool IsEqual(POINT const& p1, POINT const& p2)
{
return p1.x == p2.x && p1.y == p2.y;
}

static constexpr POINT ptNowhere = { -1, -1 };

static constexpr DWORD TIMER_ID_TRACK = 100;
static constexpr DWORD IdlePeriod = 1000;

Expand All @@ -89,11 +116,11 @@ struct SuperSonar
HWND m_hwndOwner;
SonarState m_sonarState = SonarState::Idle;
POINT m_lastKeyPos{};
DWORD m_lastKeyTime{};
ULONGLONG m_lastKeyTime{};

static constexpr DWORD NoSonar = 0;
static constexpr DWORD SonarWaitingForMouseMove = 1;
DWORD m_sonarStart = NoSonar;
ULONGLONG m_sonarStart = NoSonar;
bool m_isSnoopingMouse = false;

private:
Expand All @@ -110,10 +137,10 @@ struct SuperSonar
void OnSonarMouseInput(RAWINPUT const& input);
void OnMouseTimer();

void DetectShake();

void StartSonar();
void StopSonar();

void UpdateMouseSnooping();
};

template<typename D>
Expand Down Expand Up @@ -189,7 +216,9 @@ LRESULT SuperSonar<D>::BaseWndProc(UINT message, WPARAM wParam, LPARAM lParam) n
switch (message)
{
case WM_CREATE:
return OnSonarCreate() ? 0 : -1;
if(!OnSonarCreate()) return -1;
UpdateMouseSnooping();
return 0;

case WM_DESTROY:
OnSonarDestroy();
Expand Down Expand Up @@ -257,13 +286,7 @@ void SuperSonar<D>::OnSonarInput(WPARAM flags, HRAWINPUT hInput)
template<typename D>
void SuperSonar<D>::OnSonarKeyboardInput(RAWINPUT const& input)
{
// Don't activate if game mode is on.
if (m_doNotActivateOnGameMode && detect_game_mode())
{
return;
}

if (input.data.keyboard.VKey != VK_CONTROL)
if ( m_activationMethod != FindMyMouseActivationMethod::DoubleControlKey || input.data.keyboard.VKey != VK_CONTROL)
{
StopSonar();
return;
Expand Down Expand Up @@ -293,7 +316,7 @@ void SuperSonar<D>::OnSonarKeyboardInput(RAWINPUT const& input)
if (pressed)
{
m_sonarState = SonarState::ControlDown1;
m_lastKeyTime = GetTickCount();
m_lastKeyTime = GetTickCount64();
m_lastKeyPos = {};
GetCursorPos(&m_lastKeyPos);
UpdateMouseSnooping();
Expand All @@ -310,7 +333,7 @@ void SuperSonar<D>::OnSonarKeyboardInput(RAWINPUT const& input)
case SonarState::ControlUp1:
if (pressed)
{
auto now = GetTickCount();
auto now = GetTickCount64();
auto doubleClickInterval = now - m_lastKeyTime;
POINT ptCursor{};
auto doubleClickTimeSetting = GetDoubleClickTime();
Expand All @@ -325,7 +348,7 @@ void SuperSonar<D>::OnSonarKeyboardInput(RAWINPUT const& input)
else
{
m_sonarState = SonarState::ControlDown1;
m_lastKeyTime = GetTickCount();
m_lastKeyTime = GetTickCount64();
m_lastKeyPos = {};
GetCursorPos(&m_lastKeyPos);
UpdateMouseSnooping();
Expand All @@ -351,9 +374,92 @@ void SuperSonar<D>::OnSonarKeyboardInput(RAWINPUT const& input)
}
}

// 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()
{
ULONGLONG shakeStartTick = GetTickCount64() - ShakeIntervalMs;

// 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; });


double distanceTravelled = 0;
LONGLONG currentX=0, minX=0, maxX=0;
LONGLONG currentY=0, minY=0, maxY=0;

for (const PointerRecentMovement& movement : m_movementHistory)
{
currentX += movement.diff.x;
currentY += movement.diff.y;
distanceTravelled += sqrt((double)movement.diff.x * movement.diff.x + (double)movement.diff.y * movement.diff.y); // Pythagorean theorem
minX = min(currentX, minX);
maxX = max(currentX, maxX);
minY = min(currentY, minY);
maxY = max(currentY, maxY);
}

// Size of the rectangle the pointer moved in.
double rectangleWidth = (double)maxX - minX;
double rectangleHeight = (double)maxY - minY;

double diagonal = sqrt(rectangleWidth * rectangleWidth + rectangleHeight * rectangleHeight);
if (diagonal > 0 && distanceTravelled / diagonal > ShakeFactor)
{
m_movementHistory.clear();
StartSonar();
}

}

template<typename D>
void SuperSonar<D>::OnSonarMouseInput(RAWINPUT const& input)
{
if (m_activationMethod == FindMyMouseActivationMethod::ShakeMouse)
{
LONG relativeX = 0;
LONG relativeY = 0;
if ((input.data.mouse.usFlags & MOUSE_MOVE_ABSOLUTE) == MOUSE_MOVE_ABSOLUTE)
{
// 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;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should m_seenAnAbsoluteMousePosition be reset to false here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder. I don't know if we can receive absolute then relative then absolute. Trying to avoid cases where we might get absolute positions with small relative in between, then another absolute.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if we can receive absolute then relative then absolute

No idea tbh :) But, if I get this right, the if block should only make sense if you get 2 absolute in a row? If you don't reset this, then you can get absolute after some time on different side of the screen and relativeX & relativeY does not represent valid cases? This probably never happen though :D

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I'm not sure. I think even if we receive absolute + relative in a duplicated way it shouldn't be much of an issue 🤷 . It'll just trigger more? :P

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. We are probably overthinking this :D

}
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
{
m_movementHistory.push_back({ .diff = { .x=relativeX, .y=relativeY }, .tick = GetTickCount64() });
// Mouse movement changed directions. Take the opportunity do detect shake.
DetectShake();
}
}
else
{
m_movementHistory.push_back({ .diff = { .x = relativeX, .y = relativeY }, .tick = GetTickCount64() });
}

}

if (input.data.mouse.usButtonFlags)
{
StopSonar();
Expand All @@ -367,6 +473,12 @@ void SuperSonar<D>::OnSonarMouseInput(RAWINPUT const& input)
template<typename D>
void SuperSonar<D>::StartSonar()
{
// Don't activate if game mode is on.
if (m_doNotActivateOnGameMode && detect_game_mode())
{
return;
}

Logger::info("Focusing the sonar on the mouse cursor.");
Trace::MousePointerFocused();
// Cover the entire virtual screen.
Expand All @@ -393,7 +505,7 @@ void SuperSonar<D>::StopSonar()
template<typename D>
void SuperSonar<D>::OnMouseTimer()
{
auto now = GetTickCount();
auto now = GetTickCount64();

// If mouse has moved, then reset the sonar timer.
POINT ptCursor{};
Expand Down Expand Up @@ -433,7 +545,7 @@ void SuperSonar<D>::OnMouseTimer()
template<typename D>
void SuperSonar<D>::UpdateMouseSnooping()
{
bool wantSnoopingMouse = m_sonarStart != NoSonar || m_sonarState != SonarState::Idle;
bool wantSnoopingMouse = m_sonarStart != NoSonar || m_sonarState != SonarState::Idle || m_activationMethod == FindMyMouseActivationMethod::ShakeMouse;
if (m_isSnoopingMouse != wantSnoopingMouse)
{
m_isSnoopingMouse = wantSnoopingMouse;
Expand Down Expand Up @@ -590,6 +702,7 @@ struct CompositionSpotlight : SuperSonar<CompositionSpotlight>
m_sonarRadiusFloat = static_cast<float>(m_sonarRadius);
m_backgroundColor = settings.backgroundColor;
m_spotlightColor = settings.spotlightColor;
m_activationMethod = settings.activationMethod;
m_doNotActivateOnGameMode = settings.doNotActivateOnGameMode;
m_fadeDuration = settings.animationDurationMs > 0 ? settings.animationDurationMs : 1;
m_finalAlphaNumerator = settings.overlayOpacity;
Expand All @@ -614,11 +727,13 @@ struct CompositionSpotlight : SuperSonar<CompositionSpotlight>
m_sonarRadiusFloat = static_cast<float>(m_sonarRadius);
m_backgroundColor = localSettings.backgroundColor;
m_spotlightColor = localSettings.spotlightColor;
m_activationMethod = settings.activationMethod;
m_doNotActivateOnGameMode = localSettings.doNotActivateOnGameMode;
m_fadeDuration = localSettings.animationDurationMs > 0 ? localSettings.animationDurationMs : 1;
m_finalAlphaNumerator = localSettings.overlayOpacity;
m_sonarZoomFactor = localSettings.spotlightInitialZoom;

UpdateMouseSnooping(); // For the shake mouse activation method

// Apply new settings to runtime composition objects.
m_backdrop.Brush().as<winrt::CompositionColorBrush>().Color(m_backgroundColor);
m_circleShape.FillBrush().as<winrt::CompositionColorBrush>().Color(m_spotlightColor);
Expand Down
9 changes: 9 additions & 0 deletions src/modules/MouseUtils/FindMyMouse/FindMyMouse.h
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
#pragma once
#include "pch.h"

enum struct FindMyMouseActivationMethod : int
{
DoubleControlKey = 0,
ShakeMouse = 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;
const winrt::Windows::UI::Color FIND_MY_MOUSE_DEFAULT_BACKGROUND_COLOR = winrt::Windows::UI::ColorHelper::FromArgb(255, 0, 0, 0);
const winrt::Windows::UI::Color FIND_MY_MOUSE_DEFAULT_SPOTLIGHT_COLOR = winrt::Windows::UI::ColorHelper::FromArgb(255, 255, 255, 255);
constexpr int FIND_MY_MOUSE_DEFAULT_OVERLAY_OPACITY = 50;
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::DoubleControlKey;

struct FindMyMouseSettings
{
FindMyMouseActivationMethod activationMethod = FIND_MY_MOUSE_DEFAULT_ACTIVATION_METHOD;
bool doNotActivateOnGameMode = FIND_MY_MOUSE_DEFAULT_DO_NOT_ACTIVATE_ON_GAME_MODE;
winrt::Windows::UI::Color backgroundColor = FIND_MY_MOUSE_DEFAULT_BACKGROUND_COLOR;
winrt::Windows::UI::Color spotlightColor = FIND_MY_MOUSE_DEFAULT_SPOTLIGHT_COLOR;
Expand Down
15 changes: 15 additions & 0 deletions src/modules/MouseUtils/FindMyMouse/dllmain.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,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_DO_NOT_ACTIVATE_ON_GAME_MODE[] = L"do_not_activate_on_game_mode";
const wchar_t JSON_KEY_BACKGROUND_COLOR[] = L"background_color";
const wchar_t JSON_KEY_SPOTLIGHT_COLOR[] = L"spotlight_color";
Expand Down Expand Up @@ -171,6 +172,20 @@ void FindMyMouse::parse_settings(PowerToysSettings::PowerToyValues& settings)
FindMyMouseSettings findMyMouseSettings;
if (settingsObject.GetView().Size())
{
try
{
// Parse Activation Method
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_ACTIVATION_METHOD);
UINT value = (UINT)jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE);
if (value < (int)FindMyMouseActivationMethod::EnumElements)
{
findMyMouseSettings.activationMethod = (FindMyMouseActivationMethod)value;
}
}
catch (...)
{
Logger::warn("Failed to initialize Activation Method from settings. Will use default value");
}
try
{
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_DO_NOT_ACTIVATE_ON_GAME_MODE);
Expand Down
4 changes: 4 additions & 0 deletions src/settings-ui/Settings.UI.Library/FindMyMouseProperties.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ namespace Microsoft.PowerToys.Settings.UI.Library
{
public class FindMyMouseProperties
{
[JsonPropertyName("activation_method")]
public IntProperty ActivationMethod { get; set; }

[JsonPropertyName("do_not_activate_on_game_mode")]
public BoolProperty DoNotActivateOnGameMode { get; set; }

Expand All @@ -31,6 +34,7 @@ public class FindMyMouseProperties

public FindMyMouseProperties()
{
ActivationMethod = new IntProperty(0);
DoNotActivateOnGameMode = new BoolProperty(true);
BackgroundColor = new StringProperty("#000000");
SpotlightColor = new StringProperty("#FFFFFF");
Expand Down
Loading