Skip to content

Commit

Permalink
[FindMyMouse] Add setting to activate by shaking mouse (#16244)
Browse files Browse the repository at this point in the history
* [FindMyMouse]Initial shaking activation implementation

* Add setting to change activation method

* Update Mouse Snooping on settings change

* fix spellchecker

* Place activation method setting outside the expander

* Address PR Comments
  • Loading branch information
jaimecbernardo authored Feb 11, 2022
1 parent f0d084c commit f6a292d
Show file tree
Hide file tree
Showing 8 changed files with 205 additions and 22 deletions.
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;
}
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

0 comments on commit f6a292d

Please sign in to comment.