diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt
index d678404ba5..562ce51cf4 100644
--- a/.github/actions/spell-check/expect.txt
+++ b/.github/actions/spell-check/expect.txt
@@ -5,6 +5,7 @@ abcdef
abcdefgh
ABCDEFGHIJKLMNOPQRSTUVWXYZ
abgr
+abi
ABlocked
Abug
accctrl
@@ -382,6 +383,7 @@ CXVIRTUALSCREEN
cxx
cxxopts
CYSMICON
+CYVIRTUALSCREEN
cziplib
Dac
dacl
@@ -493,6 +495,8 @@ DPICHANGED
DPolicy
DPopup
DPSAPI
+DQTAT
+DQTYPE
DRAWFRAME
drawingcolor
dreamsofameaningfullife
@@ -788,6 +792,7 @@ hotspot
HPAINTBUFFER
hpj
hpp
+HRAWINPUT
hread
HREDRAW
href
@@ -807,6 +812,7 @@ Htmdid
html
htt
http
+HTTRANSPARENT
hwb
HWINEVENTHOOK
hwnd
@@ -840,6 +846,7 @@ ICollection
IColor
ICommand
IComparer
+ICompositor
ICONERROR
ICONINFORMATION
IContext
@@ -854,6 +861,7 @@ IDesktop
IDictionary
IDirectory
IDispatch
+IDispatcher
IDisposable
idl
IDLIST
@@ -941,6 +949,7 @@ INPUTHARDWARE
INPUTKEYBOARD
INPUTLANGCHANGED
INPUTMOUSE
+INPUTSINK
INPUTTYPE
INSTALLDESKTOPSHORTCUT
INSTALLDIR
@@ -1702,6 +1711,9 @@ Radiobuttons
RAII
RAlt
randyrants
+RAWINPUT
+RAWINPUTDEVICE
+RAWINPUTHEADER
RAWPATH
rbegin
Rbp
@@ -1786,6 +1798,7 @@ rgs
rhs
ricardosantos
Richtext
+RIDEV
RIGHTSCROLLBAR
riid
riverar
@@ -2165,7 +2178,9 @@ tsx
TYMED
typedef
TYPEKEY
+TYPEKEYBOARD
TYPELIB
+TYPEMOUSE
typename
typeof
typeparam
@@ -2468,6 +2483,7 @@ XSmall
XStr
XToolset
xunit
+XVIRTUALSCREEN
Yaml
YDiff
YIncrement
@@ -2478,6 +2494,7 @@ YStr
YUY
yuyoyuppe
YUYV
+YVIRTUALSCREEN
YVU
YVYU
ZEROINIT
diff --git a/.pipelines/pipeline.user.windows.yml b/.pipelines/pipeline.user.windows.yml
index 1e46e0810f..8f4b0da2f4 100644
--- a/.pipelines/pipeline.user.windows.yml
+++ b/.pipelines/pipeline.user.windows.yml
@@ -170,6 +170,7 @@ build:
- 'modules\launcher\Wox.dll'
- 'modules\launcher\Wox.Infrastructure.dll'
- 'modules\launcher\Wox.Plugin.dll'
+ - 'modules\MouseUtils\FindMyMouse.dll'
- 'modules\PowerRename\PowerRenameExt.dll'
- 'modules\ShortcutGuide\ShortcutGuide\PowerToys.ShortcutGuide.exe'
- 'modules\ShortcutGuide\ShortcutGuideModuleInterface\ShortcutGuideModuleInterface.dll'
diff --git a/COMMUNITY.md b/COMMUNITY.md
index a1ed96bddc..6c005bbede 100644
--- a/COMMUNITY.md
+++ b/COMMUNITY.md
@@ -72,6 +72,10 @@ PowerToys Awake is a tool to keep your computer awake.
Color Picker is from Martin.
+### [@oldnewthing](https://github.com/oldnewthing) - Raymond Chen
+
+Find My Mouse is based on Raymond Chen's SuperSonar.
+
### Microsoft InVEST team
This amazing team helped PowerToys develop PowerToys Run and Keyboard manager as well as update our Settings to v2. @alekhyareddy28, @arjunbalgovind, @jyuwono @laviusmotileng-ms, @ryanbodrug-microsoft, @saahmedm, @somil55, @traies, @udit3333
diff --git a/PowerToys.sln b/PowerToys.sln
index 4d0113baa7..17e285653d 100644
--- a/PowerToys.sln
+++ b/PowerToys.sln
@@ -372,6 +372,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.PowerToys.Run.Plu
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.PowerToys.Run.Plugin.WindowsTerminal.UnitTests", "src\modules\launcher\Plugins\Microsoft.PowerToys.Run.Plugin.WindowsTerminal.UnitTests\Microsoft.PowerToys.Run.Plugin.WindowsTerminal.UnitTests.csproj", "{4ED320BC-BA04-4D42-8D15-CBE62151F08B}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "MouseUtils", "MouseUtils", "{322566EF-20DC-43A6-B9F8-616AF942579A}"
+EndProject
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "FindMyMouse", "src\modules\MouseUtils\FindMyMouse\FindMyMouse.vcxproj", "{E94FD11C-0591-456F-899F-EFC0CA548336}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x64 = Debug|x64
@@ -981,6 +985,12 @@ Global
{4ED320BC-BA04-4D42-8D15-CBE62151F08B}.Release|x64.Build.0 = Release|x64
{4ED320BC-BA04-4D42-8D15-CBE62151F08B}.Release|x86.ActiveCfg = Release|Any CPU
{4ED320BC-BA04-4D42-8D15-CBE62151F08B}.Release|x86.Build.0 = Release|Any CPU
+ {E94FD11C-0591-456F-899F-EFC0CA548336}.Debug|x64.ActiveCfg = Debug|x64
+ {E94FD11C-0591-456F-899F-EFC0CA548336}.Debug|x64.Build.0 = Debug|x64
+ {E94FD11C-0591-456F-899F-EFC0CA548336}.Debug|x86.ActiveCfg = Debug|x64
+ {E94FD11C-0591-456F-899F-EFC0CA548336}.Release|x64.ActiveCfg = Release|x64
+ {E94FD11C-0591-456F-899F-EFC0CA548336}.Release|x64.Build.0 = Release|x64
+ {E94FD11C-0591-456F-899F-EFC0CA548336}.Release|x86.ActiveCfg = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -1098,6 +1108,8 @@ Global
{F40C3397-1834-4530-B2D9-8F8B8456BCDF} = {2F305555-C296-497E-AC20-5FA1B237996A}
{A2D583F0-B70C-4462-B1F0-8E81AFB7BA85} = {4AFC9975-2456-4C70-94A4-84073C1CED93}
{4ED320BC-BA04-4D42-8D15-CBE62151F08B} = {4AFC9975-2456-4C70-94A4-84073C1CED93}
+ {322566EF-20DC-43A6-B9F8-616AF942579A} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC}
+ {E94FD11C-0591-456F-899F-EFC0CA548336} = {322566EF-20DC-43A6-B9F8-616AF942579A}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0}
diff --git a/README.md b/README.md
index 8fbaf7633d..1f4708bc0f 100644
--- a/README.md
+++ b/README.md
@@ -19,7 +19,7 @@ Microsoft PowerToys is a set of utilities for power users to tune and streamline
| [Awake](https://aka.ms/PowerToysOverview_Awake) | [Color Picker](https://aka.ms/PowerToysOverview_ColorPicker) | [FancyZones](https://aka.ms/PowerToysOverview_FancyZones) |
| [File Explorer Add-ons](https://aka.ms/PowerToysOverview_FileExplorerAddOns) | [Image Resizer](https://aka.ms/PowerToysOverview_ImageResizer) | [Keyboard Manager](https://aka.ms/PowerToysOverview_KeyboardManager) |
| [PowerRename](https://aka.ms/PowerToysOverview_PowerRename) | [PowerToys Run](https://aka.ms/PowerToysOverview_PowerToysRun) | [Shortcut Guide](https://aka.ms/PowerToysOverview_ShortcutGuide) |
-| [Video Conference Mute (Experimental)](https://aka.ms/PowerToysOverview_VideoConference) | | |
+| [Video Conference Mute (Experimental)](https://aka.ms/PowerToysOverview_VideoConference) | [Mouse utilities](https://aka.ms/PowerToysOverview_MouseUtilities) | |
## Installing and running Microsoft PowerToys
diff --git a/doc/devdocs/akaLinks.md b/doc/devdocs/akaLinks.md
index e81275efef..064035fd16 100644
--- a/doc/devdocs/akaLinks.md
+++ b/doc/devdocs/akaLinks.md
@@ -25,6 +25,7 @@
| PowerToysOverview_FileExplorerAddOns | https://docs.microsoft.com/windows/powertoys/file-explorer |
| PowerToysOverview_ImageResizer | https://docs.microsoft.com/windows/powertoys/image-resizer |
| PowerToysOverview_KeyboardManager | https://docs.microsoft.com/windows/powertoys/keyboard-manager |
+| PowerToysOverview_MouseUtilities | https://docs.microsoft.com/windows/powertoys/mouse-utilities |
| PowerToysOverview_PowerRename | https://docs.microsoft.com/windows/powertoys/powerrename |
| PowerToysOverview_PowerToysRun | https://docs.microsoft.com/windows/powertoys/run |
| PowerToysOverview_ShortcutGuide | https://docs.microsoft.com/windows/powertoys/shortcut-guide |
diff --git a/doc/images/icons/MouseUtils.png b/doc/images/icons/MouseUtils.png
new file mode 100644
index 0000000000..08a730476e
Binary files /dev/null and b/doc/images/icons/MouseUtils.png differ
diff --git a/doc/images/overview/MouseUtils_large.png b/doc/images/overview/MouseUtils_large.png
new file mode 100644
index 0000000000..1e60dec571
Binary files /dev/null and b/doc/images/overview/MouseUtils_large.png differ
diff --git a/doc/images/overview/MouseUtils_small.png b/doc/images/overview/MouseUtils_small.png
new file mode 100644
index 0000000000..cb1c3907b1
Binary files /dev/null and b/doc/images/overview/MouseUtils_small.png differ
diff --git a/doc/images/overview/Original/MouseUtils.png b/doc/images/overview/Original/MouseUtils.png
new file mode 100644
index 0000000000..8e374e3ceb
Binary files /dev/null and b/doc/images/overview/Original/MouseUtils.png differ
diff --git a/installer/PowerToysSetup/Product.wxs b/installer/PowerToysSetup/Product.wxs
index 43518096c7..32df99d259 100644
--- a/installer/PowerToysSetup/Product.wxs
+++ b/installer/PowerToysSetup/Product.wxs
@@ -10,6 +10,7 @@
+
@@ -270,6 +271,10 @@
+
+
+
+
@@ -702,6 +707,13 @@
+
+
+
+
+
+
+
@@ -903,21 +915,21 @@
-
+
-
+
-
+
@@ -1012,6 +1024,7 @@
+
diff --git a/src/common/Microsoft.PowerToys.Common.UI/SettingsDeepLink.cs b/src/common/Microsoft.PowerToys.Common.UI/SettingsDeepLink.cs
index c5054ba7d6..e8a6cfdce0 100644
--- a/src/common/Microsoft.PowerToys.Common.UI/SettingsDeepLink.cs
+++ b/src/common/Microsoft.PowerToys.Common.UI/SettingsDeepLink.cs
@@ -19,6 +19,7 @@ namespace Microsoft.PowerToys.Common.UI
Run,
ImageResizer,
KBM,
+ MouseUtils,
PowerRename,
FileExplorer,
ShortcutGuide,
@@ -43,6 +44,8 @@ namespace Microsoft.PowerToys.Common.UI
return "ImageResizer";
case SettingsWindow.KBM:
return "KBM";
+ case SettingsWindow.MouseUtils:
+ return "MouseUtils";
case SettingsWindow.PowerRename:
return "PowerRename";
case SettingsWindow.FileExplorer:
diff --git a/src/common/logger/logger_settings.h b/src/common/logger/logger_settings.h
index d4ea19a40f..35b398792d 100644
--- a/src/common/logger/logger_settings.h
+++ b/src/common/logger/logger_settings.h
@@ -23,6 +23,7 @@ struct LogSettings
inline const static std::wstring shortcutGuideLogPath = L"ShortcutGuideLogs\\shortcut-guide-log.txt";
inline const static std::string keyboardManagerLoggerName = "keyboard-manager";
inline const static std::wstring keyboardManagerLogPath = L"Logs\\keyboard-manager-log.txt";
+ inline const static std::string findMyMouseLoggerName = "find-my-mouse";
inline const static int retention = 30;
std::wstring logLevel;
LogSettings();
diff --git a/src/modules/MouseUtils/FindMyMouse/Directory.Build.targets b/src/modules/MouseUtils/FindMyMouse/Directory.Build.targets
new file mode 100644
index 0000000000..53fe8fc425
--- /dev/null
+++ b/src/modules/MouseUtils/FindMyMouse/Directory.Build.targets
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/src/modules/MouseUtils/FindMyMouse/FindMyMouse.base.rc b/src/modules/MouseUtils/FindMyMouse/FindMyMouse.base.rc
new file mode 100644
index 0000000000..0bcdeca2ef
--- /dev/null
+++ b/src/modules/MouseUtils/FindMyMouse/FindMyMouse.base.rc
@@ -0,0 +1,40 @@
+#include
+#include "resource.h"
+#include "../../../../common/version/version.h"
+
+#define APSTUDIO_READONLY_SYMBOLS
+#include "winres.h"
+#undef APSTUDIO_READONLY_SYMBOLS
+
+1 VERSIONINFO
+FILEVERSION FILE_VERSION
+PRODUCTVERSION PRODUCT_VERSION
+FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
+#ifdef _DEBUG
+FILEFLAGS VS_FF_DEBUG
+#else
+FILEFLAGS 0x0L
+#endif
+FILEOS VOS_NT_WINDOWS32
+FILETYPE VFT_DLL
+FILESUBTYPE VFT2_UNKNOWN
+BEGIN
+ BLOCK "StringFileInfo"
+ BEGIN
+ BLOCK "040904b0" // US English (0x0409), Unicode (0x04B0) charset
+ BEGIN
+ VALUE "CompanyName", COMPANY_NAME
+ VALUE "FileDescription", FILE_DESCRIPTION
+ VALUE "FileVersion", FILE_VERSION_STRING
+ VALUE "InternalName", INTERNAL_NAME
+ VALUE "LegalCopyright", COPYRIGHT_NOTE
+ VALUE "OriginalFilename", ORIGINAL_FILENAME
+ VALUE "ProductName", PRODUCT_NAME
+ VALUE "ProductVersion", PRODUCT_VERSION_STRING
+ END
+ END
+ BLOCK "VarFileInfo"
+ BEGIN
+ VALUE "Translation", 0x409, 1200 // US English (0x0409), Unicode (1200) charset
+ END
+END
diff --git a/src/modules/MouseUtils/FindMyMouse/FindMyMouse.cpp b/src/modules/MouseUtils/FindMyMouse/FindMyMouse.cpp
new file mode 100644
index 0000000000..4af7906e43
--- /dev/null
+++ b/src/modules/MouseUtils/FindMyMouse/FindMyMouse.cpp
@@ -0,0 +1,814 @@
+// FindMyMouse.cpp : Based on Raymond Chen's SuperSonar.cpp
+//
+#include "pch.h"
+#include "FindMyMouse.h"
+#include "trace.h"
+
+#ifdef COMPOSITION
+namespace winrt
+{
+ using namespace winrt::Windows::System;
+ using namespace winrt::Windows::UI::Composition;
+}
+
+namespace ABI
+{
+ using namespace ABI::Windows::System;
+ using namespace ABI::Windows::UI::Composition::Desktop;
+}
+#endif
+
+#pragma region Super_Sonar_Base_Code
+
+template
+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;
+
+protected:
+ // Base class members you can access.
+ D* Shim() { return static_cast(this); }
+ LRESULT BaseWndProc(UINT message, WPARAM wParam, LPARAM lParam) noexcept;
+
+ HWND m_hwnd;
+ POINT m_sonarPos = ptNowhere;
+
+ static constexpr int SonarRadius = 100;
+ static constexpr int SonarZoomFactor = 9;
+ static constexpr DWORD FadeDuration = 500;
+ static constexpr int FinalAlphaNumerator = 1;
+ static constexpr int FinalAlphaDenominator = 2;
+ winrt::DispatcherQueueController m_dispatcherQueueController{ nullptr };
+
+private:
+ 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;
+
+ // Activate sonar: Hit LeftControl twice.
+ enum class SonarState
+ {
+ Idle,
+ ControlDown1,
+ ControlUp1,
+ ControlDown2,
+ ControlUp2,
+ };
+
+ HWND m_hwndOwner;
+ SonarState m_sonarState = SonarState::Idle;
+ POINT m_lastKeyPos{};
+ DWORD m_lastKeyTime{};
+
+ static constexpr DWORD NoSonar = 0;
+ static constexpr DWORD SonarWaitingForMouseMove = 1;
+ DWORD m_sonarStart = NoSonar;
+ bool m_isSnoopingMouse = false;
+
+private:
+ static constexpr auto className = L"FindMyMouse";
+
+ // Use the runner name for the Window title. Otherwise, since Find My Mouse has an actual visual, its Window name will be the one shown in Task Manager after being shown.
+ static constexpr auto windowTitle = L"PowerToys Runner";
+
+ 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();
+
+ void StartSonar();
+ void StopSonar();
+
+ void UpdateMouseSnooping();
+};
+
+template
+bool SuperSonar::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);
+ wc.hbrBackground = (HBRUSH)GetStockObject(NULL_BRUSH);
+ wc.lpszClassName = className;
+
+ if (!RegisterClassW(&wc))
+ {
+ return false;
+ }
+ }
+
+ m_hwndOwner = CreateWindow(L"static", nullptr, WS_POPUP, 0, 0, 0, 0, nullptr, nullptr, hinst, nullptr);
+
+ DWORD exStyle = WS_EX_TRANSPARENT | WS_EX_LAYERED | Shim()->GetExtendedStyle();
+ return CreateWindowExW(exStyle, className, windowTitle, WS_POPUP, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, m_hwndOwner, nullptr, hinst, this) != nullptr;
+}
+
+template
+void SuperSonar::Terminate()
+{
+ auto dispatcherQueue = m_dispatcherQueueController.DispatcherQueue();
+ bool enqueueSucceeded = dispatcherQueue.TryEnqueue([=]() {
+ DestroyWindow(m_hwndOwner);
+ });
+ if (!enqueueSucceeded)
+ {
+ Logger::error("Couldn't enqueue message to destroy the sonar Window.");
+ }
+}
+
+template
+LRESULT SuperSonar::s_WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
+{
+ SuperSonar* self;
+ if (message == WM_NCCREATE)
+ {
+ auto info = (LPCREATESTRUCT)lParam;
+ SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)info->lpCreateParams);
+ self = (SuperSonar*)info->lpCreateParams;
+ self->m_hwnd = hwnd;
+ }
+ else
+ {
+ self = (SuperSonar*)GetWindowLongPtr(hwnd, GWLP_USERDATA);
+ }
+ if (self)
+ {
+ return self->Shim()->WndProc(message, wParam, lParam);
+ }
+ else
+ {
+ return DefWindowProc(hwnd, message, wParam, lParam);
+ }
+}
+
+template
+LRESULT SuperSonar::BaseWndProc(UINT message, WPARAM wParam, LPARAM lParam) noexcept
+{
+ switch (message)
+ {
+ case WM_CREATE:
+ return OnSonarCreate() ? 0 : -1;
+
+ case WM_DESTROY:
+ OnSonarDestroy();
+ break;
+
+ case WM_INPUT:
+ OnSonarInput(wParam, (HRAWINPUT)lParam);
+ break;
+
+ case WM_TIMER:
+ switch (wParam)
+ {
+ case TIMER_ID_TRACK:
+ OnMouseTimer();
+ break;
+ }
+ break;
+
+ case WM_NCHITTEST:
+ return HTTRANSPARENT;
+ }
+
+ return DefWindowProc(m_hwnd, message, wParam, lParam);
+}
+
+template
+BOOL SuperSonar::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
+void SuperSonar::OnSonarDestroy()
+{
+ PostQuitMessage(0);
+}
+
+template
+void SuperSonar::OnSonarInput(WPARAM flags, HRAWINPUT hInput)
+{
+ RAWINPUT input;
+ UINT size = sizeof(input);
+ auto result = GetRawInputData(hInput, RID_INPUT, &input, &size, sizeof(RAWINPUTHEADER));
+ if ((int)result < sizeof(RAWINPUTHEADER))
+ {
+ return;
+ }
+
+ switch (input.header.dwType)
+ {
+ case RIM_TYPEKEYBOARD:
+ OnSonarKeyboardInput(input);
+ break;
+ case RIM_TYPEMOUSE:
+ OnSonarMouseInput(input);
+ break;
+ }
+}
+
+template
+void SuperSonar::OnSonarKeyboardInput(RAWINPUT const& input)
+{
+ if (input.data.keyboard.VKey != VK_CONTROL)
+ {
+ StopSonar();
+ return;
+ }
+
+ bool pressed = (input.data.keyboard.Flags & RI_KEY_BREAK) == 0;
+ bool rightCtrl = (input.data.keyboard.Flags & RI_KEY_E0) != 0;
+
+ // Deal with rightCtrl first.
+ if (rightCtrl)
+ {
+ /*
+ * SuperSonar originally exited when pressing right control after pressing left control twice.
+ * We take care of exiting FindMyMouse through module disabling in PowerToys settings instead.
+ if (m_sonarState == SonarState::ControlUp2)
+ {
+ Terminate();
+ }
+ */
+ StopSonar();
+ return;
+ }
+
+ switch (m_sonarState)
+ {
+ case SonarState::Idle:
+ if (pressed)
+ {
+ m_sonarState = SonarState::ControlDown1;
+ m_lastKeyTime = GetTickCount();
+ m_lastKeyPos = {};
+ GetCursorPos(&m_lastKeyPos);
+ UpdateMouseSnooping();
+ }
+ break;
+
+ case SonarState::ControlDown1:
+ if (!pressed)
+ {
+ m_sonarState = SonarState::ControlUp1;
+ }
+ break;
+
+ case SonarState::ControlUp1:
+ case SonarState::ControlUp2:
+ if (pressed)
+ {
+ m_sonarState = SonarState::ControlDown2;
+ auto now = GetTickCount();
+ POINT ptCursor{};
+ if (GetCursorPos(&ptCursor) &&
+ now - m_lastKeyTime <= GetDoubleClickTime() &&
+ IsEqual(m_lastKeyPos, ptCursor))
+ {
+ StartSonar();
+ }
+ m_lastKeyTime = now;
+ m_lastKeyPos = ptCursor;
+ }
+ break;
+
+ case SonarState::ControlDown2:
+ if (!pressed)
+ {
+ m_sonarState = SonarState::ControlUp2;
+ }
+ break;
+ }
+}
+
+template
+void SuperSonar::OnSonarMouseInput(RAWINPUT const& input)
+{
+ if (input.data.mouse.usButtonFlags)
+ {
+ StopSonar();
+ }
+ else if (m_sonarStart != NoSonar)
+ {
+ OnMouseTimer();
+ }
+}
+
+template
+void SuperSonar::StartSonar()
+{
+ Logger::info("Focusing the sonar on the mouse cursor.");
+ Trace::MousePointerFocused();
+ // Cover the entire virtual screen.
+ SetWindowPos(m_hwnd, HWND_TOPMOST, GetSystemMetrics(SM_XVIRTUALSCREEN), GetSystemMetrics(SM_YVIRTUALSCREEN), GetSystemMetrics(SM_CXVIRTUALSCREEN), GetSystemMetrics(SM_CYVIRTUALSCREEN), 0);
+ m_sonarPos = ptNowhere;
+ OnMouseTimer();
+ UpdateMouseSnooping();
+ Shim()->SetSonarVisibility(true);
+}
+
+template
+void SuperSonar::StopSonar()
+{
+ if (m_sonarStart != NoSonar)
+ {
+ m_sonarStart = NoSonar;
+ Shim()->SetSonarVisibility(false);
+ KillTimer(m_hwnd, TIMER_ID_TRACK);
+ }
+ m_sonarState = SonarState::Idle;
+ UpdateMouseSnooping();
+}
+
+template
+void SuperSonar::OnMouseTimer()
+{
+ auto now = GetTickCount();
+
+ // 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
+void SuperSonar::UpdateMouseSnooping()
+{
+ bool wantSnoopingMouse = m_sonarStart != NoSonar || m_sonarState != SonarState::Idle;
+ 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));
+ }
+}
+
+struct CompositionSpotlight : SuperSonar
+{
+ static constexpr UINT WM_OPACITY_ANIMATION_COMPLETED = WM_APP;
+ static constexpr float SonarRadiusFloat = static_cast(SonarRadius);
+
+ DWORD GetExtendedStyle()
+ {
+ return WS_EX_NOREDIRECTIONBITMAP;
+ }
+
+ void AfterMoveSonar()
+ {
+ m_spotlight.Offset({ (float)m_sonarPos.x, (float)m_sonarPos.y, 0.0f });
+ }
+
+ LRESULT WndProc(UINT message, WPARAM wParam, LPARAM lParam) noexcept
+ {
+ switch (message)
+ {
+ case WM_CREATE:
+ return OnCompositionCreate() && BaseWndProc(message, wParam, lParam);
+
+ case WM_OPACITY_ANIMATION_COMPLETED:
+ OnOpacityAnimationCompleted();
+ break;
+ }
+ return BaseWndProc(message, wParam, lParam);
+ }
+
+ void SetSonarVisibility(bool visible)
+ {
+ m_batch = m_compositor.GetCommitBatch(winrt::CompositionBatchTypes::Animation);
+ m_batch.Completed([hwnd = m_hwnd](auto&&, auto&&) {
+ PostMessage(hwnd, WM_OPACITY_ANIMATION_COMPLETED, 0, 0);
+ });
+ m_root.Opacity(visible ? static_cast(FinalAlphaNumerator) / FinalAlphaDenominator : 0.0f);
+ if (visible)
+ {
+ ShowWindow(m_hwnd, SW_SHOWNOACTIVATE);
+ }
+ }
+
+private:
+ bool OnCompositionCreate()
+ try
+ {
+ // We need a dispatcher queue.
+ DispatcherQueueOptions options = {
+ sizeof(options),
+ DQTYPE_THREAD_CURRENT,
+ DQTAT_COM_ASTA,
+ };
+ ABI::IDispatcherQueueController* controller;
+ winrt::check_hresult(CreateDispatcherQueueController(options, &controller));
+ *winrt::put_abi(m_dispatcherQueueController) = controller;
+
+ // Create the compositor for our window.
+ m_compositor = winrt::Compositor();
+ ABI::IDesktopWindowTarget* target;
+ winrt::check_hresult(m_compositor.as()->CreateDesktopWindowTarget(m_hwnd, false, &target));
+ *winrt::put_abi(m_target) = target;
+
+ // Our composition tree:
+ //
+ // [root] ContainerVisual
+ // \ LayerVisual
+ // \[gray backdrop]
+ // [spotlight]
+ m_root = m_compositor.CreateContainerVisual();
+ m_root.RelativeSizeAdjustment({ 1.0f, 1.0f }); // fill the parent
+ m_root.Opacity(0.0f);
+ m_target.Root(m_root);
+
+ auto layer = m_compositor.CreateLayerVisual();
+ layer.RelativeSizeAdjustment({ 1.0f, 1.0f }); // fill the parent
+ m_root.Children().InsertAtTop(layer);
+
+ auto backdrop = m_compositor.CreateSpriteVisual();
+ backdrop.RelativeSizeAdjustment({ 1.0f, 1.0f }); // fill the parent
+ backdrop.Brush(m_compositor.CreateColorBrush({ 255, 0, 0, 0 }));
+ layer.Children().InsertAtTop(backdrop);
+
+ m_circleGeometry = m_compositor.CreateEllipseGeometry(); // radius set via expression animation
+ auto circleShape = m_compositor.CreateSpriteShape(m_circleGeometry);
+ circleShape.FillBrush(m_compositor.CreateColorBrush({ 255, 255, 255, 255 }));
+ circleShape.Offset({ SonarRadiusFloat * SonarZoomFactor, SonarRadiusFloat * SonarZoomFactor });
+ m_spotlight = m_compositor.CreateShapeVisual();
+ m_spotlight.Size({ SonarRadiusFloat * 2 * SonarZoomFactor, SonarRadiusFloat * 2 * SonarZoomFactor });
+ m_spotlight.AnchorPoint({ 0.5f, 0.5f });
+ m_spotlight.Shapes().Append(circleShape);
+
+ layer.Children().InsertAtTop(m_spotlight);
+
+ // Implicitly animate the alpha.
+ auto animation = m_compositor.CreateScalarKeyFrameAnimation();
+ animation.Target(L"Opacity");
+ animation.InsertExpressionKeyFrame(1.0f, L"this.FinalValue");
+ animation.Duration(std::chrono::milliseconds{ FadeDuration });
+ auto collection = m_compositor.CreateImplicitAnimationCollection();
+ collection.Insert(L"Opacity", animation);
+ m_root.ImplicitAnimations(collection);
+
+ // Radius of spotlight shrinks as opacity increases.
+ // At opacity zero, it is SonarRadius * SonarZoomFactor.
+ // At maximum opacity, it is SonarRadius.
+ 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(%d, %d), Vector2(%d, %d), Root.Opacity * %d / %d)", SonarRadius * SonarZoomFactor, SonarRadius * SonarZoomFactor, SonarRadius, SonarRadius, FinalAlphaDenominator, FinalAlphaNumerator));
+ radiusExpression.Expression(expressionText);
+ m_circleGeometry.StartAnimation(L"Radius", radiusExpression);
+
+ return true;
+ }
+ catch (...)
+ {
+ return false;
+ }
+
+ void OnOpacityAnimationCompleted()
+ {
+ if (m_root.Opacity() < 0.01f)
+ {
+ ShowWindow(m_hwnd, SW_HIDE);
+ }
+ }
+
+private:
+ winrt::Compositor m_compositor{ nullptr };
+ winrt::Desktop::DesktopWindowTarget m_target{ nullptr };
+ winrt::ContainerVisual m_root{ nullptr };
+ winrt::CompositionEllipseGeometry m_circleGeometry{ nullptr };
+ winrt::ShapeVisual m_spotlight{ nullptr };
+ winrt::CompositionCommitBatch m_batch{ nullptr };
+};
+
+template
+struct GdiSonar : SuperSonar
+{
+ LRESULT WndProc(UINT message, WPARAM wParam, LPARAM lParam) noexcept
+ {
+ switch (message)
+ {
+ case WM_CREATE:
+ SetLayeredWindowAttributes(this->m_hwnd, 0, 0, LWA_ALPHA);
+ break;
+
+ case WM_TIMER:
+ switch (wParam)
+ {
+ case TIMER_ID_FADE:
+ OnFadeTimer();
+ break;
+ }
+ break;
+
+ case WM_PAINT:
+ this->Shim()->OnPaint();
+ break;
+ }
+ return this->BaseWndProc(message, wParam, lParam);
+ }
+
+ void BeforeMoveSonar() { this->Shim()->InvalidateSonar(); }
+ void AfterMoveSonar() { this->Shim()->InvalidateSonar(); }
+
+ void SetSonarVisibility(bool visible)
+ {
+ m_alphaTarget = visible ? MaxAlpha : 0;
+ m_fadeStart = GetTickCount() - FadeFramePeriod;
+ SetTimer(this->m_hwnd, TIMER_ID_FADE, FadeFramePeriod, nullptr);
+ OnFadeTimer();
+ }
+
+ void OnFadeTimer()
+ {
+ auto now = GetTickCount();
+ auto step = (int)((now - m_fadeStart) * MaxAlpha / this->FadeDuration);
+
+ this->Shim()->InvalidateSonar();
+ if (m_alpha < m_alphaTarget)
+ {
+ m_alpha += step;
+ if (m_alpha > m_alphaTarget)
+ m_alpha = m_alphaTarget;
+ }
+ else if (m_alpha > m_alphaTarget)
+ {
+ m_alpha -= step;
+ if (m_alpha < m_alphaTarget)
+ m_alpha = m_alphaTarget;
+ }
+ SetLayeredWindowAttributes(this->m_hwnd, 0, (BYTE)m_alpha, LWA_ALPHA);
+ this->Shim()->InvalidateSonar();
+ if (m_alpha == m_alphaTarget)
+ {
+ KillTimer(this->m_hwnd, TIMER_ID_FADE);
+ if (m_alpha == 0)
+ {
+ ShowWindow(this->m_hwnd, SW_HIDE);
+ }
+ }
+ else
+ {
+ ShowWindow(this->m_hwnd, SW_SHOWNOACTIVATE);
+ }
+ }
+
+protected:
+ int CurrentSonarRadius()
+ {
+ int range = MaxAlpha - m_alpha;
+ int radius = this->SonarRadius + this->SonarRadius * range * (this->SonarZoomFactor - 1) / MaxAlpha;
+ return radius;
+ }
+
+private:
+ static constexpr DWORD FadeFramePeriod = 10;
+ static constexpr int MaxAlpha = SuperSonar::FinalAlphaNumerator * 255 / SuperSonar::FinalAlphaDenominator;
+ static constexpr DWORD TIMER_ID_FADE = 101;
+
+private:
+ int m_alpha = 0;
+ int m_alphaTarget = 0;
+ DWORD m_fadeStart = 0;
+};
+
+struct GdiSpotlight : GdiSonar
+{
+ void InvalidateSonar()
+ {
+ RECT rc;
+ auto radius = CurrentSonarRadius();
+ rc.left = this->m_sonarPos.x - radius;
+ rc.top = this->m_sonarPos.y - radius;
+ rc.right = this->m_sonarPos.x + radius;
+ rc.bottom = this->m_sonarPos.y + radius;
+ InvalidateRect(this->m_hwnd, &rc, FALSE);
+ }
+
+ void OnPaint()
+ {
+ PAINTSTRUCT ps;
+ BeginPaint(this->m_hwnd, &ps);
+
+ auto radius = CurrentSonarRadius();
+ auto spotlight = CreateRoundRectRgn(
+ this->m_sonarPos.x - radius, this->m_sonarPos.y - radius, this->m_sonarPos.x + radius, this->m_sonarPos.y + radius, radius * 2, radius * 2);
+
+ FillRgn(ps.hdc, spotlight, (HBRUSH)GetStockObject(WHITE_BRUSH));
+ Sleep(1000 / 60);
+ ExtSelectClipRgn(ps.hdc, spotlight, RGN_DIFF);
+ FillRect(ps.hdc, &ps.rcPaint, (HBRUSH)GetStockObject(BLACK_BRUSH));
+ DeleteObject(spotlight);
+
+ EndPaint(this->m_hwnd, &ps);
+ }
+};
+
+struct GdiCrosshairs : GdiSonar
+{
+ void InvalidateSonar()
+ {
+ RECT rc;
+ auto radius = CurrentSonarRadius();
+ GetClientRect(m_hwnd, &rc);
+ rc.left = m_sonarPos.x - radius;
+ rc.right = m_sonarPos.x + radius;
+ InvalidateRect(m_hwnd, &rc, FALSE);
+
+ GetClientRect(m_hwnd, &rc);
+ rc.top = m_sonarPos.y - radius;
+ rc.bottom = m_sonarPos.y + radius;
+ InvalidateRect(m_hwnd, &rc, FALSE);
+ }
+
+ void OnPaint()
+ {
+ PAINTSTRUCT ps;
+ BeginPaint(this->m_hwnd, &ps);
+
+ auto radius = CurrentSonarRadius();
+ RECT rc;
+
+ HBRUSH white = (HBRUSH)GetStockObject(WHITE_BRUSH);
+
+ rc.left = m_sonarPos.x - radius;
+ rc.top = ps.rcPaint.top;
+ rc.right = m_sonarPos.x + radius;
+ rc.bottom = ps.rcPaint.bottom;
+ FillRect(ps.hdc, &rc, white);
+
+ rc.left = ps.rcPaint.left;
+ rc.top = m_sonarPos.y - radius;
+ rc.right = ps.rcPaint.right;
+ rc.bottom = m_sonarPos.y + radius;
+ FillRect(ps.hdc, &rc, white);
+
+ HBRUSH black = (HBRUSH)GetStockObject(BLACK_BRUSH);
+
+ // Top left
+ rc.left = ps.rcPaint.left;
+ rc.top = ps.rcPaint.top;
+ rc.right = m_sonarPos.x - radius;
+ rc.bottom = m_sonarPos.y - radius;
+ FillRect(ps.hdc, &rc, black);
+
+ // Top right
+ rc.left = m_sonarPos.x + radius;
+ rc.top = ps.rcPaint.top;
+ rc.right = ps.rcPaint.right;
+ rc.bottom = m_sonarPos.y - radius;
+ FillRect(ps.hdc, &rc, black);
+
+ // Bottom left
+ rc.left = ps.rcPaint.left;
+ rc.top = m_sonarPos.y + radius;
+ rc.right = m_sonarPos.x - radius;
+ rc.bottom = ps.rcPaint.bottom;
+ FillRect(ps.hdc, &rc, black);
+
+ // Bottom right
+ rc.left = m_sonarPos.x + radius;
+ rc.top = m_sonarPos.y + radius;
+ rc.right = ps.rcPaint.right;
+ rc.bottom = ps.rcPaint.bottom;
+ FillRect(ps.hdc, &rc, black);
+
+ EndPaint(this->m_hwnd, &ps);
+ }
+};
+
+#pragma endregion Super_Sonar_Base_Code
+
+
+#pragma region Super_Sonar_API
+
+CompositionSpotlight* m_sonar = nullptr;
+
+void FindMyMouseDisable()
+{
+ if (m_sonar != nullptr)
+ {
+ Logger::info("Terminating a sonar instance.");
+ m_sonar->Terminate();
+ }
+}
+
+bool FindMyMouseIsEnabled()
+{
+ return (m_sonar != nullptr);
+}
+
+// Based on SuperSonar's original wWinMain.
+int FindMyMouseMain(HINSTANCE hinst)
+{
+ Logger::info("Starting a sonar instance.");
+ if (m_sonar != nullptr)
+ {
+ Logger::error("A sonar instance was still working when trying to start a new one.");
+ return 0;
+ }
+
+ CompositionSpotlight sonar;
+ if (!sonar.Initialize(hinst))
+ {
+ Logger::error("Couldn't initialize a sonar instance.");
+ return 0;
+ }
+ m_sonar = &sonar;
+ Logger::info("Initialized the sonar instance.");
+
+ MSG msg;
+
+ // Main message loop:
+ while (GetMessage(&msg, nullptr, 0, 0))
+ {
+ TranslateMessage(&msg);
+ DispatchMessage(&msg);
+ }
+
+ Logger::info("Sonar message loop ended.");
+ m_sonar = nullptr;
+
+ return (int)msg.wParam;
+}
+
+#pragma endregion Super_Sonar_API
diff --git a/src/modules/MouseUtils/FindMyMouse/FindMyMouse.h b/src/modules/MouseUtils/FindMyMouse/FindMyMouse.h
new file mode 100644
index 0000000000..feaa5d121b
--- /dev/null
+++ b/src/modules/MouseUtils/FindMyMouse/FindMyMouse.h
@@ -0,0 +1,6 @@
+#pragma once
+#include "pch.h"
+int FindMyMouseMain(HINSTANCE hinst);
+void FindMyMouseDisable();
+bool FindMyMouseIsEnabled();
+
diff --git a/src/modules/MouseUtils/FindMyMouse/FindMyMouse.vcxproj b/src/modules/MouseUtils/FindMyMouse/FindMyMouse.vcxproj
new file mode 100644
index 0000000000..575955bd08
--- /dev/null
+++ b/src/modules/MouseUtils/FindMyMouse/FindMyMouse.vcxproj
@@ -0,0 +1,146 @@
+
+
+
+
+
+ Debug
+ x64
+
+
+ Release
+ x64
+
+
+
+ 15.0
+ {e94fd11c-0591-456f-899f-efc0ca548336}
+ Win32Proj
+ FindMyMouse
+ true
+ 10.0.18362.0
+ FindMyMouse
+
+
+
+ DynamicLibrary
+ true
+ v142
+ Unicode
+
+
+ DynamicLibrary
+ false
+ v142
+ true
+ Unicode
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+ $(SolutionDir)$(Platform)\$(Configuration)\modules\MouseUtils\
+
+
+ false
+ $(SolutionDir)$(Platform)\$(Configuration)\modules\MouseUtils\
+
+
+
+ Level3
+ Disabled
+ true
+ _DEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)
+ true
+ MultiThreadedDebug
+ stdcpplatest
+
+
+ Windows
+ true
+ $(OutDir)$(TargetName)$(TargetExt)
+
+
+
+
+ Level3
+ MaxSpeed
+ true
+ true
+ true
+ NDEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)
+ true
+ MultiThreaded
+ stdcpplatest
+
+
+ Windows
+ true
+ true
+ true
+ $(OutDir)$(TargetName)$(TargetExt)
+
+
+
+
+ $(SolutionDir)src\;$(SolutionDir)src\modules;$(SolutionDir)src\common\Telemetry;%(AdditionalIncludeDirectories)
+
+
+
+
+ Use
+ pch.h
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Create
+
+
+
+
+
+
+ {d9b8fc84-322a-4f9f-bbb9-20915c47ddfd}
+
+
+ {6955446d-23f7-4023-9bb3-8657f904af99}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.
+
+
+
+
+
\ No newline at end of file
diff --git a/src/modules/MouseUtils/FindMyMouse/FindMyMouse.vcxproj.filters b/src/modules/MouseUtils/FindMyMouse/FindMyMouse.vcxproj.filters
new file mode 100644
index 0000000000..f2c5464f75
--- /dev/null
+++ b/src/modules/MouseUtils/FindMyMouse/FindMyMouse.vcxproj.filters
@@ -0,0 +1,62 @@
+
+
+
+
+ {4FC737F1-C7A5-4376-A066-2A32D752A2FF}
+ cpp;c;cc;cxx;c++;def;odl;idl;hpj;bat;asm;asmx
+
+
+ {93995380-89BD-4b04-88EB-625FBE52EBFB}
+ h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd
+
+
+ {67DA6AB6-F800-4c08-8B7A-83BB121AAD01}
+ rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms
+
+
+ {875a08c6-f610-4667-bd0f-80171ed96072}
+
+
+
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
+
+
+
+ Header Files
+
+
+ Header Files
+
+
+ Header Files
+
+
+ Generated Files
+
+
+
+
+
+ Resource Files
+
+
+ Resource Files
+
+
+
+
+ Generated Files
+
+
+
\ No newline at end of file
diff --git a/src/modules/MouseUtils/FindMyMouse/dllmain.cpp b/src/modules/MouseUtils/FindMyMouse/dllmain.cpp
new file mode 100644
index 0000000000..26ebbb8994
--- /dev/null
+++ b/src/modules/MouseUtils/FindMyMouse/dllmain.cpp
@@ -0,0 +1,148 @@
+#include "pch.h"
+#include
+#include
+#include "trace.h"
+#include "FindMyMouse.h"
+#include
+#include
+
+extern "C" IMAGE_DOS_HEADER __ImageBase;
+
+HMODULE m_hModule;
+
+BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
+{
+ m_hModule = hModule;
+ switch (ul_reason_for_call)
+ {
+ case DLL_PROCESS_ATTACH:
+ Trace::RegisterProvider();
+ break;
+ case DLL_THREAD_ATTACH:
+ case DLL_THREAD_DETACH:
+ break;
+ case DLL_PROCESS_DETACH:
+ Trace::UnregisterProvider();
+ break;
+ }
+ return TRUE;
+}
+
+// The PowerToy name that will be shown in the settings.
+const static wchar_t* MODULE_NAME = L"FindMyMouse";
+// Add a description that will we shown in the module settings page.
+const static wchar_t* MODULE_DESC = L"Focus the mouse pointer";
+
+// Implement the PowerToy Module Interface and all the required methods.
+class FindMyMouse : public PowertoyModuleIface
+{
+private:
+ // The PowerToy state.
+ bool m_enabled = false;
+
+ // Load initial settings from the persisted values.
+ void init_settings();
+
+public:
+ // Constructor
+ FindMyMouse()
+ {
+ LoggerHelpers::init_logger(MODULE_NAME, L"ModuleInterface", LogSettings::findMyMouseLoggerName);
+ init_settings();
+ };
+
+ // Destroy the powertoy and free memory
+ virtual void destroy() override
+ {
+ delete this;
+ }
+
+ // Return the localized display name of the powertoy
+ virtual const wchar_t* get_name() override
+ {
+ return MODULE_NAME;
+ }
+
+ // Return the non localized key of the powertoy, this will be cached by the runner
+ virtual const wchar_t* get_key() override
+ {
+ return MODULE_NAME;
+ }
+
+ // Return JSON with the configuration options.
+ virtual bool get_config(wchar_t* buffer, int* buffer_size) override
+ {
+ HINSTANCE hinstance = reinterpret_cast(&__ImageBase);
+
+ // Create a Settings object.
+ PowerToysSettings::Settings settings(hinstance, get_name());
+ settings.set_description(MODULE_DESC);
+
+ return settings.serialize_to_buffer(buffer, buffer_size);
+ }
+
+ // Signal from the Settings editor to call a custom action.
+ // This can be used to spawn more complex editors.
+ virtual void call_custom_action(const wchar_t* action) override
+ {
+ }
+
+ // Called by the runner to pass the updated settings values as a serialized JSON.
+ virtual void set_config(const wchar_t* config) override
+ {
+ try
+ {
+ // Parse the input JSON string.
+ PowerToysSettings::PowerToyValues values =
+ PowerToysSettings::PowerToyValues::from_json_string(config, get_key());
+
+ values.save_to_settings_file();
+ }
+ catch (std::exception&)
+ {
+ // Improper JSON.
+ }
+ }
+
+ // Enable the powertoy
+ virtual void enable()
+ {
+ m_enabled = true;
+ Trace::EnableFindMyMouse(true);
+ std::thread([]() { FindMyMouseMain(m_hModule); }).detach();
+ }
+
+ // Disable the powertoy
+ virtual void disable()
+ {
+ m_enabled = false;
+ Trace::EnableFindMyMouse(false);
+ FindMyMouseDisable();
+ }
+
+ // Returns if the powertoys is enabled
+ virtual bool is_enabled() override
+ {
+ return m_enabled;
+ }
+};
+
+// Load the settings file.
+void FindMyMouse::init_settings()
+{
+ try
+ {
+ // Load and parse the settings file for this PowerToy.
+ PowerToysSettings::PowerToyValues settings =
+ PowerToysSettings::PowerToyValues::load_from_settings_file(FindMyMouse::get_key());
+ }
+ catch (std::exception&)
+ {
+ // Error while loading from the settings file. Let default values stay as they are.
+ }
+}
+
+extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()
+{
+ return new FindMyMouse();
+}
\ No newline at end of file
diff --git a/src/modules/MouseUtils/FindMyMouse/packages.config b/src/modules/MouseUtils/FindMyMouse/packages.config
new file mode 100644
index 0000000000..81f107b8bc
--- /dev/null
+++ b/src/modules/MouseUtils/FindMyMouse/packages.config
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/src/modules/MouseUtils/FindMyMouse/pch.cpp b/src/modules/MouseUtils/FindMyMouse/pch.cpp
new file mode 100644
index 0000000000..1d9f38c57d
--- /dev/null
+++ b/src/modules/MouseUtils/FindMyMouse/pch.cpp
@@ -0,0 +1 @@
+#include "pch.h"
diff --git a/src/modules/MouseUtils/FindMyMouse/pch.h b/src/modules/MouseUtils/FindMyMouse/pch.h
new file mode 100644
index 0000000000..6dbc256004
--- /dev/null
+++ b/src/modules/MouseUtils/FindMyMouse/pch.h
@@ -0,0 +1,20 @@
+#pragma once
+
+#define COMPOSITION
+#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers
+#include
+#include
+#include
+
+#ifdef COMPOSITION
+#include
+#include
+#include
+#include
+#include
+#endif
+
+#include
+#include
+#include
+#include
diff --git a/src/modules/MouseUtils/FindMyMouse/resource.base.h b/src/modules/MouseUtils/FindMyMouse/resource.base.h
new file mode 100644
index 0000000000..83354c2884
--- /dev/null
+++ b/src/modules/MouseUtils/FindMyMouse/resource.base.h
@@ -0,0 +1,14 @@
+//{{NO_DEPENDENCIES}}
+// Microsoft Visual C++ generated include file.
+// Used by FindMyMouse.rc
+
+//////////////////////////////
+// Non-localizable
+
+#define FILE_DESCRIPTION "PowerToys FindMyMouse"
+#define INTERNAL_NAME "FindMyMouse"
+#define ORIGINAL_FILENAME "FindMyMouse.dll"
+#define IDS_KEYBOARDMANAGER_ICON 1001
+
+// Non-localizable
+//////////////////////////////
diff --git a/src/modules/MouseUtils/FindMyMouse/trace.cpp b/src/modules/MouseUtils/FindMyMouse/trace.cpp
new file mode 100644
index 0000000000..a5cfe02417
--- /dev/null
+++ b/src/modules/MouseUtils/FindMyMouse/trace.cpp
@@ -0,0 +1,40 @@
+#include "pch.h"
+#include "trace.h"
+
+TRACELOGGING_DEFINE_PROVIDER(
+ g_hProvider,
+ "Microsoft.PowerToys",
+ // {38e8889b-9731-53f5-e901-e8a7c1753074}
+ (0x38e8889b, 0x9731, 0x53f5, 0xe9, 0x01, 0xe8, 0xa7, 0xc1, 0x75, 0x30, 0x74),
+ TraceLoggingOptionProjectTelemetry());
+
+void Trace::RegisterProvider() noexcept
+{
+ TraceLoggingRegister(g_hProvider);
+}
+
+void Trace::UnregisterProvider() noexcept
+{
+ TraceLoggingUnregister(g_hProvider);
+}
+
+// Log if the user has FindMyMouse enabled or disabled
+void Trace::EnableFindMyMouse(const bool enabled) noexcept
+{
+ TraceLoggingWrite(
+ g_hProvider,
+ "FindMyMouse_EnableFindMyMouse",
+ ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
+ TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE),
+ TraceLoggingBoolean(enabled, "Enabled"));
+}
+
+// Log that the user activated the module by focusing the mouse pointer
+void Trace::MousePointerFocused() noexcept
+{
+ TraceLoggingWrite(
+ g_hProvider,
+ "FindMyMouse_MousePointerFocused",
+ ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
+ TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE));
+}
diff --git a/src/modules/MouseUtils/FindMyMouse/trace.h b/src/modules/MouseUtils/FindMyMouse/trace.h
new file mode 100644
index 0000000000..623ce60176
--- /dev/null
+++ b/src/modules/MouseUtils/FindMyMouse/trace.h
@@ -0,0 +1,14 @@
+#pragma once
+
+class Trace
+{
+public:
+ static void RegisterProvider() noexcept;
+ static void UnregisterProvider() noexcept;
+
+ // Log if the user has FindMyMouse enabled or disabled
+ static void EnableFindMyMouse(const bool enabled) noexcept;
+
+ // Log that the user activated the module by focusing the mouse pointer
+ static void MousePointerFocused() noexcept;
+};
diff --git a/src/runner/main.cpp b/src/runner/main.cpp
index 0a2941ec56..7743ae5818 100644
--- a/src/runner/main.cpp
+++ b/src/runner/main.cpp
@@ -147,7 +147,8 @@ int runner(bool isProcessElevated, bool openSettings, std::string settingsWindow
L"modules/PowerRename/PowerRenameExt.dll",
L"modules/ShortcutGuide/ShortcutGuideModuleInterface/ShortcutGuideModuleInterface.dll",
L"modules/ColorPicker/ColorPicker.dll",
- L"modules/Awake/AwakeModuleInterface.dll"
+ L"modules/Awake/AwakeModuleInterface.dll",
+ L"modules/MouseUtils/FindMyMouse.dll"
};
const auto VCM_PATH = L"modules/VideoConference/VideoConferenceModule.dll";
diff --git a/src/runner/settings_window.cpp b/src/runner/settings_window.cpp
index 9d91bbb020..cb009dfef6 100644
--- a/src/runner/settings_window.cpp
+++ b/src/runner/settings_window.cpp
@@ -523,6 +523,8 @@ std::string ESettingsWindowNames_to_string(ESettingsWindowNames value)
return "ImageResizer";
case ESettingsWindowNames::KBM:
return "KBM";
+ case ESettingsWindowNames::MouseUtils:
+ return "MouseUtils";
case ESettingsWindowNames::PowerRename:
return "PowerRename";
case ESettingsWindowNames::FileExplorer:
@@ -570,6 +572,10 @@ ESettingsWindowNames ESettingsWindowNames_from_string(std::string value)
{
return ESettingsWindowNames::KBM;
}
+ else if (value == "MouseUtils")
+ {
+ return ESettingsWindowNames::MouseUtils;
+ }
else if (value == "PowerRename")
{
return ESettingsWindowNames::PowerRename;
diff --git a/src/runner/settings_window.h b/src/runner/settings_window.h
index 44614b0a01..feeaa7abb3 100644
--- a/src/runner/settings_window.h
+++ b/src/runner/settings_window.h
@@ -11,6 +11,7 @@ enum class ESettingsWindowNames
Run,
ImageResizer,
KBM,
+ MouseUtils,
PowerRename,
FileExplorer,
ShortcutGuide,
diff --git a/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/EnabledModules.cs b/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/EnabledModules.cs
index 227ebb9488..0232ff641b 100644
--- a/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/EnabledModules.cs
+++ b/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/EnabledModules.cs
@@ -175,6 +175,22 @@ namespace Microsoft.PowerToys.Settings.UI.Library
}
}
+ private bool findMyMouse = true;
+
+ [JsonPropertyName("FindMyMouse")]
+ public bool FindMyMouse
+ {
+ get => findMyMouse;
+ set
+ {
+ if (findMyMouse != value)
+ {
+ LogTelemetryEvent(value);
+ findMyMouse = value;
+ }
+ }
+ }
+
public string ToJsonString()
{
return JsonSerializer.Serialize(this);
diff --git a/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/ViewModels/MouseUtilsViewModel.cs b/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/ViewModels/MouseUtilsViewModel.cs
new file mode 100644
index 0000000000..6aeadef00d
--- /dev/null
+++ b/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/ViewModels/MouseUtilsViewModel.cs
@@ -0,0 +1,56 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using Microsoft.PowerToys.Settings.UI.Library.Helpers;
+using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
+
+namespace Microsoft.PowerToys.Settings.UI.Library.ViewModels
+{
+ public class MouseUtilsViewModel : Observable
+ {
+ private GeneralSettings GeneralSettingsConfig { get; set; }
+
+ public MouseUtilsViewModel(ISettingsRepository settingsRepository, Func ipcMSGCallBackFunc)
+ {
+ // To obtain the general settings configurations of PowerToys Settings.
+ if (settingsRepository == null)
+ {
+ throw new ArgumentNullException(nameof(settingsRepository));
+ }
+
+ GeneralSettingsConfig = settingsRepository.SettingsConfig;
+
+ _isFindMyMouseEnabled = GeneralSettingsConfig.Enabled.FindMyMouse;
+
+ // set the callback functions value to hangle outgoing IPC message.
+ SendConfigMSG = ipcMSGCallBackFunc;
+ }
+
+ public bool IsFindMyMouseEnabled
+ {
+ get => _isFindMyMouseEnabled;
+ set
+ {
+ if (_isFindMyMouseEnabled != value)
+ {
+ _isFindMyMouseEnabled = value;
+
+ GeneralSettingsConfig.Enabled.FindMyMouse = value;
+ OnPropertyChanged(nameof(IsFindMyMouseEnabled));
+
+ OutGoingGeneralSettings outgoing = new OutGoingGeneralSettings(GeneralSettingsConfig);
+ SendConfigMSG(outgoing.ToString());
+
+ // TODO: Implement when this module has properties.
+ // NotifyPropertyChanged();
+ }
+ }
+ }
+
+ private Func SendConfigMSG { get; }
+
+ private bool _isFindMyMouseEnabled;
+ }
+}
diff --git a/src/settings-ui/Microsoft.PowerToys.Settings.UI/Assets/FluentIcons/FluentIconsMouseUtils.png b/src/settings-ui/Microsoft.PowerToys.Settings.UI/Assets/FluentIcons/FluentIconsMouseUtils.png
new file mode 100644
index 0000000000..26f935bea3
Binary files /dev/null and b/src/settings-ui/Microsoft.PowerToys.Settings.UI/Assets/FluentIcons/FluentIconsMouseUtils.png differ
diff --git a/src/settings-ui/Microsoft.PowerToys.Settings.UI/Assets/Modules/MouseUtils.png b/src/settings-ui/Microsoft.PowerToys.Settings.UI/Assets/Modules/MouseUtils.png
new file mode 100644
index 0000000000..96a58da259
Binary files /dev/null and b/src/settings-ui/Microsoft.PowerToys.Settings.UI/Assets/Modules/MouseUtils.png differ
diff --git a/src/settings-ui/Microsoft.PowerToys.Settings.UI/Assets/Modules/OOBE/MouseUtils.gif b/src/settings-ui/Microsoft.PowerToys.Settings.UI/Assets/Modules/OOBE/MouseUtils.gif
new file mode 100644
index 0000000000..8f4a97c0a2
Binary files /dev/null and b/src/settings-ui/Microsoft.PowerToys.Settings.UI/Assets/Modules/OOBE/MouseUtils.gif differ
diff --git a/src/settings-ui/Microsoft.PowerToys.Settings.UI/Microsoft.PowerToys.Settings.UI.csproj b/src/settings-ui/Microsoft.PowerToys.Settings.UI/Microsoft.PowerToys.Settings.UI.csproj
index 32924752b5..a6c6bc44aa 100644
--- a/src/settings-ui/Microsoft.PowerToys.Settings.UI/Microsoft.PowerToys.Settings.UI.csproj
+++ b/src/settings-ui/Microsoft.PowerToys.Settings.UI/Microsoft.PowerToys.Settings.UI.csproj
@@ -155,6 +155,9 @@
OobeKBM.xaml
+
+ OobeMouseUtils.xaml
+
OobeOverview.xaml
@@ -192,6 +195,9 @@
KeyboardManagerPage.xaml
+
+ MouseUtilsPage.xaml
+
PowerLauncherPage.xaml
@@ -227,6 +233,7 @@
+
@@ -239,12 +246,14 @@
+
+
@@ -365,6 +374,10 @@
Designer
MSBuild:Compile
+
+ Designer
+ MSBuild:Compile
+
Designer
MSBuild:Compile
@@ -429,6 +442,10 @@
Designer
MSBuild:Compile
+
+ Designer
+ MSBuild:Compile
+
Designer
MSBuild:Compile
diff --git a/src/settings-ui/Microsoft.PowerToys.Settings.UI/OOBE/Enums/PowerToysModulesEnum.cs b/src/settings-ui/Microsoft.PowerToys.Settings.UI/OOBE/Enums/PowerToysModulesEnum.cs
index 70c65c44bf..6464b97e41 100644
--- a/src/settings-ui/Microsoft.PowerToys.Settings.UI/OOBE/Enums/PowerToysModulesEnum.cs
+++ b/src/settings-ui/Microsoft.PowerToys.Settings.UI/OOBE/Enums/PowerToysModulesEnum.cs
@@ -13,6 +13,7 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Enums
FileExplorer,
ImageResizer,
KBM,
+ MouseUtils,
PowerRename,
Run,
ShortcutGuide,
diff --git a/src/settings-ui/Microsoft.PowerToys.Settings.UI/OOBE/Views/OobeMouseUtils.xaml b/src/settings-ui/Microsoft.PowerToys.Settings.UI/OOBE/Views/OobeMouseUtils.xaml
new file mode 100644
index 0000000000..6fe25099ce
--- /dev/null
+++ b/src/settings-ui/Microsoft.PowerToys.Settings.UI/OOBE/Views/OobeMouseUtils.xaml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/settings-ui/Microsoft.PowerToys.Settings.UI/OOBE/Views/OobeMouseUtils.xaml.cs b/src/settings-ui/Microsoft.PowerToys.Settings.UI/OOBE/Views/OobeMouseUtils.xaml.cs
new file mode 100644
index 0000000000..9bfaf2c445
--- /dev/null
+++ b/src/settings-ui/Microsoft.PowerToys.Settings.UI/OOBE/Views/OobeMouseUtils.xaml.cs
@@ -0,0 +1,45 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.PowerToys.Settings.UI.Library;
+using Microsoft.PowerToys.Settings.UI.OOBE.Enums;
+using Microsoft.PowerToys.Settings.UI.OOBE.ViewModel;
+using Microsoft.PowerToys.Settings.UI.Views;
+using Windows.UI.Xaml.Controls;
+using Windows.UI.Xaml.Navigation;
+
+namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
+{
+ public sealed partial class OobeMouseUtils : Page
+ {
+ public OobePowerToysModule ViewModel { get; set; }
+
+ public OobeMouseUtils()
+ {
+ this.InitializeComponent();
+ ViewModel = new OobePowerToysModule(OobeShellPage.OobeShellHandler.Modules[(int)PowerToysModulesEnum.MouseUtils]);
+ DataContext = ViewModel;
+ }
+
+ private void SettingsLaunchButton_Click(object sender, Windows.UI.Xaml.RoutedEventArgs e)
+ {
+ if (OobeShellPage.OpenMainWindowCallback != null)
+ {
+ OobeShellPage.OpenMainWindowCallback(typeof(MouseUtilsPage));
+ }
+
+ ViewModel.LogOpeningSettingsEvent();
+ }
+
+ protected override void OnNavigatedTo(NavigationEventArgs e)
+ {
+ ViewModel.LogOpeningModuleEvent();
+ }
+
+ protected override void OnNavigatedFrom(NavigationEventArgs e)
+ {
+ ViewModel.LogClosingModuleEvent();
+ }
+ }
+}
diff --git a/src/settings-ui/Microsoft.PowerToys.Settings.UI/OOBE/Views/OobeShellPage.xaml.cs b/src/settings-ui/Microsoft.PowerToys.Settings.UI/OOBE/Views/OobeShellPage.xaml.cs
index a587ec1563..94cb7c082a 100644
--- a/src/settings-ui/Microsoft.PowerToys.Settings.UI/OOBE/Views/OobeShellPage.xaml.cs
+++ b/src/settings-ui/Microsoft.PowerToys.Settings.UI/OOBE/Views/OobeShellPage.xaml.cs
@@ -143,6 +143,18 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
PreviewImageSource = "ms-appx:///Assets/Modules/OOBE/KBM.gif",
Link = "https://aka.ms/PowerToysOverview_KeyboardManager",
});
+ Modules.Insert((int)PowerToysModulesEnum.MouseUtils, new OobePowerToysModule()
+ {
+ ModuleName = loader.GetString("Oobe_MouseUtils"),
+ Tag = "MouseUtils",
+ IsNew = true,
+ Icon = "\uE962",
+ FluentIcon = "ms-appx:///Assets/FluentIcons/FluentIconsMouseUtils.png",
+ Image = "ms-appx:///Assets/Modules/MouseUtils.png",
+ Description = loader.GetString("Oobe_MouseUtils_Description"),
+ PreviewImageSource = "ms-appx:///Assets/Modules/OOBE/MouseUtils.gif",
+ Link = "https://aka.ms/PowerToysOverview_MouseUtilities", // TODO: Add correct link after it's been created.
+ });
Modules.Insert((int)PowerToysModulesEnum.PowerRename, new OobePowerToysModule()
{
ModuleName = loader.GetString("Oobe_PowerRename"),
@@ -228,6 +240,7 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
case "FileExplorer": NavigationFrame.Navigate(typeof(OobeFileExplorer)); break;
case "ShortcutGuide": NavigationFrame.Navigate(typeof(OobeShortcutGuide)); break;
case "VideoConference": NavigationFrame.Navigate(typeof(OobeVideoConference)); break;
+ case "MouseUtils": NavigationFrame.Navigate(typeof(OobeMouseUtils)); break;
}
}
diff --git a/src/settings-ui/Microsoft.PowerToys.Settings.UI/Strings/en-us/Resources.resw b/src/settings-ui/Microsoft.PowerToys.Settings.UI/Strings/en-us/Resources.resw
index ef5c3b3ebe..ade1c0774b 100644
--- a/src/settings-ui/Microsoft.PowerToys.Settings.UI/Strings/en-us/Resources.resw
+++ b/src/settings-ui/Microsoft.PowerToys.Settings.UI/Strings/en-us/Resources.resw
@@ -248,6 +248,10 @@
Keyboard Manager
Product name: Navigation view item name for Keyboard Manager
+
+ Mouse utilities
+ Product name: Navigation view item name for Mouse utilities
+
Navigation closed
Accessibility announcement when the navigation pane collapses
@@ -1275,6 +1279,10 @@ Made with 💗 by Microsoft and the PowerToys community.
Video Conference Mute allows users to quickly mute the microphone and turn off the camera while on a conference call with a single keystroke, regardless of what application has focus on your computer.
+
+ A collection of utilities to enhance your mouse.
+ Mouse as in the hardware peripheral
+
Microsoft PowerToys is a set of utilities for power users to tune and streamline their Windows experience for greater productivity.
@@ -1567,6 +1575,10 @@ From there, simply click on a Markdown file, PDF file or SVG icon in the File Ex
Learn more about Keyboard Manager
Keyboard Manager is a product name, do not loc
+
+ Learn more about Mouse utilities
+ Mouse utilities is a product name, do not loc
+
Learn more about File Explorer add-ons
File Explorer is a product name, do not loc
@@ -1591,6 +1603,18 @@ From there, simply click on a Markdown file, PDF file or SVG icon in the File Ex
File Explorer add-ons
Do not localize this string
+
+ Mouse utilities
+ Mouse as in the hardware peripheral
+
+
+ Find My Mouse
+ Mouse as in the hardware peripheral
+
+
+ Click twice on the Left Control key to focus on your mouse.
+ Mouse as in the hardware peripheral. Key as in a keyboard key
+
Launch PowerToys Run
@@ -1621,6 +1645,9 @@ From there, simply click on a Markdown file, PDF file or SVG icon in the File Ex
Attribution
+
+ Attribution
+
Attribution
@@ -1661,4 +1688,18 @@ From there, simply click on a Markdown file, PDF file or SVG icon in the File Ex
Time before returning to the previous awakeness state
+
+ Mouse utilities
+
+
+ A collection of mouse utilities.
+
+
+ Enable Find My Mouse
+ "Find My Mouse" is the name of the utility.
+
+
+ Press the Left Control key twice to focus the mouse pointer.
+ "Left Control" is a keyboard key.
+
\ No newline at end of file
diff --git a/src/settings-ui/Microsoft.PowerToys.Settings.UI/Views/MouseUtilsPage.xaml b/src/settings-ui/Microsoft.PowerToys.Settings.UI/Views/MouseUtilsPage.xaml
new file mode 100644
index 0000000000..64627b24c6
--- /dev/null
+++ b/src/settings-ui/Microsoft.PowerToys.Settings.UI/Views/MouseUtilsPage.xaml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/settings-ui/Microsoft.PowerToys.Settings.UI/Views/MouseUtilsPage.xaml.cs b/src/settings-ui/Microsoft.PowerToys.Settings.UI/Views/MouseUtilsPage.xaml.cs
new file mode 100644
index 0000000000..bbc294fad9
--- /dev/null
+++ b/src/settings-ui/Microsoft.PowerToys.Settings.UI/Views/MouseUtilsPage.xaml.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.PowerToys.Settings.UI.Library;
+using Microsoft.PowerToys.Settings.UI.Library.ViewModels;
+using Windows.UI.Xaml.Controls;
+
+namespace Microsoft.PowerToys.Settings.UI.Views
+{
+ public sealed partial class MouseUtilsPage : Page
+ {
+ private MouseUtilsViewModel ViewModel { get; set; }
+
+ public MouseUtilsPage()
+ {
+ var settingsUtils = new SettingsUtils();
+ ViewModel = new MouseUtilsViewModel(SettingsRepository.GetInstance(settingsUtils), ShellPage.SendDefaultIPCMessage);
+ DataContext = ViewModel;
+ InitializeComponent();
+ }
+ }
+}
diff --git a/src/settings-ui/Microsoft.PowerToys.Settings.UI/Views/ShellPage.xaml b/src/settings-ui/Microsoft.PowerToys.Settings.UI/Views/ShellPage.xaml
index 187368167b..d76f7dcc50 100644
--- a/src/settings-ui/Microsoft.PowerToys.Settings.UI/Views/ShellPage.xaml
+++ b/src/settings-ui/Microsoft.PowerToys.Settings.UI/Views/ShellPage.xaml
@@ -95,6 +95,14 @@
+
+
+
+
+
+
diff --git a/src/settings-ui/PowerToys.Settings/Program.cs b/src/settings-ui/PowerToys.Settings/Program.cs
index 90d49b7f37..e10131d970 100644
--- a/src/settings-ui/PowerToys.Settings/Program.cs
+++ b/src/settings-ui/PowerToys.Settings/Program.cs
@@ -68,6 +68,7 @@ namespace PowerToys.Settings
case "Run": app.StartupPage = typeof(Microsoft.PowerToys.Settings.UI.Views.PowerLauncherPage); break;
case "ImageResizer": app.StartupPage = typeof(Microsoft.PowerToys.Settings.UI.Views.ImageResizerPage); break;
case "KBM": app.StartupPage = typeof(Microsoft.PowerToys.Settings.UI.Views.KeyboardManagerPage); break;
+ case "MouseUtils": app.StartupPage = typeof(Microsoft.PowerToys.Settings.UI.Views.MouseUtilsPage); break;
case "PowerRename": app.StartupPage = typeof(Microsoft.PowerToys.Settings.UI.Views.PowerRenamePage); break;
case "FileExplorer": app.StartupPage = typeof(Microsoft.PowerToys.Settings.UI.Views.PowerPreviewPage); break;
case "ShortcutGuide": app.StartupPage = typeof(Microsoft.PowerToys.Settings.UI.Views.ShortcutGuidePage); break;