From f6a292d47f7ffdf77b30bdd83ab777d720378c4b Mon Sep 17 00:00:00 2001 From: Jaime Bernardo Date: Fri, 11 Feb 2022 22:52:57 +0000 Subject: [PATCH] [FindMyMouse] Add setting to activate by shaking mouse (#16244) * [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 --- .github/actions/spell-check/expect.txt | 1 + .../MouseUtils/FindMyMouse/FindMyMouse.cpp | 155 +++++++++++++++--- .../MouseUtils/FindMyMouse/FindMyMouse.h | 9 + .../MouseUtils/FindMyMouse/dllmain.cpp | 15 ++ .../FindMyMouseProperties.cs | 4 + .../ViewModels/MouseUtilsViewModel.cs | 20 +++ .../Settings.UI/Strings/en-us/Resources.resw | 15 +- .../Settings.UI/Views/MouseUtilsPage.xaml | 8 + 8 files changed, 205 insertions(+), 22 deletions(-) diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index 44f2fa9bdc7f..7c02ed32b134 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -1126,6 +1126,7 @@ logon LOGPIXELSX LOn longdate +LONGLONG lookbehind lowlevel LOWORD diff --git a/src/modules/MouseUtils/FindMyMouse/FindMyMouse.cpp b/src/modules/MouseUtils/FindMyMouse/FindMyMouse.cpp index 0897ab7fad08..204d7e482de5 100644 --- a/src/modules/MouseUtils/FindMyMouse/FindMyMouse.cpp +++ b/src/modules/MouseUtils/FindMyMouse/FindMyMouse.cpp @@ -4,6 +4,7 @@ #include "FindMyMouse.h" #include "trace.h" #include "common/utils/game_mode.h" +#include #ifdef COMPOSITION namespace winrt @@ -43,6 +44,7 @@ struct SuperSonar void BeforeMoveSonar() {} void AfterMoveSonar() {} void SetSonarVisibility(bool visible) = delete; + void UpdateMouseSnooping(); protected: // Base class members you can access. @@ -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; @@ -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 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; @@ -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: @@ -110,10 +137,10 @@ struct SuperSonar void OnSonarMouseInput(RAWINPUT const& input); void OnMouseTimer(); + void DetectShake(); + void StartSonar(); void StopSonar(); - - void UpdateMouseSnooping(); }; template @@ -189,7 +216,9 @@ LRESULT SuperSonar::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(); @@ -257,13 +286,7 @@ void SuperSonar::OnSonarInput(WPARAM flags, HRAWINPUT hInput) template void SuperSonar::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; @@ -293,7 +316,7 @@ void SuperSonar::OnSonarKeyboardInput(RAWINPUT const& input) if (pressed) { m_sonarState = SonarState::ControlDown1; - m_lastKeyTime = GetTickCount(); + m_lastKeyTime = GetTickCount64(); m_lastKeyPos = {}; GetCursorPos(&m_lastKeyPos); UpdateMouseSnooping(); @@ -310,7 +333,7 @@ void SuperSonar::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(); @@ -325,7 +348,7 @@ void SuperSonar::OnSonarKeyboardInput(RAWINPUT const& input) else { m_sonarState = SonarState::ControlDown1; - m_lastKeyTime = GetTickCount(); + m_lastKeyTime = GetTickCount64(); m_lastKeyPos = {}; GetCursorPos(&m_lastKeyPos); UpdateMouseSnooping(); @@ -351,9 +374,92 @@ void SuperSonar::OnSonarKeyboardInput(RAWINPUT const& input) } } +// Shaking detection algorithm is: Has distance travelled been much greater than the diagonal of the rectangle containing the movement? +template +void SuperSonar::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 void SuperSonar::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(); @@ -367,6 +473,12 @@ void SuperSonar::OnSonarMouseInput(RAWINPUT const& input) template void SuperSonar::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. @@ -393,7 +505,7 @@ void SuperSonar::StopSonar() template void SuperSonar::OnMouseTimer() { - auto now = GetTickCount(); + auto now = GetTickCount64(); // If mouse has moved, then reset the sonar timer. POINT ptCursor{}; @@ -433,7 +545,7 @@ void SuperSonar::OnMouseTimer() template void SuperSonar::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; @@ -590,6 +702,7 @@ struct CompositionSpotlight : SuperSonar m_sonarRadiusFloat = static_cast(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; @@ -614,11 +727,13 @@ struct CompositionSpotlight : SuperSonar m_sonarRadiusFloat = static_cast(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().Color(m_backgroundColor); m_circleShape.FillBrush().as().Color(m_spotlightColor); diff --git a/src/modules/MouseUtils/FindMyMouse/FindMyMouse.h b/src/modules/MouseUtils/FindMyMouse/FindMyMouse.h index f5a81b019d54..d6e121ef777c 100644 --- a/src/modules/MouseUtils/FindMyMouse/FindMyMouse.h +++ b/src/modules/MouseUtils/FindMyMouse/FindMyMouse.h @@ -1,6 +1,13 @@ #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); @@ -8,9 +15,11 @@ 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; diff --git a/src/modules/MouseUtils/FindMyMouse/dllmain.cpp b/src/modules/MouseUtils/FindMyMouse/dllmain.cpp index f771764a53db..1e497a00b857 100644 --- a/src/modules/MouseUtils/FindMyMouse/dllmain.cpp +++ b/src/modules/MouseUtils/FindMyMouse/dllmain.cpp @@ -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"; @@ -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); diff --git a/src/settings-ui/Settings.UI.Library/FindMyMouseProperties.cs b/src/settings-ui/Settings.UI.Library/FindMyMouseProperties.cs index fb7eb158fc9e..c5cb5a790944 100644 --- a/src/settings-ui/Settings.UI.Library/FindMyMouseProperties.cs +++ b/src/settings-ui/Settings.UI.Library/FindMyMouseProperties.cs @@ -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; } @@ -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"); diff --git a/src/settings-ui/Settings.UI.Library/ViewModels/MouseUtilsViewModel.cs b/src/settings-ui/Settings.UI.Library/ViewModels/MouseUtilsViewModel.cs index e336777dd953..3572ff792528 100644 --- a/src/settings-ui/Settings.UI.Library/ViewModels/MouseUtilsViewModel.cs +++ b/src/settings-ui/Settings.UI.Library/ViewModels/MouseUtilsViewModel.cs @@ -47,6 +47,7 @@ public MouseUtilsViewModel(ISettingsUtils settingsUtils, ISettingsRepository SendConfigMSG { get; } private bool _isFindMyMouseEnabled; + private int _findMyMouseActivationMethod; private bool _findMyMouseDoNotActivateOnGameMode; private string _findMyMouseBackgroundColor; private string _findMyMouseSpotlightColor; diff --git a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw index e7cbc8a64cb6..3850220c746e 100644 --- a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw +++ b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw @@ -1648,7 +1648,7 @@ From there, simply click on a Markdown file, PDF file or SVG icon in the File Ex Mouse as in the hardware peripheral - Press the left Ctrl key twice to focus the mouse pointer. + Shake the mouse or press the left Ctrl key twice to focus the mouse pointer. Mouse as in the hardware peripheral. Key as in a keyboard key @@ -1757,13 +1757,24 @@ From there, simply click on a Markdown file, PDF file or SVG icon in the File Ex Refers to the utility name - Find My Mouse highlights the position of the cursor when pressing the left Ctrl key twice. + Find My Mouse highlights the position of the cursor when shaking the mouse or pressing the left Ctrl key twice. "Ctrl" is a keyboard key. "Find My Mouse" is the name of the utility Enable Find My Mouse "Find My Mouse" is the name of the utility. + + Activation method + + + Press Left Control twice + Left control is the physical key on the keyboard. + + + Shake mouse + Mouse is the hardware peripheral. + Do not activate when Game Mode is on "Game mode" is the Windows feature to prevent notification when playing a game. diff --git a/src/settings-ui/Settings.UI/Views/MouseUtilsPage.xaml b/src/settings-ui/Settings.UI/Views/MouseUtilsPage.xaml index adca0a0514f8..11c813a61c6d 100644 --- a/src/settings-ui/Settings.UI/Views/MouseUtilsPage.xaml +++ b/src/settings-ui/Settings.UI/Views/MouseUtilsPage.xaml @@ -22,6 +22,14 @@ + + + + + + + +