diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index d1b2de49dd..883ff38071 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 22f6cf723a..b727524f29 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 e185f09f71..5b023cf437 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 0000000000..5098e2237a Binary files /dev/null and b/doc/images/icons/Find My Mouse.png differ diff --git a/doc/images/icons/Mouse Highlighter.png b/doc/images/icons/Mouse Highlighter.png new file mode 100644 index 0000000000..789b0b1c04 Binary files /dev/null and b/doc/images/icons/Mouse Highlighter.png differ diff --git a/installer/PowerToysSetup/Product.wxs b/installer/PowerToysSetup/Product.wxs index 2b11399842..0cbf25227f 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 1015ef9f57..b88af2bc77 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 0000000000..7e1362ac60 --- /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 0000000000..0bcdeca2ef --- /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 0000000000..8b8c83c1fa --- /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 0000000000..eb1948bd09 --- /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 0000000000..5c127f8525 --- /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 0000000000..ab12bcf6d6 --- /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 0000000000..78858cbed2 --- /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 0000000000..81f107b8bc --- /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 0000000000..1d9f38c57d --- /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 0000000000..bfb4a4776a --- /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 0000000000..b49d62acd5 --- /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 0000000000..feefa17745 --- /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 0000000000..01d660bbc0 --- /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 7743ae5818..bee782b8ac 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 0232ff641b..910a4dd78f 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 @@ namespace Microsoft.PowerToys.Settings.UI.Library } } + 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 0000000000..913948921c --- /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 0000000000..7ccd5534f3 --- /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 0000000000..100e5b60d6 --- /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 0000000000..e96669493c --- /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 0000000000..ccaf04e0da --- /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 29d3650449..7c057e024d 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 @@ namespace Microsoft.PowerToys.Settings.UI.Library.ViewModels // 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 @@ namespace Microsoft.PowerToys.Settings.UI.Library.ViewModels // 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 @@ namespace Microsoft.PowerToys.Settings.UI.Library.ViewModels // 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 @@ namespace Microsoft.PowerToys.Settings.UI.Library.ViewModels 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 5faec966f7..1a22ba39d0 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 @@ namespace Microsoft.PowerToys.Settings.UI.Library.ViewModels 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 @@ namespace Microsoft.PowerToys.Settings.UI.Library.ViewModels _isFindMyMouseEnabled = GeneralSettingsConfig.Enabled.FindMyMouse; + _isMouseHighlighterEnabled = GeneralSettingsConfig.Enabled.MouseHighlighter; + // To obtain the find my mouse settings, if the file exists. // If not, to create a file with the default settings and to return the default configurations. if (findMyMouseSettingsRepository == null) @@ -41,6 +45,23 @@ namespace Microsoft.PowerToys.Settings.UI.Library.ViewModels FindMyMouseSettingsConfig = findMyMouseSettingsRepository.SettingsConfig; _findMyMouseDoNotActivateOnGameMode = FindMyMouseSettingsConfig.Properties.DoNotActivateOnGameMode.Value; + if (mouseHighlighterSettingsRepository == null) + { + throw new ArgumentNullException(nameof(mouseHighlighterSettingsRepository)); + } + + MouseHighlighterSettingsConfig = mouseHighlighterSettingsRepository.SettingsConfig; + string leftClickColor = MouseHighlighterSettingsConfig.Properties.LeftButtonClickColor.Value; + _highlighterLeftButtonClickColor = !string.IsNullOrEmpty(leftClickColor) ? leftClickColor : "#FFFF00"; + + string rightClickColor = MouseHighlighterSettingsConfig.Properties.RightButtonClickColor.Value; + _highlighterRightButtonClickColor = !string.IsNullOrEmpty(rightClickColor) ? rightClickColor : "#0000FF"; + + _highlighterOpacity = MouseHighlighterSettingsConfig.Properties.HighlightOpacity.Value; + _highlighterRadius = MouseHighlighterSettingsConfig.Properties.HighlightRadius.Value; + _highlightFadeDelayMs = MouseHighlighterSettingsConfig.Properties.HighlightFadeDelayMs.Value; + _highlightFadeDurationMs = MouseHighlighterSettingsConfig.Properties.HighlightFadeDurationMs.Value; + // set the callback functions value to handle outgoing IPC message. SendConfigMSG = ipcMSGCallBackFunc; } @@ -93,9 +114,180 @@ namespace Microsoft.PowerToys.Settings.UI.Library.ViewModels SettingsUtils.SaveSettings(FindMyMouseSettingsConfig.ToJsonString(), FindMyMouseSettings.ModuleName); } + public bool IsMouseHighlighterEnabled + { + get => _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 0000000000..98e7e4cf68 Binary files /dev/null and b/src/settings-ui/Microsoft.PowerToys.Settings.UI/Assets/FluentIcons/FluentIconsFindMyMouse.png differ diff --git a/src/settings-ui/Microsoft.PowerToys.Settings.UI/Assets/FluentIcons/FluentIconsMouseHighlighter.png b/src/settings-ui/Microsoft.PowerToys.Settings.UI/Assets/FluentIcons/FluentIconsMouseHighlighter.png new file mode 100644 index 0000000000..33c05d9760 Binary files /dev/null and b/src/settings-ui/Microsoft.PowerToys.Settings.UI/Assets/FluentIcons/FluentIconsMouseHighlighter.png 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 33ad5c2dfb..1c675efda5 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 @@ -237,8 +237,11 @@ + + + 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 6fe25099ce..5ac7945097 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}" /> + + +