From 2d5276f74289630a32926a67bc783ab859fc1f2b Mon Sep 17 00:00:00 2001 From: Jaime Bernardo Date: Mon, 22 Nov 2021 13:31:31 +0000 Subject: [PATCH] Mouse Utils - Mouse Highlighter (#14496) * New PowerToys template * Add CppWinRt to empty PowerToy * Add Settings reference to empty PowerToy * Use proper output dir * Proper WindowsTargetPlatformVersion * Add filters to vcxproj * Proper resource file generation * Add MouseHighlighter proof of concept code * Abstract implementation into a struct * Enable module * Disable module * Add enable module to settings page * Can change the hotkey in settings * Remove remaining boilerplate code * Add logging * Add telemetry * Add Oobe entry * Add installer instructions * Add dll to pipelines * fix spellchecker * Add more configurability * Make settings a bit prettier * Fix spellchecker * Fix wrong default fade timers * Fix user facing strings * Tweak default duration values * Fix to appear in every virtual desktop * [Mouse Highlighter] Show highlight on mouse drag (#14529) * [Mouse Highlighter]show pointer on mouse drag * fix spellchecker * [MU] UI tweaks (#14544) * UI tweaks * Update Resources.resw * Updated text strings * fix spellcheck Co-authored-by: Laute * tweak default values * PR feedback: use wstring_view * PR feedback: Log error on json error * PR feedback: don't throw 1 * PR feedback: fix copy-pasta leftColor->rightColor * PR feedback:Add another error message on exception * PR feedback: add todo to use commons/utils/json.h Co-authored-by: Niels Laute Co-authored-by: Laute --- .github/actions/spell-check/expect.txt | 3 + .pipelines/pipeline.user.windows.yml | 1 + PowerToys.sln | 9 + doc/images/icons/Find My Mouse.png | Bin 0 -> 21427 bytes doc/images/icons/Mouse Highlighter.png | Bin 0 -> 17318 bytes installer/PowerToysSetup/Product.wxs | 4 + src/common/logger/logger_settings.h | 1 + .../MouseHighlighter/Directory.Build.targets | 5 + .../MouseHighlighter/MouseHighlighter.base.rc | 40 ++ .../MouseHighlighter/MouseHighlighter.cpp | 443 ++++++++++++++++++ .../MouseHighlighter/MouseHighlighter.h | 24 + .../MouseHighlighter/MouseHighlighter.vcxproj | 145 ++++++ .../MouseHighlighter.vcxproj.filters | 62 +++ .../MouseUtils/MouseHighlighter/dllmain.cpp | 323 +++++++++++++ .../MouseHighlighter/packages.config | 4 + .../MouseUtils/MouseHighlighter/pch.cpp | 1 + src/modules/MouseUtils/MouseHighlighter/pch.h | 22 + .../MouseHighlighter/resource.base.h | 14 + .../MouseUtils/MouseHighlighter/trace.cpp | 40 ++ .../MouseUtils/MouseHighlighter/trace.h | 14 + src/runner/main.cpp | 3 +- .../EnabledModules.cs | 16 + .../Helpers/SettingsUtilities.cs | 39 ++ .../MouseHighlighterProperties.cs | 43 ++ .../MouseHighlighterSettings.cs | 35 ++ .../MouseHighlighterSettingsIPCMessage.cs | 29 ++ .../SndMouseHighlighterSettings.cs | 29 ++ .../ViewModels/FancyZonesViewModel.cs | 28 +- .../ViewModels/MouseUtilsViewModel.cs | 194 +++++++- .../FluentIcons/FluentIconsFindMyMouse.png | Bin 0 -> 2701 bytes .../FluentIconsMouseHighlighter.png | Bin 0 -> 2060 bytes .../Microsoft.PowerToys.Settings.UI.csproj | 3 + .../OOBE/Views/OobeMouseUtils.xaml | 4 + .../Strings/en-us/Resources.resw | 71 ++- .../Views/MouseUtilsPage.xaml | 125 ++++- .../Views/MouseUtilsPage.xaml.cs | 2 +- 36 files changed, 1726 insertions(+), 50 deletions(-) create mode 100644 doc/images/icons/Find My Mouse.png create mode 100644 doc/images/icons/Mouse Highlighter.png create mode 100644 src/modules/MouseUtils/MouseHighlighter/Directory.Build.targets create mode 100644 src/modules/MouseUtils/MouseHighlighter/MouseHighlighter.base.rc create mode 100644 src/modules/MouseUtils/MouseHighlighter/MouseHighlighter.cpp create mode 100644 src/modules/MouseUtils/MouseHighlighter/MouseHighlighter.h create mode 100644 src/modules/MouseUtils/MouseHighlighter/MouseHighlighter.vcxproj create mode 100644 src/modules/MouseUtils/MouseHighlighter/MouseHighlighter.vcxproj.filters create mode 100644 src/modules/MouseUtils/MouseHighlighter/dllmain.cpp create mode 100644 src/modules/MouseUtils/MouseHighlighter/packages.config create mode 100644 src/modules/MouseUtils/MouseHighlighter/pch.cpp create mode 100644 src/modules/MouseUtils/MouseHighlighter/pch.h create mode 100644 src/modules/MouseUtils/MouseHighlighter/resource.base.h create mode 100644 src/modules/MouseUtils/MouseHighlighter/trace.cpp create mode 100644 src/modules/MouseUtils/MouseHighlighter/trace.h create mode 100644 src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/Helpers/SettingsUtilities.cs create mode 100644 src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/MouseHighlighterProperties.cs create mode 100644 src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/MouseHighlighterSettings.cs create mode 100644 src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/MouseHighlighterSettingsIPCMessage.cs create mode 100644 src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/SndMouseHighlighterSettings.cs create mode 100644 src/settings-ui/Microsoft.PowerToys.Settings.UI/Assets/FluentIcons/FluentIconsFindMyMouse.png create mode 100644 src/settings-ui/Microsoft.PowerToys.Settings.UI/Assets/FluentIcons/FluentIconsMouseHighlighter.png diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index d1b2de49dde1..883ff38071f2 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -763,8 +763,10 @@ hglobal hhk HHmmss HHOOK +hhx HICON HIDEWINDOW +highlighter HIMAGELIST himl hinst @@ -2084,6 +2086,7 @@ Switchbetweenvirtualdesktops SWP swprintf SWRESTORE +swscanf SYMED SYMOPT SYNCMFT diff --git a/.pipelines/pipeline.user.windows.yml b/.pipelines/pipeline.user.windows.yml index 22f6cf723ae6..b727524f29a8 100644 --- a/.pipelines/pipeline.user.windows.yml +++ b/.pipelines/pipeline.user.windows.yml @@ -171,6 +171,7 @@ build: - 'modules\launcher\Wox.Infrastructure.dll' - 'modules\launcher\Wox.Plugin.dll' - 'modules\MouseUtils\FindMyMouse.dll' + - 'modules\MouseUtils\MouseHighlighter.dll' - 'modules\PowerRename\PowerRenameExt.dll' - 'modules\PowerRename\PowerRenameUILib.dll' - 'modules\PowerRename\PowerRename.exe' diff --git a/PowerToys.sln b/PowerToys.sln index e185f09f71ed..5b023cf4370e 100644 --- a/PowerToys.sln +++ b/PowerToys.sln @@ -371,6 +371,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "MouseUtils", "MouseUtils", EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "FindMyMouse", "src\modules\MouseUtils\FindMyMouse\FindMyMouse.vcxproj", "{E94FD11C-0591-456F-899F-EFC0CA548336}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "MouseHighlighter", "src\modules\MouseUtils\MouseHighlighter\MouseHighlighter.vcxproj", "{782A61BE-9D85-4081-B35C-1CCC9DCC1E88}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x64 = Debug|x64 @@ -986,6 +988,12 @@ Global {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 + {782A61BE-9D85-4081-B35C-1CCC9DCC1E88}.Debug|x64.ActiveCfg = Debug|x64 + {782A61BE-9D85-4081-B35C-1CCC9DCC1E88}.Debug|x64.Build.0 = Debug|x64 + {782A61BE-9D85-4081-B35C-1CCC9DCC1E88}.Debug|x86.ActiveCfg = Debug|x64 + {782A61BE-9D85-4081-B35C-1CCC9DCC1E88}.Release|x64.ActiveCfg = Release|x64 + {782A61BE-9D85-4081-B35C-1CCC9DCC1E88}.Release|x64.Build.0 = Release|x64 + {782A61BE-9D85-4081-B35C-1CCC9DCC1E88}.Release|x86.ActiveCfg = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1105,6 +1113,7 @@ Global {4642D596-723F-4BFC-894C-46811219AC4A} = {89E20BCE-EB9C-46C8-8B50-E01A82E6FDC3} {322566EF-20DC-43A6-B9F8-616AF942579A} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC} {E94FD11C-0591-456F-899F-EFC0CA548336} = {322566EF-20DC-43A6-B9F8-616AF942579A} + {782A61BE-9D85-4081-B35C-1CCC9DCC1E88} = {322566EF-20DC-43A6-B9F8-616AF942579A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0} diff --git a/doc/images/icons/Find My Mouse.png b/doc/images/icons/Find My Mouse.png new file mode 100644 index 0000000000000000000000000000000000000000..5098e2237abf16c58fa4fa02069b5d47073b7242 GIT binary patch literal 21427 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 literal 0 HcmV?d00001 diff --git a/doc/images/icons/Mouse Highlighter.png b/doc/images/icons/Mouse Highlighter.png new file mode 100644 index 0000000000000000000000000000000000000000..789b0b1c04907068f1146dda344c991d1caf2ef5 GIT binary patch literal 17318 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 REW7{!002ovPDHLkV1oX)_geq} literal 0 HcmV?d00001 diff --git a/installer/PowerToysSetup/Product.wxs b/installer/PowerToysSetup/Product.wxs index 2b11399842a5..0cbf25227fe6 100644 --- a/installer/PowerToysSetup/Product.wxs +++ b/installer/PowerToysSetup/Product.wxs @@ -657,6 +657,9 @@ + + + @@ -971,6 +974,7 @@ + diff --git a/src/common/logger/logger_settings.h b/src/common/logger/logger_settings.h index 1015ef9f5740..b88af2bc77a1 100644 --- a/src/common/logger/logger_settings.h +++ b/src/common/logger/logger_settings.h @@ -26,6 +26,7 @@ struct LogSettings 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 std::string mouseHighlighterLoggerName = "mouse-highlighter"; inline const static std::string powerRenameLoggerName = "powerrename"; inline const static int retention = 30; std::wstring logLevel; diff --git a/src/modules/MouseUtils/MouseHighlighter/Directory.Build.targets b/src/modules/MouseUtils/MouseHighlighter/Directory.Build.targets new file mode 100644 index 000000000000..7e1362ac6066 --- /dev/null +++ b/src/modules/MouseUtils/MouseHighlighter/Directory.Build.targets @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/modules/MouseUtils/MouseHighlighter/MouseHighlighter.base.rc b/src/modules/MouseUtils/MouseHighlighter/MouseHighlighter.base.rc new file mode 100644 index 000000000000..0bcdeca2ef58 --- /dev/null +++ b/src/modules/MouseUtils/MouseHighlighter/MouseHighlighter.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/MouseHighlighter/MouseHighlighter.cpp b/src/modules/MouseUtils/MouseHighlighter/MouseHighlighter.cpp new file mode 100644 index 000000000000..8b8c83c1fa77 --- /dev/null +++ b/src/modules/MouseUtils/MouseHighlighter/MouseHighlighter.cpp @@ -0,0 +1,443 @@ +// MouseHighlighter.cpp : Defines the entry point for the application. +// + +#include "pch.h" +#include "MouseHighlighter.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 + +struct Highlighter +{ + bool MyRegisterClass(HINSTANCE hInstance); + static Highlighter* instance; + void Terminate(); + void SwitchActivationMode(); + void ApplySettings(MouseHighlighterSettings settings); + +private: + enum class MouseButton + { + Left, + Right + }; + + void DestroyHighlighter(); + static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) noexcept; + void StartDrawing(); + void StopDrawing(); + bool CreateHighlighter(); + void AddDrawingPoint(MouseButton button); + void UpdateDrawingPointPosition(MouseButton button); + void StartDrawingPointFading(MouseButton button); + void ClearDrawing(); + HHOOK m_mouseHook = NULL; + static LRESULT CALLBACK MouseHookProc(int nCode, WPARAM wParam, LPARAM lParam) noexcept; + + static constexpr auto m_className = L"MouseHighlighter"; + static constexpr auto m_windowTitle = L"MouseHighlighter"; + HWND m_hwndOwner = NULL; + HWND m_hwnd = NULL; + HINSTANCE m_hinstance = NULL; + static constexpr DWORD WM_SWITCH_ACTIVATION_MODE = WM_APP; + + winrt::DispatcherQueueController m_dispatcherQueueController{ nullptr }; + winrt::Compositor m_compositor{ nullptr }; + winrt::Desktop::DesktopWindowTarget m_target{ nullptr }; + winrt::ContainerVisual m_root{ nullptr }; + winrt::LayerVisual m_layer{ nullptr }; + winrt::ShapeVisual m_shape{ nullptr }; + + winrt::CompositionSpriteShape m_leftPointer{ nullptr }; + winrt::CompositionSpriteShape m_rightPointer{ nullptr }; + bool m_leftButtonPressed = false; + bool m_rightButtonPressed = false; + + bool m_visible = false; + + // Possible configurable settings + float m_radius = MOUSE_HIGHLIGHTER_DEFAULT_RADIUS; + + int m_fadeDelay_ms = MOUSE_HIGHLIGHTER_DEFAULT_DELAY_MS; + int m_fadeDuration_ms = MOUSE_HIGHLIGHTER_DEFAULT_DURATION_MS; + + winrt::Windows::UI::Color m_leftClickColor = MOUSE_HIGHLIGHTER_DEFAULT_LEFT_BUTTON_COLOR; + winrt::Windows::UI::Color m_rightClickColor = MOUSE_HIGHLIGHTER_DEFAULT_RIGHT_BUTTON_COLOR; +}; + +Highlighter* Highlighter::instance = nullptr; + +bool Highlighter::CreateHighlighter() +{ + 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; + + // Create visual root + m_root = m_compositor.CreateContainerVisual(); + m_root.RelativeSizeAdjustment({ 1.0f, 1.0f }); + m_target.Root(m_root); + + // Create the shapes container visual and add it to root. + m_shape = m_compositor.CreateShapeVisual(); + m_shape.RelativeSizeAdjustment({ 1.0f, 1.0f }); + m_root.Children().InsertAtTop(m_shape); + + return true; + } catch (...) + { + return false; + } +} + + +void Highlighter::AddDrawingPoint(MouseButton button) +{ + POINT pt; + + // Applies DPIs. + GetCursorPos(&pt); + + // Converts to client area of the Windows. + ScreenToClient(m_hwnd, &pt); + + // Create circle and add it. + auto circleGeometry = m_compositor.CreateEllipseGeometry(); + circleGeometry.Radius({ m_radius, m_radius }); + auto circleShape = m_compositor.CreateSpriteShape(circleGeometry); + circleShape.Offset({ (float)pt.x, (float)pt.y }); + if (button == MouseButton::Left) + { + circleShape.FillBrush(m_compositor.CreateColorBrush(m_leftClickColor)); + m_leftPointer = circleShape; + } + else + { + //right + circleShape.FillBrush(m_compositor.CreateColorBrush(m_rightClickColor)); + m_rightPointer = circleShape; + } + m_shape.Shapes().Append(circleShape); + + // TODO: We're leaking shapes for long drawing sessions. + // Perhaps add a task to the Dispatcher every X circles to clean up. + + // Get back on top in case other Window is now the topmost. + SetWindowPos(m_hwnd, HWND_TOPMOST, GetSystemMetrics(SM_XVIRTUALSCREEN), GetSystemMetrics(SM_YVIRTUALSCREEN), + GetSystemMetrics(SM_CXVIRTUALSCREEN), GetSystemMetrics(SM_CYVIRTUALSCREEN), 0); +} + +void Highlighter::UpdateDrawingPointPosition(MouseButton button) +{ + POINT pt; + + // Applies DPIs. + GetCursorPos(&pt); + + // Converts to client area of the Windows. + ScreenToClient(m_hwnd, &pt); + + if (button == MouseButton::Left) + { + m_leftPointer.Offset({ (float)pt.x, (float)pt.y }); + } + else + { + //right + m_rightPointer.Offset({ (float)pt.x, (float)pt.y }); + } +} +void Highlighter::StartDrawingPointFading(MouseButton button) +{ + winrt::Windows::UI::Composition::CompositionSpriteShape circleShape{ nullptr }; + if (button == MouseButton::Left) + { + circleShape = m_leftPointer; + } + else + { + //right + circleShape = m_rightPointer; + } + + auto brushColor = circleShape.FillBrush().as().Color(); + + // Animate opacity to simulate a fade away effect. + auto animation = m_compositor.CreateColorKeyFrameAnimation(); + animation.InsertKeyFrame(1, winrt::Windows::UI::ColorHelper::FromArgb(0, brushColor.R, brushColor.G, brushColor.B)); + using timeSpan = std::chrono::duration>; + std::chrono::milliseconds duration(m_fadeDuration_ms); + std::chrono::milliseconds delay(m_fadeDelay_ms); + animation.Duration(timeSpan(duration)); + animation.DelayTime(timeSpan(delay)); + + circleShape.FillBrush().StartAnimation(L"Color", animation); +} + + +void Highlighter::ClearDrawing() +{ + m_shape.Shapes().Clear(); +} + +LRESULT CALLBACK Highlighter::MouseHookProc(int nCode, WPARAM wParam, LPARAM lParam) noexcept +{ + if (nCode >= 0) + { + MSLLHOOKSTRUCT* hookData = (MSLLHOOKSTRUCT*)lParam; + switch (wParam) + { + case WM_LBUTTONDOWN: + instance->AddDrawingPoint(MouseButton::Left); + instance->m_leftButtonPressed = true; + break; + case WM_RBUTTONDOWN: + instance->AddDrawingPoint(MouseButton::Right); + instance->m_rightButtonPressed = true; + break; + case WM_MOUSEMOVE: + if (instance->m_leftButtonPressed) + { + instance->UpdateDrawingPointPosition(MouseButton::Left); + } + if (instance->m_rightButtonPressed) + { + instance->UpdateDrawingPointPosition(MouseButton::Right); + } + break; + case WM_LBUTTONUP: + if (instance->m_leftButtonPressed) + { + instance->StartDrawingPointFading(MouseButton::Left); + instance->m_leftButtonPressed = false; + } + break; + case WM_RBUTTONUP: + if (instance->m_rightButtonPressed) + { + instance->StartDrawingPointFading(MouseButton::Right); + instance->m_rightButtonPressed = false; + } + break; + default: + break; + } + } + return CallNextHookEx(0, nCode, wParam, lParam); +} + + +void Highlighter::StartDrawing() +{ + Logger::info("Starting draw mode."); + Trace::StartHighlightingSession(); + m_visible = true; + SetWindowPos(m_hwnd, HWND_TOPMOST, GetSystemMetrics(SM_XVIRTUALSCREEN), GetSystemMetrics(SM_YVIRTUALSCREEN), + GetSystemMetrics(SM_CXVIRTUALSCREEN), GetSystemMetrics(SM_CYVIRTUALSCREEN), 0); + ClearDrawing(); + ShowWindow(m_hwnd, SW_SHOWNOACTIVATE); + m_mouseHook = SetWindowsHookEx(WH_MOUSE_LL, MouseHookProc, m_hinstance, 0); +} + +void Highlighter::StopDrawing() +{ + Logger::info("Stopping draw mode."); + m_visible = false; + m_leftButtonPressed = false; + m_rightButtonPressed = false; + m_leftPointer = nullptr; + m_rightPointer = nullptr; + ShowWindow(m_hwnd, SW_HIDE); + UnhookWindowsHookEx(m_mouseHook); + ClearDrawing(); + m_mouseHook = NULL; +} + +void Highlighter::SwitchActivationMode() +{ + PostMessage(m_hwnd, WM_SWITCH_ACTIVATION_MODE, 0, 0); +} + +void Highlighter::ApplySettings(MouseHighlighterSettings settings) { + m_radius = (float)settings.radius; + m_fadeDelay_ms = settings.fadeDelayMs; + m_fadeDuration_ms = settings.fadeDurationMs; + m_leftClickColor = settings.leftButtonColor; + m_rightClickColor = settings.rightButtonColor; +} + +void Highlighter::DestroyHighlighter() +{ + StopDrawing(); + PostQuitMessage(0); +} + +LRESULT CALLBACK Highlighter::WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) noexcept +{ + switch (message) + { + case WM_NCCREATE: + instance->m_hwnd = hWnd; + return DefWindowProc(hWnd, message, wParam, lParam); + case WM_CREATE: + return instance->CreateHighlighter() ? 0 : -1; + case WM_NCHITTEST: + return HTTRANSPARENT; + case WM_SWITCH_ACTIVATION_MODE: + if (instance->m_visible) + { + instance->StopDrawing(); + } + else + { + instance->StartDrawing(); + } + break; + case WM_DESTROY: + instance->DestroyHighlighter(); + break; + default: + return DefWindowProc(hWnd, message, wParam, lParam); + } + return 0; +} + +bool Highlighter::MyRegisterClass(HINSTANCE hInstance) +{ + WNDCLASS wc{}; + + m_hinstance = hInstance; + + SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); + if (!GetClassInfoW(hInstance, m_className, &wc)) + { + wc.lpfnWndProc = WndProc; + wc.hInstance = hInstance; + wc.hIcon = LoadIcon(hInstance, IDI_APPLICATION); + wc.hCursor = LoadCursor(nullptr, IDC_ARROW); + wc.hbrBackground = (HBRUSH)GetStockObject(NULL_BRUSH); + wc.lpszClassName = m_className; + + if (!RegisterClassW(&wc)) + { + return false; + } + } + + m_hwndOwner = CreateWindow(L"static", nullptr, WS_POPUP, 0, 0, 0, 0, nullptr, nullptr, hInstance, nullptr); + + DWORD exStyle = WS_EX_TRANSPARENT | WS_EX_LAYERED | WS_EX_NOREDIRECTIONBITMAP | WS_EX_TOOLWINDOW; + return CreateWindowExW(exStyle, m_className, m_windowTitle, WS_POPUP, + CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, m_hwndOwner, nullptr, hInstance, nullptr) != nullptr; +} + +void Highlighter::Terminate() +{ + auto dispatcherQueue = m_dispatcherQueueController.DispatcherQueue(); + bool enqueueSucceeded = dispatcherQueue.TryEnqueue([=]() { + DestroyWindow(m_hwndOwner); + }); + if (!enqueueSucceeded) + { + Logger::error("Couldn't enqueue message to destroy the window."); + } +} + +#pragma region MouseHighlighter_API + +void MouseHighlighterApplySettings(MouseHighlighterSettings settings) +{ + if (Highlighter::instance != nullptr) + { + Logger::info("Applying settings."); + Highlighter::instance->ApplySettings(settings); + } +} + +void MouseHighlighterSwitch() +{ + if (Highlighter::instance != nullptr) + { + Logger::info("Switching activation mode."); + Highlighter::instance->SwitchActivationMode(); + } +} + +void MouseHighlighterDisable() +{ + if (Highlighter::instance != nullptr) + { + Logger::info("Terminating the highlighter instance."); + Highlighter::instance->Terminate(); + } +} + +bool MouseHighlighterIsEnabled() +{ + return (Highlighter::instance != nullptr); +} + +int MouseHighlighterMain(HINSTANCE hInstance, MouseHighlighterSettings settings) +{ + Logger::info("Starting a highlighter instance."); + if (Highlighter::instance != nullptr) + { + Logger::error("A highlighter instance was still working when trying to start a new one."); + return 0; + } + + // Perform application initialization: + Highlighter highlighter; + Highlighter::instance = &highlighter; + highlighter.ApplySettings(settings); + if (!highlighter.MyRegisterClass(hInstance)) + { + Logger::error("Couldn't initialize a highlighter instance."); + Highlighter::instance = nullptr; + return FALSE; + } + Logger::info("Initialized the highlighter instance."); + + MSG msg; + + // Main message loop: + while (GetMessage(&msg, nullptr, 0, 0)) + { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + + Logger::info("Mouse highlighter message loop ended."); + Highlighter::instance = nullptr; + + return (int)msg.wParam; +} + +#pragma endregion MouseHighlighter_API diff --git a/src/modules/MouseUtils/MouseHighlighter/MouseHighlighter.h b/src/modules/MouseUtils/MouseHighlighter/MouseHighlighter.h new file mode 100644 index 000000000000..eb1948bd090f --- /dev/null +++ b/src/modules/MouseUtils/MouseHighlighter/MouseHighlighter.h @@ -0,0 +1,24 @@ +#pragma once +#include "pch.h" + +constexpr int MOUSE_HIGHLIGHTER_DEFAULT_OPACITY = 160; +const winrt::Windows::UI::Color MOUSE_HIGHLIGHTER_DEFAULT_LEFT_BUTTON_COLOR = winrt::Windows::UI::ColorHelper::FromArgb(MOUSE_HIGHLIGHTER_DEFAULT_OPACITY, 255, 255, 0); +const winrt::Windows::UI::Color MOUSE_HIGHLIGHTER_DEFAULT_RIGHT_BUTTON_COLOR = winrt::Windows::UI::ColorHelper::FromArgb(MOUSE_HIGHLIGHTER_DEFAULT_OPACITY, 0, 0, 255); +constexpr int MOUSE_HIGHLIGHTER_DEFAULT_RADIUS = 20; +constexpr int MOUSE_HIGHLIGHTER_DEFAULT_DELAY_MS = 500; +constexpr int MOUSE_HIGHLIGHTER_DEFAULT_DURATION_MS = 250; + +struct MouseHighlighterSettings +{ + winrt::Windows::UI::Color leftButtonColor = MOUSE_HIGHLIGHTER_DEFAULT_LEFT_BUTTON_COLOR; + winrt::Windows::UI::Color rightButtonColor = MOUSE_HIGHLIGHTER_DEFAULT_RIGHT_BUTTON_COLOR; + int radius = MOUSE_HIGHLIGHTER_DEFAULT_RADIUS; + int fadeDelayMs = MOUSE_HIGHLIGHTER_DEFAULT_DELAY_MS; + int fadeDurationMs = MOUSE_HIGHLIGHTER_DEFAULT_DURATION_MS; +}; + +int MouseHighlighterMain(HINSTANCE hinst, MouseHighlighterSettings settings); +void MouseHighlighterDisable(); +bool MouseHighlighterIsEnabled(); +void MouseHighlighterSwitch(); +void MouseHighlighterApplySettings(MouseHighlighterSettings settings); diff --git a/src/modules/MouseUtils/MouseHighlighter/MouseHighlighter.vcxproj b/src/modules/MouseUtils/MouseHighlighter/MouseHighlighter.vcxproj new file mode 100644 index 000000000000..5c127f852519 --- /dev/null +++ b/src/modules/MouseUtils/MouseHighlighter/MouseHighlighter.vcxproj @@ -0,0 +1,145 @@ + + + + + + Debug + x64 + + + Release + x64 + + + + 15.0 + {782a61be-9d85-4081-b35c-1ccc9dcc1e88} + Win32Proj + MouseHighlighter + 10.0.18362.0 + MouseHighlighter + + + + 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/MouseHighlighter/MouseHighlighter.vcxproj.filters b/src/modules/MouseUtils/MouseHighlighter/MouseHighlighter.vcxproj.filters new file mode 100644 index 000000000000..ab12bcf6d6ef --- /dev/null +++ b/src/modules/MouseUtils/MouseHighlighter/MouseHighlighter.vcxproj.filters @@ -0,0 +1,62 @@ + + + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + + + Header Files + + + Header Files + + + Generated Files + + + Header Files + + + + + + Resource Files + + + Resource Files + + + + + {b012a2c8-5ccb-47fc-9429-4ebf877928e2} + cpp;c;cc;cxx;c++;def;odl;idl;hpj;bat;asm;asmx + + + {c8345550-9836-40a0-b473-0f4bf6129568} + h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd + + + {7934ee5b-8427-486d-9324-73b6bcf60eed} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + {e1083d6b-b856-42a6-bd1f-1710e96170ba} + + + + + Generated Files + + + \ No newline at end of file diff --git a/src/modules/MouseUtils/MouseHighlighter/dllmain.cpp b/src/modules/MouseUtils/MouseHighlighter/dllmain.cpp new file mode 100644 index 000000000000..78858cbed2ef --- /dev/null +++ b/src/modules/MouseUtils/MouseHighlighter/dllmain.cpp @@ -0,0 +1,323 @@ +#include "pch.h" +#include +#include +#include "trace.h" +#include "MouseHighlighter.h" + +namespace +{ + const wchar_t JSON_KEY_PROPERTIES[] = L"properties"; + const wchar_t JSON_KEY_VALUE[] = L"value"; + const wchar_t JSON_KEY_ACTIVATION_SHORTCUT[] = L"activation_shortcut"; + const wchar_t JSON_KEY_LEFT_BUTTON_CLICK_COLOR[] = L"left_button_click_color"; + const wchar_t JSON_KEY_RIGHT_BUTTON_CLICK_COLOR[] = L"right_button_click_color"; + const wchar_t JSON_KEY_HIGHLIGHT_OPACITY[] = L"highlight_opacity"; + const wchar_t JSON_KEY_HIGHLIGHT_RADIUS[] = L"highlight_radius"; + const wchar_t JSON_KEY_HIGHLIGHT_FADE_DELAY_MS[] = L"highlight_fade_delay_ms"; + const wchar_t JSON_KEY_HIGHLIGHT_FADE_DURATION_MS[] = L"highlight_fade_duration_ms"; +} + +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"MouseHighlighter"; +// Add a description that will we shown in the module settings page. +const static wchar_t* MODULE_DESC = L""; + +// Implement the PowerToy Module Interface and all the required methods. +class MouseHighlighter : public PowertoyModuleIface +{ +private: + // The PowerToy state. + bool m_enabled = false; + + // Hotkey to invoke the module + HotkeyEx m_hotkey; + + // Mouse Highlighter specific settings + MouseHighlighterSettings m_highlightSettings; + + // helper function to get the RGB from a #FFFFFF string. + bool checkValidRGB(std::wstring_view hex, uint8_t* R, uint8_t* G, uint8_t* B) + { + if (hex.length() != 7) + return false; + hex = hex.substr(1, 6); // remove # + for (auto& c : hex) + { + if (!((c >= '0' && c <= '9') || (c >= 'A' && c <= 'F'))) + { + return false; + } + } + if (swscanf_s(hex.data(), L"%2hhx%2hhx%2hhx", R, G, B) != 3) + { + return false; + } + return true; + } + +public: + // Constructor + MouseHighlighter() + { + LoggerHelpers::init_logger(MODULE_NAME, L"ModuleInterface", LogSettings::mouseHighlighterLoggerName); + 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); + PowerToysSettings::Settings settings(hinstance, get_name()); + 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()); + + parse_settings(values); + + MouseHighlighterApplySettings(m_highlightSettings); + } + catch (std::exception&) + { + Logger::error("Invalid json when trying to parse Mouse Highlighter settings json."); + } + } + + // Enable the powertoy + virtual void enable() + { + m_enabled = true; + Trace::EnableMouseHighlighter(true); + std::thread([=]() { MouseHighlighterMain(m_hModule, m_highlightSettings); }).detach(); + } + + // Disable the powertoy + virtual void disable() + { + m_enabled = false; + Trace::EnableMouseHighlighter(false); + MouseHighlighterDisable(); + } + + // Returns if the powertoys is enabled + virtual bool is_enabled() override + { + return m_enabled; + } + + virtual std::optional GetHotkeyEx() override + { + return m_hotkey; + } + + virtual void OnHotkeyEx() override + { + MouseHighlighterSwitch(); + } + + // Load the settings file. + void init_settings() + { + try + { + // Load and parse the settings file for this PowerToy. + PowerToysSettings::PowerToyValues settings = + PowerToysSettings::PowerToyValues::load_from_settings_file(MouseHighlighter::get_key()); + parse_settings(settings); + } + catch (std::exception&) + { + Logger::error("Invalid json when trying to load the Mouse Highlighter settings json from file."); + } + } + + void parse_settings(PowerToysSettings::PowerToyValues& settings) + { + // TODO: refactor to use common/utils/json.h instead + auto settingsObject = settings.get_raw_json(); + MouseHighlighterSettings highlightSettings; + if (settingsObject.GetView().Size()) + { + try + { + // Parse HotKey + auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_ACTIVATION_SHORTCUT); + auto hotkey = PowerToysSettings::HotkeyObject::from_json(jsonPropertiesObject); + m_hotkey = HotkeyEx(); + if (hotkey.win_pressed()) + { + m_hotkey.modifiersMask |= MOD_WIN; + } + + if (hotkey.ctrl_pressed()) + { + m_hotkey.modifiersMask |= MOD_CONTROL; + } + + if (hotkey.shift_pressed()) + { + m_hotkey.modifiersMask |= MOD_SHIFT; + } + + if (hotkey.alt_pressed()) + { + m_hotkey.modifiersMask |= MOD_ALT; + } + + m_hotkey.vkCode = hotkey.get_code(); + } + catch (...) + { + Logger::warn("Failed to initialize Mouse Highlighter activation shortcut"); + } + uint8_t opacity = MOUSE_HIGHLIGHTER_DEFAULT_OPACITY; + try + { + // Parse Opacity + auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_HIGHLIGHT_OPACITY); + opacity = (uint8_t)jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE); + } + catch (...) + { + Logger::warn("Failed to initialize Opacity from settings. Will use default value"); + } + try + { + // Parse left button click color + auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_LEFT_BUTTON_CLICK_COLOR); + auto leftColor = (std::wstring)jsonPropertiesObject.GetNamedString(JSON_KEY_VALUE); + uint8_t r, g, b; + if (!checkValidRGB(leftColor,&r,&g,&b)) + { + Logger::error("Left click color RGB value is invalid. Will use default value"); + } + else + { + highlightSettings.leftButtonColor = winrt::Windows::UI::ColorHelper::FromArgb(opacity, r, g, b); + } + } + catch (...) + { + Logger::warn("Failed to initialize left click color from settings. Will use default value"); + } + try + { + // Parse right button click color + auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_RIGHT_BUTTON_CLICK_COLOR); + auto rightColor = (std::wstring)jsonPropertiesObject.GetNamedString(JSON_KEY_VALUE); + uint8_t r, g, b; + if (!checkValidRGB(rightColor, &r, &g, &b)) + { + Logger::error("Right click color RGB value is invalid. Will use default value"); + } + else + { + highlightSettings.rightButtonColor = winrt::Windows::UI::ColorHelper::FromArgb(opacity, r, g, b); + } + } + catch (...) + { + Logger::warn("Failed to initialize right click color from settings. Will use default value"); + } + try + { + // Parse Radius + auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_HIGHLIGHT_RADIUS); + highlightSettings.radius = (UINT)jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE); + } + catch (...) + { + Logger::warn("Failed to initialize Radius from settings. Will use default value"); + } + try + { + // Parse Fade Delay + auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_HIGHLIGHT_FADE_DELAY_MS); + highlightSettings.fadeDelayMs = (UINT)jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE); + } + catch (...) + { + Logger::warn("Failed to initialize Fade Delay from settings. Will use default value"); + } + try + { + // Parse Fade Duration + auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_HIGHLIGHT_FADE_DURATION_MS); + highlightSettings.fadeDurationMs = (UINT)jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE); + } + catch (...) + { + Logger::warn("Failed to initialize Fade Duration from settings. Will use default value"); + } + } + else + { + Logger::info("Mouse Highlighter settings are empty"); + } + if (!m_hotkey.modifiersMask) + { + Logger::info("Mouse Highlighter is going to use default shortcut"); + m_hotkey.modifiersMask = MOD_SHIFT | MOD_WIN; + m_hotkey.vkCode = 0x48; // H key + } + m_highlightSettings = highlightSettings; + } +}; + +extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create() +{ + return new MouseHighlighter(); +} \ No newline at end of file diff --git a/src/modules/MouseUtils/MouseHighlighter/packages.config b/src/modules/MouseUtils/MouseHighlighter/packages.config new file mode 100644 index 000000000000..81f107b8bcab --- /dev/null +++ b/src/modules/MouseUtils/MouseHighlighter/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/modules/MouseUtils/MouseHighlighter/pch.cpp b/src/modules/MouseUtils/MouseHighlighter/pch.cpp new file mode 100644 index 000000000000..1d9f38c57d63 --- /dev/null +++ b/src/modules/MouseUtils/MouseHighlighter/pch.cpp @@ -0,0 +1 @@ +#include "pch.h" diff --git a/src/modules/MouseUtils/MouseHighlighter/pch.h b/src/modules/MouseUtils/MouseHighlighter/pch.h new file mode 100644 index 000000000000..bfb4a4776a48 --- /dev/null +++ b/src/modules/MouseUtils/MouseHighlighter/pch.h @@ -0,0 +1,22 @@ +#pragma once + +#define COMPOSITION +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#include + +#ifdef COMPOSITION +#include +#include +#include +#include +#include +#include +#endif + +#include +#include +#include +#include diff --git a/src/modules/MouseUtils/MouseHighlighter/resource.base.h b/src/modules/MouseUtils/MouseHighlighter/resource.base.h new file mode 100644 index 000000000000..b49d62acd5de --- /dev/null +++ b/src/modules/MouseUtils/MouseHighlighter/resource.base.h @@ -0,0 +1,14 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by MouseHighlighter.rc + +////////////////////////////// +// Non-localizable + +#define FILE_DESCRIPTION "PowerToys MouseHighlighter" +#define INTERNAL_NAME "MouseHighlighter" +#define ORIGINAL_FILENAME "MouseHighlighter.dll" +#define IDS_KEYBOARDMANAGER_ICON 1001 + +// Non-localizable +////////////////////////////// diff --git a/src/modules/MouseUtils/MouseHighlighter/trace.cpp b/src/modules/MouseUtils/MouseHighlighter/trace.cpp new file mode 100644 index 000000000000..feefa17745cc --- /dev/null +++ b/src/modules/MouseUtils/MouseHighlighter/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 MouseHighlighter enabled or disabled +void Trace::EnableMouseHighlighter(const bool enabled) noexcept +{ + TraceLoggingWrite( + g_hProvider, + "MouseHighlighter_EnableMouseHighlighter", + ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance), + TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE), + TraceLoggingBoolean(enabled, "Enabled")); +} + +// Log that the user activated the module by starting a highlighting session +void Trace::StartHighlightingSession() noexcept +{ + TraceLoggingWrite( + g_hProvider, + "MouseHighlighter_StartHighlightingSession", + ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance), + TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE)); +} diff --git a/src/modules/MouseUtils/MouseHighlighter/trace.h b/src/modules/MouseUtils/MouseHighlighter/trace.h new file mode 100644 index 000000000000..01d660bbc040 --- /dev/null +++ b/src/modules/MouseUtils/MouseHighlighter/trace.h @@ -0,0 +1,14 @@ +#pragma once + +class Trace +{ +public: + static void RegisterProvider() noexcept; + static void UnregisterProvider() noexcept; + + // Log if the user has MouseHighlighter enabled or disabled + static void EnableMouseHighlighter(const bool enabled) noexcept; + + // Log that the user activated the module by starting a highlighting session + static void StartHighlightingSession() noexcept; +}; diff --git a/src/runner/main.cpp b/src/runner/main.cpp index 7743ae581873..bee782b8ac68 100644 --- a/src/runner/main.cpp +++ b/src/runner/main.cpp @@ -148,7 +148,8 @@ int runner(bool isProcessElevated, bool openSettings, std::string settingsWindow L"modules/ShortcutGuide/ShortcutGuideModuleInterface/ShortcutGuideModuleInterface.dll", L"modules/ColorPicker/ColorPicker.dll", L"modules/Awake/AwakeModuleInterface.dll", - L"modules/MouseUtils/FindMyMouse.dll" + L"modules/MouseUtils/FindMyMouse.dll" , + L"modules/MouseUtils/MouseHighlighter.dll" }; const auto VCM_PATH = L"modules/VideoConference/VideoConferenceModule.dll"; 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 0232ff641ba3..910a4dd78ffb 100644 --- a/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/EnabledModules.cs +++ b/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/EnabledModules.cs @@ -191,6 +191,22 @@ public bool FindMyMouse } } + private bool mouseHighlighter = true; + + [JsonPropertyName("MouseHighlighter")] + public bool MouseHighlighter + { + get => mouseHighlighter; + set + { + if (mouseHighlighter != value) + { + LogTelemetryEvent(value); + mouseHighlighter = value; + } + } + } + public string ToJsonString() { return JsonSerializer.Serialize(this); diff --git a/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/Helpers/SettingsUtilities.cs b/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/Helpers/SettingsUtilities.cs new file mode 100644 index 000000000000..913948921c99 --- /dev/null +++ b/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/Helpers/SettingsUtilities.cs @@ -0,0 +1,39 @@ +// 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.Drawing; +using System.Globalization; + +namespace Microsoft.PowerToys.Settings.UI.Library.Helpers +{ + public static class SettingsUtilities + { + public static string ToRGBHex(string color) + { + if (color == null) + { + return "#FFFFFF"; + } + + // Using InvariantCulture as these are expected to be hex codes. + bool success = int.TryParse( + color.Replace("#", string.Empty), + System.Globalization.NumberStyles.HexNumber, + CultureInfo.InvariantCulture, + out int argb); + + if (success) + { + Color clr = Color.FromArgb(argb); + return "#" + clr.R.ToString("X2", CultureInfo.InvariantCulture) + + clr.G.ToString("X2", CultureInfo.InvariantCulture) + + clr.B.ToString("X2", CultureInfo.InvariantCulture); + } + else + { + return "#FFFFFF"; + } + } + } +} diff --git a/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/MouseHighlighterProperties.cs b/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/MouseHighlighterProperties.cs new file mode 100644 index 000000000000..7ccd5534f3d0 --- /dev/null +++ b/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/MouseHighlighterProperties.cs @@ -0,0 +1,43 @@ +// 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.Text.Json.Serialization; + +namespace Microsoft.PowerToys.Settings.UI.Library +{ + public class MouseHighlighterProperties + { + [JsonPropertyName("activation_shortcut")] + public HotkeySettings ActivationShortcut { get; set; } + + [JsonPropertyName("left_button_click_color")] + public StringProperty LeftButtonClickColor { get; set; } + + [JsonPropertyName("right_button_click_color")] + public StringProperty RightButtonClickColor { get; set; } + + [JsonPropertyName("highlight_opacity")] + public IntProperty HighlightOpacity { get; set; } + + [JsonPropertyName("highlight_radius")] + public IntProperty HighlightRadius { get; set; } + + [JsonPropertyName("highlight_fade_delay_ms")] + public IntProperty HighlightFadeDelayMs { get; set; } + + [JsonPropertyName("highlight_fade_duration_ms")] + public IntProperty HighlightFadeDurationMs { get; set; } + + public MouseHighlighterProperties() + { + ActivationShortcut = new HotkeySettings(true, false, false, true, 0x48); + LeftButtonClickColor = new StringProperty("#FFFF00"); + RightButtonClickColor = new StringProperty("#0000FF"); + HighlightOpacity = new IntProperty(160); + HighlightRadius = new IntProperty(20); + HighlightFadeDelayMs = new IntProperty(500); + HighlightFadeDurationMs = new IntProperty(250); + } + } +} diff --git a/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/MouseHighlighterSettings.cs b/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/MouseHighlighterSettings.cs new file mode 100644 index 000000000000..100e5b60d6fc --- /dev/null +++ b/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/MouseHighlighterSettings.cs @@ -0,0 +1,35 @@ +// 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.Text.Json.Serialization; +using Microsoft.PowerToys.Settings.UI.Library.Interfaces; + +namespace Microsoft.PowerToys.Settings.UI.Library +{ + public class MouseHighlighterSettings : BasePTModuleSettings, ISettingsConfig + { + public const string ModuleName = "MouseHighlighter"; + + [JsonPropertyName("properties")] + public MouseHighlighterProperties Properties { get; set; } + + public MouseHighlighterSettings() + { + Name = ModuleName; + Properties = new MouseHighlighterProperties(); + Version = "1.0"; + } + + public string GetModuleName() + { + return Name; + } + + // This can be utilized in the future if the settings.json file is to be modified/deleted. + public bool UpgradeSettingsConfiguration() + { + return false; + } + } +} diff --git a/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/MouseHighlighterSettingsIPCMessage.cs b/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/MouseHighlighterSettingsIPCMessage.cs new file mode 100644 index 000000000000..e96669493c0e --- /dev/null +++ b/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/MouseHighlighterSettingsIPCMessage.cs @@ -0,0 +1,29 @@ +// 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.Text.Json; +using System.Text.Json.Serialization; + +namespace Microsoft.PowerToys.Settings.UI.Library +{ + public class MouseHighlighterSettingsIPCMessage + { + [JsonPropertyName("powertoys")] + public SndMouseHighlighterSettings Powertoys { get; set; } + + public MouseHighlighterSettingsIPCMessage() + { + } + + public MouseHighlighterSettingsIPCMessage(SndMouseHighlighterSettings settings) + { + this.Powertoys = settings; + } + + public string ToJsonString() + { + return JsonSerializer.Serialize(this); + } + } +} diff --git a/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/SndMouseHighlighterSettings.cs b/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/SndMouseHighlighterSettings.cs new file mode 100644 index 000000000000..ccaf04e0dae1 --- /dev/null +++ b/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/SndMouseHighlighterSettings.cs @@ -0,0 +1,29 @@ +// 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.Text.Json; +using System.Text.Json.Serialization; + +namespace Microsoft.PowerToys.Settings.UI.Library +{ + public class SndMouseHighlighterSettings + { + [JsonPropertyName("MouseHighlighter")] + public MouseHighlighterSettings MouseHighlighter { get; set; } + + public SndMouseHighlighterSettings() + { + } + + public SndMouseHighlighterSettings(MouseHighlighterSettings settings) + { + MouseHighlighter = settings; + } + + public string ToJsonString() + { + return JsonSerializer.Serialize(this); + } + } +} diff --git a/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/ViewModels/FancyZonesViewModel.cs b/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/ViewModels/FancyZonesViewModel.cs index 29d3650449cb..7c057e024d4f 100644 --- a/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/ViewModels/FancyZonesViewModel.cs +++ b/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/ViewModels/FancyZonesViewModel.cs @@ -554,7 +554,7 @@ public string ZoneHighlightColor // The fallback value is based on ToRGBHex's behavior, which returns // #FFFFFF if any exceptions are encountered, e.g. from passing in a null value. // This extra handling is added here to deal with FxCop warnings. - value = (value != null) ? ToRGBHex(value) : "#FFFFFF"; + value = (value != null) ? SettingsUtilities.ToRGBHex(value) : "#FFFFFF"; if (!value.Equals(_zoneHighlightColor, StringComparison.OrdinalIgnoreCase)) { _zoneHighlightColor = value; @@ -576,7 +576,7 @@ public string ZoneBorderColor // The fallback value is based on ToRGBHex's behavior, which returns // #FFFFFF if any exceptions are encountered, e.g. from passing in a null value. // This extra handling is added here to deal with FxCop warnings. - value = (value != null) ? ToRGBHex(value) : "#FFFFFF"; + value = (value != null) ? SettingsUtilities.ToRGBHex(value) : "#FFFFFF"; if (!value.Equals(_zoneBorderColor, StringComparison.OrdinalIgnoreCase)) { _zoneBorderColor = value; @@ -598,7 +598,7 @@ public string ZoneInActiveColor // The fallback value is based on ToRGBHex's behavior, which returns // #FFFFFF if any exceptions are encountered, e.g. from passing in a null value. // This extra handling is added here to deal with FxCop warnings. - value = (value != null) ? ToRGBHex(value) : "#FFFFFF"; + value = (value != null) ? SettingsUtilities.ToRGBHex(value) : "#FFFFFF"; if (!value.Equals(_zoneInActiveColor, StringComparison.OrdinalIgnoreCase)) { _zoneInActiveColor = value; @@ -753,27 +753,5 @@ public void NotifyPropertyChanged([CallerMemberName] string propertyName = null) OnPropertyChanged(propertyName); SettingsUtils.SaveSettings(Settings.ToJsonString(), GetSettingsSubPath()); } - - private static string ToRGBHex(string color) - { - // Using InvariantCulture as these are expected to be hex codes. - bool success = int.TryParse( - color.Replace("#", string.Empty), - System.Globalization.NumberStyles.HexNumber, - CultureInfo.InvariantCulture, - out int argb); - - if (success) - { - Color clr = Color.FromArgb(argb); - return "#" + clr.R.ToString("X2", CultureInfo.InvariantCulture) + - clr.G.ToString("X2", CultureInfo.InvariantCulture) + - clr.B.ToString("X2", CultureInfo.InvariantCulture); - } - else - { - return "#FFFFFF"; - } - } } } 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 index 5faec966f79b..1a22ba39d024 100644 --- a/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/ViewModels/MouseUtilsViewModel.cs +++ b/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/ViewModels/MouseUtilsViewModel.cs @@ -17,7 +17,9 @@ public class MouseUtilsViewModel : Observable private FindMyMouseSettings FindMyMouseSettingsConfig { get; set; } - public MouseUtilsViewModel(ISettingsUtils settingsUtils, ISettingsRepository settingsRepository, ISettingsRepository findMyMouseSettingsRepository, Func ipcMSGCallBackFunc) + private MouseHighlighterSettings MouseHighlighterSettingsConfig { get; set; } + + public MouseUtilsViewModel(ISettingsUtils settingsUtils, ISettingsRepository settingsRepository, ISettingsRepository findMyMouseSettingsRepository, ISettingsRepository mouseHighlighterSettingsRepository, Func ipcMSGCallBackFunc) { SettingsUtils = settingsUtils; @@ -31,6 +33,8 @@ public MouseUtilsViewModel(ISettingsUtils settingsUtils, ISettingsRepository _isMouseHighlighterEnabled; + set + { + if (_isMouseHighlighterEnabled != value) + { + _isMouseHighlighterEnabled = value; + + GeneralSettingsConfig.Enabled.MouseHighlighter = value; + OnPropertyChanged(nameof(_isMouseHighlighterEnabled)); + + OutGoingGeneralSettings outgoing = new OutGoingGeneralSettings(GeneralSettingsConfig); + SendConfigMSG(outgoing.ToString()); + + NotifyMouseHighlighterPropertyChanged(); + } + } + } + + public HotkeySettings MouseHighlighterActivationShortcut + { + get + { + return MouseHighlighterSettingsConfig.Properties.ActivationShortcut; + } + + set + { + if (MouseHighlighterSettingsConfig.Properties.ActivationShortcut != value) + { + MouseHighlighterSettingsConfig.Properties.ActivationShortcut = value; + NotifyMouseHighlighterPropertyChanged(); + } + } + } + + public string MouseHighlighterLeftButtonClickColor + { + get + { + return _highlighterLeftButtonClickColor; + } + + set + { + // The fallback value is based on ToRGBHex's behavior, which returns + // #FFFFFF if any exceptions are encountered, e.g. from passing in a null value. + // This extra handling is added here to deal with FxCop warnings. + value = (value != null) ? SettingsUtilities.ToRGBHex(value) : "#FFFFFF"; + if (!value.Equals(_highlighterLeftButtonClickColor, StringComparison.OrdinalIgnoreCase)) + { + _highlighterLeftButtonClickColor = value; + MouseHighlighterSettingsConfig.Properties.LeftButtonClickColor.Value = value; + NotifyMouseHighlighterPropertyChanged(); + } + } + } + + public string MouseHighlighterRightButtonClickColor + { + get + { + return _highlighterRightButtonClickColor; + } + + set + { + // The fallback value is based on ToRGBHex's behavior, which returns + // #FFFFFF if any exceptions are encountered, e.g. from passing in a null value. + // This extra handling is added here to deal with FxCop warnings. + value = (value != null) ? SettingsUtilities.ToRGBHex(value) : "#FFFFFF"; + if (!value.Equals(_highlighterRightButtonClickColor, StringComparison.OrdinalIgnoreCase)) + { + _highlighterRightButtonClickColor = value; + MouseHighlighterSettingsConfig.Properties.RightButtonClickColor.Value = value; + NotifyMouseHighlighterPropertyChanged(); + } + } + } + + public int MouseHighlighterOpacity + { + get + { + return _highlighterOpacity; + } + + set + { + if (value != _highlighterOpacity) + { + _highlighterOpacity = value; + MouseHighlighterSettingsConfig.Properties.HighlightOpacity.Value = value; + NotifyMouseHighlighterPropertyChanged(); + } + } + } + + public int MouseHighlighterRadius + { + get + { + return _highlighterRadius; + } + + set + { + if (value != _highlighterRadius) + { + _highlighterRadius = value; + MouseHighlighterSettingsConfig.Properties.HighlightRadius.Value = value; + NotifyMouseHighlighterPropertyChanged(); + } + } + } + + public int MouseHighlighterFadeDelayMs + { + get + { + return _highlightFadeDelayMs; + } + + set + { + if (value != _highlightFadeDelayMs) + { + _highlightFadeDelayMs = value; + MouseHighlighterSettingsConfig.Properties.HighlightFadeDelayMs.Value = value; + NotifyMouseHighlighterPropertyChanged(); + } + } + } + + public int MouseHighlighterFadeDurationMs + { + get + { + return _highlightFadeDurationMs; + } + + set + { + if (value != _highlightFadeDurationMs) + { + _highlightFadeDurationMs = value; + MouseHighlighterSettingsConfig.Properties.HighlightFadeDurationMs.Value = value; + NotifyMouseHighlighterPropertyChanged(); + } + } + } + + public void NotifyMouseHighlighterPropertyChanged([CallerMemberName] string propertyName = null) + { + OnPropertyChanged(propertyName); + + SndMouseHighlighterSettings outsettings = new SndMouseHighlighterSettings(MouseHighlighterSettingsConfig); + SndModuleSettings ipcMessage = new SndModuleSettings(outsettings); + SendConfigMSG(ipcMessage.ToJsonString()); + SettingsUtils.SaveSettings(MouseHighlighterSettingsConfig.ToJsonString(), MouseHighlighterSettings.ModuleName); + } + private Func SendConfigMSG { get; } private bool _isFindMyMouseEnabled; private bool _findMyMouseDoNotActivateOnGameMode; + + private bool _isMouseHighlighterEnabled; + private string _highlighterLeftButtonClickColor; + private string _highlighterRightButtonClickColor; + private int _highlighterOpacity; + private int _highlighterRadius; + private int _highlightFadeDelayMs; + private int _highlightFadeDurationMs; } } diff --git a/src/settings-ui/Microsoft.PowerToys.Settings.UI/Assets/FluentIcons/FluentIconsFindMyMouse.png b/src/settings-ui/Microsoft.PowerToys.Settings.UI/Assets/FluentIcons/FluentIconsFindMyMouse.png new file mode 100644 index 0000000000000000000000000000000000000000..98e7e4cf68099908f7c143bfbc97e66e0ed3b603 GIT binary patch literal 2701 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 + + + 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 index 6fe25099cef1..5ac794509747 100644 --- a/src/settings-ui/Microsoft.PowerToys.Settings.UI/OOBE/Views/OobeMouseUtils.xaml +++ b/src/settings-ui/Microsoft.PowerToys.Settings.UI/OOBE/Views/OobeMouseUtils.xaml @@ -18,6 +18,10 @@ Style="{ThemeResource OobeSubtitleStyle}" /> + + +