diff --git a/.pipelines/ESRPSigning_core.json b/.pipelines/ESRPSigning_core.json index eb08c34c9c..618e437b9c 100644 --- a/.pipelines/ESRPSigning_core.json +++ b/.pipelines/ESRPSigning_core.json @@ -95,6 +95,7 @@ "modules\\MouseUtils\\PowerToys.FindMyMouse.dll", "modules\\MouseUtils\\PowerToys.MouseHighlighter.dll", + "modules\\MouseUtils\\PowerToys.MousePointerCrosshair.dll", "modules\\PowerRename\\PowerToys.PowerRenameExt.dll", "modules\\PowerRename\\PowerToys.PowerRenameUILib.dll", diff --git a/PowerToys.sln b/PowerToys.sln index 5cd578d81d..2e80669738 100644 --- a/PowerToys.sln +++ b/PowerToys.sln @@ -388,6 +388,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "AlwaysOnTopModuleInterface" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Community.PowerToys.Run.Plugin.WebSearch", "src\modules\launcher\Plugins\Community.PowerToys.Run.Plugin.WebSearch\Community.PowerToys.Run.Plugin.WebSearch.csproj", "{9F94B303-5E21-4364-9362-64426F8DB932}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "MousePointerCrosshair", "src\modules\MouseUtils\MousePointerCrosshair\MousePointerCrosshair.vcxproj", "{EAE14C0E-7A6B-45DA-9080-A7D8C077BA6E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x64 = Debug|x64 @@ -1045,6 +1047,12 @@ Global {9F94B303-5E21-4364-9362-64426F8DB932}.Release|x64.ActiveCfg = Release|x64 {9F94B303-5E21-4364-9362-64426F8DB932}.Release|x64.Build.0 = Release|x64 {9F94B303-5E21-4364-9362-64426F8DB932}.Release|x86.ActiveCfg = Release|x64 + {EAE14C0E-7A6B-45DA-9080-A7D8C077BA6E}.Debug|x64.ActiveCfg = Debug|x64 + {EAE14C0E-7A6B-45DA-9080-A7D8C077BA6E}.Debug|x64.Build.0 = Debug|x64 + {EAE14C0E-7A6B-45DA-9080-A7D8C077BA6E}.Debug|x86.ActiveCfg = Debug|x64 + {EAE14C0E-7A6B-45DA-9080-A7D8C077BA6E}.Release|x64.ActiveCfg = Release|x64 + {EAE14C0E-7A6B-45DA-9080-A7D8C077BA6E}.Release|x64.Build.0 = Release|x64 + {EAE14C0E-7A6B-45DA-9080-A7D8C077BA6E}.Release|x86.ActiveCfg = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1172,6 +1180,7 @@ Global {1DC3BE92-CE89-43FB-8110-9C043A2FE7A2} = {60CD2D4F-C3B9-4897-9821-FCA5098B41CE} {48A0A19E-A0BE-4256-ACF8-CC3B80291AF9} = {60CD2D4F-C3B9-4897-9821-FCA5098B41CE} {9F94B303-5E21-4364-9362-64426F8DB932} = {4AFC9975-2456-4C70-94A4-84073C1CED93} + {EAE14C0E-7A6B-45DA-9080-A7D8C077BA6E} = {322566EF-20DC-43A6-B9F8-616AF942579A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0} diff --git a/installer/PowerToysSetup/Product.wxs b/installer/PowerToysSetup/Product.wxs index 3ea4274ea0..138fa8b3be 100644 --- a/installer/PowerToysSetup/Product.wxs +++ b/installer/PowerToysSetup/Product.wxs @@ -707,6 +707,9 @@ + + + @@ -1045,6 +1048,7 @@ + diff --git a/src/common/logger/logger_settings.h b/src/common/logger/logger_settings.h index e45e79c042..a5be14ced5 100644 --- a/src/common/logger/logger_settings.h +++ b/src/common/logger/logger_settings.h @@ -27,6 +27,7 @@ struct LogSettings 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 mousePointerCrosshairLoggerName = "mouse-pointer-crosshair"; inline const static std::string powerRenameLoggerName = "powerrename"; inline const static std::string alwaysOnTopLoggerName = "always-on-top"; inline const static std::wstring alwaysOnTopLogPath = L"always-on-top-log.txt"; diff --git a/src/modules/MouseUtils/MousePointerCrosshair/InclusiveCrosshair.cpp b/src/modules/MouseUtils/MousePointerCrosshair/InclusiveCrosshair.cpp new file mode 100644 index 0000000000..0c67e4e77d --- /dev/null +++ b/src/modules/MouseUtils/MousePointerCrosshair/InclusiveCrosshair.cpp @@ -0,0 +1,452 @@ +// InclusiveCrosshair.cpp : Defines the entry point for the application. +// + +#include "pch.h" +#include "InclusiveCrosshair.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 InclusiveCrosshair +{ + bool MyRegisterClass(HINSTANCE hInstance); + static InclusiveCrosshair* instance; + void Terminate(); + void SwitchActivationMode(); + void ApplySettings(InclusiveCrosshairSettings& settings, bool applyToRuntimeObjects); + +private: + enum class MouseButton + { + Left, + Right + }; + + void DestroyInclusiveCrosshair(); + static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) noexcept; + void StartDrawing(); + void StopDrawing(); + bool CreateInclusiveCrosshair(); + void UpdateCrosshairPosition(); + HHOOK m_mouseHook = NULL; + static LRESULT CALLBACK MouseHookProc(int nCode, WPARAM wParam, LPARAM lParam) noexcept; + + static constexpr auto m_className = L"MousePointerCrosshair"; + static constexpr auto m_windowTitle = L"PowerToys Mouse Pointer Crosshair"; + 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_crosshair_border_layer{ nullptr }; + winrt::LayerVisual m_crosshair_layer{ nullptr }; + winrt::SpriteVisual m_left_crosshair_border{ nullptr }; + winrt::SpriteVisual m_left_crosshair{ nullptr }; + winrt::SpriteVisual m_right_crosshair_border{ nullptr }; + winrt::SpriteVisual m_right_crosshair{ nullptr }; + winrt::SpriteVisual m_top_crosshair_border{ nullptr }; + winrt::SpriteVisual m_top_crosshair{ nullptr }; + winrt::SpriteVisual m_bottom_crosshair_border{ nullptr }; + winrt::SpriteVisual m_bottom_crosshair{ nullptr }; + + bool m_visible = false; + bool m_destroyed = false; + + // Configurable Settings + winrt::Windows::UI::Color m_crosshair_border_color = INCLUSIVE_MOUSE_DEFAULT_CROSSHAIR_BORDER_COLOR; + winrt::Windows::UI::Color m_crosshair_color = INCLUSIVE_MOUSE_DEFAULT_CROSSHAIR_COLOR; + float m_crosshair_radius = INCLUSIVE_MOUSE_DEFAULT_CROSSHAIR_RADIUS; + float m_crosshair_thickness = INCLUSIVE_MOUSE_DEFAULT_CROSSHAIR_THICKNESS; + float m_crosshair_border_size = INCLUSIVE_MOUSE_DEFAULT_CROSSHAIR_BORDER_SIZE; + float m_crosshair_opacity = max(0.f, min(1.f, (float)INCLUSIVE_MOUSE_DEFAULT_CROSSHAIR_OPACITY / 100.0f)); +}; + +InclusiveCrosshair* InclusiveCrosshair::instance = nullptr; + +bool InclusiveCrosshair::CreateInclusiveCrosshair() +{ + try + { + // We need a dispatcher queue. + DispatcherQueueOptions options = { + sizeof(options), + DQTYPE_THREAD_CURRENT, + DQTAT_COM_ASTA, + }; + ABI::IDispatcherQueueController* controller; + winrt::check_hresult(CreateDispatcherQueueController(options, &controller)); + *winrt::put_abi(m_dispatcherQueueController) = controller; + + // Create the compositor for our window. + m_compositor = winrt::Compositor(); + ABI::IDesktopWindowTarget* target; + winrt::check_hresult(m_compositor.as()->CreateDesktopWindowTarget(m_hwnd, false, &target)); + *winrt::put_abi(m_target) = target; + + // Our composition tree: + // + // [root] ContainerVisual + // \ [crosshair border layer] LayerVisual + // \ [crosshair border sprites] + // [crosshair layer] LayerVisual + // \ [crosshair sprites] + + m_root = m_compositor.CreateContainerVisual(); + m_root.RelativeSizeAdjustment({ 1.0f, 1.0f }); + m_target.Root(m_root); + + m_root.Opacity(m_crosshair_opacity); + + m_crosshair_border_layer = m_compositor.CreateLayerVisual(); + m_crosshair_border_layer.RelativeSizeAdjustment({ 1.0f, 1.0f }); + m_root.Children().InsertAtTop(m_crosshair_border_layer); + m_crosshair_border_layer.Opacity(1.0f); + + m_crosshair_layer = m_compositor.CreateLayerVisual(); + m_crosshair_layer.RelativeSizeAdjustment({ 1.0f, 1.0f }); + + // Create the crosshair sprites. + m_left_crosshair_border = m_compositor.CreateSpriteVisual(); + m_left_crosshair_border.AnchorPoint({ 1.0f, 0.5f }); + m_left_crosshair_border.Brush(m_compositor.CreateColorBrush(m_crosshair_border_color)); + m_crosshair_border_layer.Children().InsertAtTop(m_left_crosshair_border); + m_left_crosshair = m_compositor.CreateSpriteVisual(); + m_left_crosshair.AnchorPoint({ 1.0f, 0.5f }); + m_left_crosshair.Brush(m_compositor.CreateColorBrush(m_crosshair_color)); + m_crosshair_layer.Children().InsertAtTop(m_left_crosshair); + + m_right_crosshair_border = m_compositor.CreateSpriteVisual(); + m_right_crosshair_border.AnchorPoint({ 0.0f, 0.5f }); + m_right_crosshair_border.Brush(m_compositor.CreateColorBrush(m_crosshair_border_color)); + m_crosshair_border_layer.Children().InsertAtTop(m_right_crosshair_border); + m_right_crosshair = m_compositor.CreateSpriteVisual(); + m_right_crosshair.AnchorPoint({ 0.0f, 0.5f }); + m_right_crosshair.Brush(m_compositor.CreateColorBrush(m_crosshair_color)); + m_crosshair_layer.Children().InsertAtTop(m_right_crosshair); + + m_top_crosshair_border = m_compositor.CreateSpriteVisual(); + m_top_crosshair_border.AnchorPoint({ 0.5f, 1.0f }); + m_top_crosshair_border.Brush(m_compositor.CreateColorBrush(m_crosshair_border_color)); + m_crosshair_border_layer.Children().InsertAtTop(m_top_crosshair_border); + m_top_crosshair = m_compositor.CreateSpriteVisual(); + m_top_crosshair.AnchorPoint({ 0.5f, 1.0f }); + m_top_crosshair.Brush(m_compositor.CreateColorBrush(m_crosshair_color)); + m_crosshair_layer.Children().InsertAtTop(m_top_crosshair); + + m_bottom_crosshair_border = m_compositor.CreateSpriteVisual(); + m_bottom_crosshair_border.AnchorPoint({ 0.5f, 0.0f }); + m_bottom_crosshair_border.Brush(m_compositor.CreateColorBrush(m_crosshair_border_color)); + m_crosshair_border_layer.Children().InsertAtTop(m_bottom_crosshair_border); + m_bottom_crosshair = m_compositor.CreateSpriteVisual(); + m_bottom_crosshair.AnchorPoint({ 0.5f, 0.0f }); + m_bottom_crosshair.Brush(m_compositor.CreateColorBrush(m_crosshair_color)); + m_crosshair_layer.Children().InsertAtTop(m_bottom_crosshair); + + m_crosshair_border_layer.Children().InsertAtTop(m_crosshair_layer); + m_crosshair_layer.Opacity(1.0f); + + UpdateCrosshairPosition(); + + return true; + } + catch (...) + { + return false; + } +} + +void InclusiveCrosshair::UpdateCrosshairPosition() +{ + POINT ptCursor; + + GetCursorPos(&ptCursor); + + HMONITOR cursorMonitor = MonitorFromPoint(ptCursor, MONITOR_DEFAULTTONEAREST); + + if (cursorMonitor == NULL) + { + return; + } + + MONITORINFO monitorInfo; + monitorInfo.cbSize = sizeof(monitorInfo); + + if (!GetMonitorInfo(cursorMonitor, &monitorInfo)) + { + return; + } + + POINT ptMonitorUpperLeft; + ptMonitorUpperLeft.x = monitorInfo.rcMonitor.left; + ptMonitorUpperLeft.y = monitorInfo.rcMonitor.top; + + POINT ptMonitorBottomRight; + ptMonitorBottomRight.x = monitorInfo.rcMonitor.right; + ptMonitorBottomRight.y = monitorInfo.rcMonitor.bottom; + + // Convert everything to client coordinates. + ScreenToClient(m_hwnd, &ptCursor); + ScreenToClient(m_hwnd, &ptMonitorUpperLeft); + ScreenToClient(m_hwnd, &ptMonitorBottomRight); + + // Position crosshair components around the mouse pointer. + m_left_crosshair_border.Offset({ (float)ptCursor.x - m_crosshair_radius + m_crosshair_border_size, (float)ptCursor.y, .0f }); + m_left_crosshair_border.Size({ (float)ptCursor.x - (float)ptMonitorUpperLeft.x - m_crosshair_radius + m_crosshair_border_size, m_crosshair_thickness + m_crosshair_border_size * 2 }); + m_left_crosshair.Offset({ (float)ptCursor.x - m_crosshair_radius, (float)ptCursor.y, .0f }); + m_left_crosshair.Size({ (float)ptCursor.x - (float)ptMonitorUpperLeft.x - m_crosshair_radius, m_crosshair_thickness }); + + m_right_crosshair_border.Offset({ (float)ptCursor.x + m_crosshair_radius - m_crosshair_border_size, (float)ptCursor.y, .0f }); + m_right_crosshair_border.Size({ (float)ptMonitorBottomRight.x - (float)ptCursor.x - m_crosshair_radius + m_crosshair_border_size, m_crosshair_thickness + m_crosshair_border_size * 2 }); + m_right_crosshair.Offset({ (float)ptCursor.x + m_crosshair_radius, (float)ptCursor.y, .0f }); + m_right_crosshair.Size({ (float)ptMonitorBottomRight.x - (float)ptCursor.x - m_crosshair_radius, m_crosshair_thickness }); + + m_top_crosshair_border.Offset({ (float)ptCursor.x, (float)ptCursor.y - m_crosshair_radius + m_crosshair_border_size, .0f }); + m_top_crosshair_border.Size({ m_crosshair_thickness + m_crosshair_border_size * 2, (float)ptCursor.y - (float)ptMonitorUpperLeft.y - m_crosshair_radius + m_crosshair_border_size }); + m_top_crosshair.Offset({ (float)ptCursor.x, (float)ptCursor.y - m_crosshair_radius, .0f }); + m_top_crosshair.Size({ m_crosshair_thickness, (float)ptCursor.y - (float)ptMonitorUpperLeft.y - m_crosshair_radius }); + + m_bottom_crosshair_border.Offset({ (float)ptCursor.x, (float)ptCursor.y + m_crosshair_radius - m_crosshair_border_size, .0f }); + m_bottom_crosshair_border.Size({ m_crosshair_thickness + m_crosshair_border_size * 2, (float)ptMonitorBottomRight.y - (float)ptCursor.y - m_crosshair_radius + m_crosshair_border_size }); + m_bottom_crosshair.Offset({ (float)ptCursor.x, (float)ptCursor.y + m_crosshair_radius, .0f }); + m_bottom_crosshair.Size({ m_crosshair_thickness, (float)ptMonitorBottomRight.y - (float)ptCursor.y - m_crosshair_radius }); + +} + +LRESULT CALLBACK InclusiveCrosshair::MouseHookProc(int nCode, WPARAM wParam, LPARAM lParam) noexcept +{ + if (nCode >= 0) + { + MSLLHOOKSTRUCT* hookData = (MSLLHOOKSTRUCT*)lParam; + if (wParam == WM_MOUSEMOVE) { + instance->UpdateCrosshairPosition(); + } + } + return CallNextHookEx(0, nCode, wParam, lParam); +} + +void InclusiveCrosshair::StartDrawing() +{ + Logger::info("Start drawing crosshairs."); + Trace::StartDrawingCrosshair(); + m_visible = true; + SetWindowPos(m_hwnd, HWND_TOPMOST, GetSystemMetrics(SM_XVIRTUALSCREEN), GetSystemMetrics(SM_YVIRTUALSCREEN), GetSystemMetrics(SM_CXVIRTUALSCREEN), GetSystemMetrics(SM_CYVIRTUALSCREEN), 0); + ShowWindow(m_hwnd, SW_SHOWNOACTIVATE); + m_mouseHook = SetWindowsHookEx(WH_MOUSE_LL, MouseHookProc, m_hinstance, 0); + UpdateCrosshairPosition(); +} + +void InclusiveCrosshair::StopDrawing() +{ + Logger::info("Stop drawing crosshairs."); + m_visible = false; + ShowWindow(m_hwnd, SW_HIDE); + UnhookWindowsHookEx(m_mouseHook); + m_mouseHook = NULL; +} + +void InclusiveCrosshair::SwitchActivationMode() +{ + PostMessage(m_hwnd, WM_SWITCH_ACTIVATION_MODE, 0, 0); +} + +void InclusiveCrosshair::ApplySettings(InclusiveCrosshairSettings& settings, bool applyToRunTimeObjects) +{ + m_crosshair_radius = (float)settings.crosshairRadius; + m_crosshair_thickness = (float)settings.crosshairThickness; + m_crosshair_color = settings.crosshairColor; + m_crosshair_opacity = max(0.f, min(1.f, (float)settings.crosshairOpacity / 100.0f)); + m_crosshair_border_color = settings.crosshairBorderColor; + m_crosshair_border_size = (float)settings.crosshairBorderSize; + + if (applyToRunTimeObjects) + { + // Runtime objects already created. Should update in the owner thread. + auto dispatcherQueue = m_dispatcherQueueController.DispatcherQueue(); + InclusiveCrosshairSettings localSettings = settings; + bool enqueueSucceeded = dispatcherQueue.TryEnqueue([=]() { + if (!m_destroyed) + { + // Apply new settings to runtime composition objects. + m_left_crosshair.Brush().as().Color(m_crosshair_color); + m_right_crosshair.Brush().as().Color(m_crosshair_color); + m_top_crosshair.Brush().as().Color(m_crosshair_color); + m_bottom_crosshair.Brush().as().Color(m_crosshair_color); + m_left_crosshair_border.Brush().as().Color(m_crosshair_border_color); + m_right_crosshair_border.Brush().as().Color(m_crosshair_border_color); + m_top_crosshair_border.Brush().as().Color(m_crosshair_border_color); + m_bottom_crosshair_border.Brush().as().Color(m_crosshair_border_color); + m_root.Opacity(m_crosshair_opacity); + UpdateCrosshairPosition(); + } + }); + if (!enqueueSucceeded) + { + Logger::error("Couldn't enqueue message to update the crosshair settings."); + } + } +} + +void InclusiveCrosshair::DestroyInclusiveCrosshair() +{ + StopDrawing(); + PostQuitMessage(0); +} + +LRESULT CALLBACK InclusiveCrosshair::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->CreateInclusiveCrosshair() ? 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->DestroyInclusiveCrosshair(); + break; + default: + return DefWindowProc(hWnd, message, wParam, lParam); + } + return 0; +} + +bool InclusiveCrosshair::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 InclusiveCrosshair::Terminate() +{ + auto dispatcherQueue = m_dispatcherQueueController.DispatcherQueue(); + bool enqueueSucceeded = dispatcherQueue.TryEnqueue([=]() { + m_destroyed = true; + DestroyWindow(m_hwndOwner); + }); + if (!enqueueSucceeded) + { + Logger::error("Couldn't enqueue message to destroy the window."); + } +} + +#pragma region InclusiveCrosshair_API + +void InclusiveCrosshairApplySettings(InclusiveCrosshairSettings& settings) +{ + if (InclusiveCrosshair::instance != nullptr) + { + Logger::info("Applying settings."); + InclusiveCrosshair::instance->ApplySettings(settings, true); + } +} + +void InclusiveCrosshairSwitch() +{ + if (InclusiveCrosshair::instance != nullptr) + { + Logger::info("Switching activation mode."); + InclusiveCrosshair::instance->SwitchActivationMode(); + } +} + +void InclusiveCrosshairDisable() +{ + if (InclusiveCrosshair::instance != nullptr) + { + Logger::info("Terminating the crosshair instance."); + InclusiveCrosshair::instance->Terminate(); + } +} + +bool InclusiveCrosshairIsEnabled() +{ + return (InclusiveCrosshair::instance != nullptr); +} + +int InclusiveCrosshairMain(HINSTANCE hInstance, InclusiveCrosshairSettings& settings) +{ + Logger::info("Starting a crosshair instance."); + if (InclusiveCrosshair::instance != nullptr) + { + Logger::error("A crosshair instance was still working when trying to start a new one."); + return 0; + } + + // Perform application initialization: + InclusiveCrosshair crosshair; + InclusiveCrosshair::instance = &crosshair; + crosshair.ApplySettings(settings, false); + if (!crosshair.MyRegisterClass(hInstance)) + { + Logger::error("Couldn't initialize a crosshair instance."); + InclusiveCrosshair::instance = nullptr; + return FALSE; + } + Logger::info("Initialized the crosshair instance."); + + MSG msg; + + // Main message loop: + while (GetMessage(&msg, nullptr, 0, 0)) + { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + + Logger::info("Crosshair message loop ended."); + InclusiveCrosshair::instance = nullptr; + + return (int)msg.wParam; +} + +#pragma endregion InclusiveCrosshair_API diff --git a/src/modules/MouseUtils/MousePointerCrosshair/InclusiveCrosshair.h b/src/modules/MouseUtils/MousePointerCrosshair/InclusiveCrosshair.h new file mode 100644 index 0000000000..086ef709b2 --- /dev/null +++ b/src/modules/MouseUtils/MousePointerCrosshair/InclusiveCrosshair.h @@ -0,0 +1,25 @@ +#pragma once +#include "pch.h" + +constexpr int INCLUSIVE_MOUSE_DEFAULT_CROSSHAIR_OPACITY = 75; +const winrt::Windows::UI::Color INCLUSIVE_MOUSE_DEFAULT_CROSSHAIR_COLOR = winrt::Windows::UI::ColorHelper::FromArgb(255, 255, 0, 0); +const winrt::Windows::UI::Color INCLUSIVE_MOUSE_DEFAULT_CROSSHAIR_BORDER_COLOR = winrt::Windows::UI::ColorHelper::FromArgb(255, 255, 255, 255); +constexpr int INCLUSIVE_MOUSE_DEFAULT_CROSSHAIR_RADIUS = 20; +constexpr int INCLUSIVE_MOUSE_DEFAULT_CROSSHAIR_THICKNESS = 5; +constexpr int INCLUSIVE_MOUSE_DEFAULT_CROSSHAIR_BORDER_SIZE = 1; + +struct InclusiveCrosshairSettings +{ + winrt::Windows::UI::Color crosshairColor = INCLUSIVE_MOUSE_DEFAULT_CROSSHAIR_COLOR; + winrt::Windows::UI::Color crosshairBorderColor = INCLUSIVE_MOUSE_DEFAULT_CROSSHAIR_BORDER_COLOR; + int crosshairRadius = INCLUSIVE_MOUSE_DEFAULT_CROSSHAIR_RADIUS; + int crosshairThickness = INCLUSIVE_MOUSE_DEFAULT_CROSSHAIR_THICKNESS; + int crosshairOpacity = INCLUSIVE_MOUSE_DEFAULT_CROSSHAIR_OPACITY; + int crosshairBorderSize = INCLUSIVE_MOUSE_DEFAULT_CROSSHAIR_BORDER_SIZE; +}; + +int InclusiveCrosshairMain(HINSTANCE hinst, InclusiveCrosshairSettings& settings); +void InclusiveCrosshairDisable(); +bool InclusiveCrosshairIsEnabled(); +void InclusiveCrosshairSwitch(); +void InclusiveCrosshairApplySettings(InclusiveCrosshairSettings& settings); diff --git a/src/modules/MouseUtils/MousePointerCrosshair/MousePointerCrosshair.rc b/src/modules/MouseUtils/MousePointerCrosshair/MousePointerCrosshair.rc new file mode 100644 index 0000000000..5fa3c8b90d --- /dev/null +++ b/src/modules/MouseUtils/MousePointerCrosshair/MousePointerCrosshair.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/MousePointerCrosshair/MousePointerCrosshair.vcxproj b/src/modules/MouseUtils/MousePointerCrosshair/MousePointerCrosshair.vcxproj new file mode 100644 index 0000000000..c1f783ca29 --- /dev/null +++ b/src/modules/MouseUtils/MousePointerCrosshair/MousePointerCrosshair.vcxproj @@ -0,0 +1,145 @@ + + + + + + Debug + x64 + + + Release + x64 + + + + 15.0 + {eae14c0e-7a6b-45da-9080-a7d8c077ba6e} + Win32Proj + MousePointerCrosshair + 10.0.18362.0 + MousePointerCrosshair + + + + + DynamicLibrary + true + v142 + Unicode + + + DynamicLibrary + false + v142 + true + Unicode + + + + + + + + + + + + + + + true + $(SolutionDir)$(Platform)\$(Configuration)\modules\MouseUtils\ + PowerToys.MousePointerCrosshair + + + false + $(SolutionDir)$(Platform)\$(Configuration)\modules\MouseUtils\ + PowerToys.MousePointerCrosshair + + + + 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/MousePointerCrosshair/MousePointerCrosshair.vcxproj.filters b/src/modules/MouseUtils/MousePointerCrosshair/MousePointerCrosshair.vcxproj.filters new file mode 100644 index 0000000000..208f1a0dda --- /dev/null +++ b/src/modules/MouseUtils/MousePointerCrosshair/MousePointerCrosshair.vcxproj.filters @@ -0,0 +1,56 @@ + + + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + + + Header Files + + + Header Files + + + Resource Files + + + Header Files + + + + + + + + {890924c4-f592-4e84-b140-4b07a607a224} + + + {a9a2fd9b-de66-43dc-99b2-56f0d1ddecda} + h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd + + + {b5d0d62e-0275-439b-a910-e7c5befad045} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + {5e350f4d-b07a-4bb2-8e63-b2c527358234} + cpp;c;cc;cxx;c++;def;odl;idl;hpj;bat;asm;asmx + + + + + Resource Files + + + \ No newline at end of file diff --git a/src/modules/MouseUtils/MousePointerCrosshair/dllmain.cpp b/src/modules/MouseUtils/MousePointerCrosshair/dllmain.cpp new file mode 100644 index 0000000000..532069e396 --- /dev/null +++ b/src/modules/MouseUtils/MousePointerCrosshair/dllmain.cpp @@ -0,0 +1,312 @@ +#include "pch.h" +#include +#include +#include "trace.h" +#include "InclusiveCrosshair.h" +#include "common/utils/color.h" + +// Non-Localizable strings +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_CROSSHAIR_COLOR[] = L"crosshair_color"; + const wchar_t JSON_KEY_CROSSHAIR_OPACITY[] = L"crosshair_opacity"; + const wchar_t JSON_KEY_CROSSHAIR_RADIUS[] = L"crosshair_radius"; + const wchar_t JSON_KEY_CROSSHAIR_THICKNESS[] = L"crosshair_thickness"; + const wchar_t JSON_KEY_CROSSHAIR_BORDER_COLOR[] = L"crosshair_border_color"; + const wchar_t JSON_KEY_CROSSHAIR_BORDER_SIZE[] = L"crosshair_border_size"; +} + +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"MousePointerCrosshair"; +// 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 MousePointerCrosshair : public PowertoyModuleIface +{ +private: + // The PowerToy state. + bool m_enabled = false; + + // Hotkey to invoke the module + HotkeyEx m_hotkey; + + // Mouse Pointer Crosshair specific settings + InclusiveCrosshairSettings m_inclusiveCrosshairSettings; + +public: + // Constructor + MousePointerCrosshair() + { + LoggerHelpers::init_logger(MODULE_NAME, L"ModuleInterface", LogSettings::mousePointerCrosshairLoggerName); + 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); + + InclusiveCrosshairApplySettings(m_inclusiveCrosshairSettings); + } + catch (std::exception&) + { + Logger::error("Invalid json when trying to parse Mouse Pointer Crosshair settings json."); + } + } + + // Enable the powertoy + virtual void enable() + { + m_enabled = true; + Trace::EnableMousePointerCrosshair(true); + std::thread([=]() { InclusiveCrosshairMain(m_hModule, m_inclusiveCrosshairSettings); }).detach(); + } + + // Disable the powertoy + virtual void disable() + { + m_enabled = false; + Trace::EnableMousePointerCrosshair(false); + InclusiveCrosshairDisable(); + } + + // Returns if the powertoys is enabled + virtual bool is_enabled() override + { + return m_enabled; + } + + // Returns whether the PowerToys should be enabled by default + virtual bool is_enabled_by_default() const override + { + return false; + } + + virtual std::optional GetHotkeyEx() override + { + return m_hotkey; + } + + virtual void OnHotkeyEx() override + { + InclusiveCrosshairSwitch(); + } + // 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(MousePointerCrosshair::get_key()); + parse_settings(settings); + } + catch (std::exception&) + { + Logger::error("Invalid json when trying to load the Mouse Pointer Crosshair 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(); + InclusiveCrosshairSettings inclusiveCrosshairSettings; + 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 Pointer Crosshair activation shortcut"); + } + try + { + // Parse Opacity + auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_CROSSHAIR_OPACITY); + inclusiveCrosshairSettings.crosshairOpacity = (uint8_t)jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE); + } + catch (...) + { + Logger::warn("Failed to initialize Opacity from settings. Will use default value"); + } + try + { + // Parse crosshair color + auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_CROSSHAIR_COLOR); + auto crosshairColor = (std::wstring)jsonPropertiesObject.GetNamedString(JSON_KEY_VALUE); + uint8_t r, g, b; + if (!checkValidRGB(crosshairColor, &r, &g, &b)) + { + Logger::error("Crosshair color RGB value is invalid. Will use default value"); + } + else + { + inclusiveCrosshairSettings.crosshairColor = winrt::Windows::UI::ColorHelper::FromArgb(255, r, g, b); + } + } + catch (...) + { + Logger::warn("Failed to initialize crosshair color from settings. Will use default value"); + } + try + { + // Parse Radius + auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_CROSSHAIR_RADIUS); + inclusiveCrosshairSettings.crosshairRadius = (UINT)jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE); + } + catch (...) + { + Logger::warn("Failed to initialize Radius from settings. Will use default value"); + } + try + { + // Parse Thickness + auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_CROSSHAIR_THICKNESS); + inclusiveCrosshairSettings.crosshairThickness = (UINT)jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE); + } + catch (...) + { + Logger::warn("Failed to initialize Thickness from settings. Will use default value"); + } + try + { + // Parse crosshair border color + auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_CROSSHAIR_BORDER_COLOR); + auto crosshairBorderColor = (std::wstring)jsonPropertiesObject.GetNamedString(JSON_KEY_VALUE); + uint8_t r, g, b; + if (!checkValidRGB(crosshairBorderColor, &r, &g, &b)) + { + Logger::error("Crosshair border color RGB value is invalid. Will use default value"); + } + else + { + inclusiveCrosshairSettings.crosshairBorderColor = winrt::Windows::UI::ColorHelper::FromArgb(255, r, g, b); + } + } + catch (...) + { + Logger::warn("Failed to initialize crosshair border color from settings. Will use default value"); + } + try + { + // Parse border size + auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_CROSSHAIR_BORDER_SIZE); + inclusiveCrosshairSettings.crosshairBorderSize = (UINT)jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE); + } + catch (...) + { + Logger::warn("Failed to initialize border color from settings. Will use default value"); + } + } + else + { + Logger::info("Mouse Pointer Crosshair settings are empty"); + } + if (!m_hotkey.modifiersMask) + { + Logger::info("Mouse Pointer Crosshair is going to use default shortcut"); + m_hotkey.modifiersMask = MOD_CONTROL | MOD_ALT; + m_hotkey.vkCode = 0x50; // P key + } + m_inclusiveCrosshairSettings = inclusiveCrosshairSettings; + } + +}; + +extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create() +{ + return new MousePointerCrosshair(); +} \ No newline at end of file diff --git a/src/modules/MouseUtils/MousePointerCrosshair/packages.config b/src/modules/MouseUtils/MousePointerCrosshair/packages.config new file mode 100644 index 0000000000..81f107b8bc --- /dev/null +++ b/src/modules/MouseUtils/MousePointerCrosshair/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/modules/MouseUtils/MousePointerCrosshair/pch.cpp b/src/modules/MouseUtils/MousePointerCrosshair/pch.cpp new file mode 100644 index 0000000000..1d9f38c57d --- /dev/null +++ b/src/modules/MouseUtils/MousePointerCrosshair/pch.cpp @@ -0,0 +1 @@ +#include "pch.h" diff --git a/src/modules/MouseUtils/MousePointerCrosshair/pch.h b/src/modules/MouseUtils/MousePointerCrosshair/pch.h new file mode 100644 index 0000000000..5fc459cbc9 --- /dev/null +++ b/src/modules/MouseUtils/MousePointerCrosshair/pch.h @@ -0,0 +1,16 @@ +#pragma once + +#define COMPOSITION +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include diff --git a/src/modules/MouseUtils/MousePointerCrosshair/resource.h b/src/modules/MouseUtils/MousePointerCrosshair/resource.h new file mode 100644 index 0000000000..e4ff6cf76a --- /dev/null +++ b/src/modules/MouseUtils/MousePointerCrosshair/resource.h @@ -0,0 +1,13 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by MousePointerCrosshair.rc + +////////////////////////////// +// Non-localizable + +#define FILE_DESCRIPTION "PowerToys MousePointerCrosshair" +#define INTERNAL_NAME "MousePointerCrosshair" +#define ORIGINAL_FILENAME "PowerToys.MousePointerCrosshair.dll" + +// Non-localizable +////////////////////////////// diff --git a/src/modules/MouseUtils/MousePointerCrosshair/trace.cpp b/src/modules/MouseUtils/MousePointerCrosshair/trace.cpp new file mode 100644 index 0000000000..b4fc60c923 --- /dev/null +++ b/src/modules/MouseUtils/MousePointerCrosshair/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 MousePointerCrosshair enabled or disabled +void Trace::EnableMousePointerCrosshair(const bool enabled) noexcept +{ + TraceLoggingWrite( + g_hProvider, + "MousePointerCrosshair_EnableMousePointerCrosshair", + ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance), + TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE), + TraceLoggingBoolean(enabled, "Enabled")); +} + +// Log that the user activated the module by having the crosshair be drawn +void Trace::StartDrawingCrosshair() noexcept +{ + TraceLoggingWrite( + g_hProvider, + "MousePointerCrosshair_StartDrawingCrosshair", + ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance), + TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE)); +} diff --git a/src/modules/MouseUtils/MousePointerCrosshair/trace.h b/src/modules/MouseUtils/MousePointerCrosshair/trace.h new file mode 100644 index 0000000000..d12eabc44a --- /dev/null +++ b/src/modules/MouseUtils/MousePointerCrosshair/trace.h @@ -0,0 +1,14 @@ +#pragma once + +class Trace +{ +public: + static void RegisterProvider() noexcept; + static void UnregisterProvider() noexcept; + + // Log if the user has MousePointerCrosshair enabled or disabled + static void EnableMousePointerCrosshair(const bool enabled) noexcept; + + // Log that the user activated the module by having the crosshair be drawn + static void StartDrawingCrosshair() noexcept; +}; diff --git a/src/runner/main.cpp b/src/runner/main.cpp index 08b6816d59..f1b9a48960 100644 --- a/src/runner/main.cpp +++ b/src/runner/main.cpp @@ -151,7 +151,7 @@ int runner(bool isProcessElevated, bool openSettings, std::string settingsWindow L"modules/MouseUtils/PowerToys.FindMyMouse.dll" , L"modules/MouseUtils/PowerToys.MouseHighlighter.dll", L"modules/AlwaysOnTop/PowerToys.AlwaysOnTopModuleInterface.dll", - + L"modules/MouseUtils/PowerToys.MousePointerCrosshair.dll", }; const auto VCM_PATH = L"modules/VideoConference/PowerToys.VideoConferenceModule.dll"; if (const auto mf = LoadLibraryA("mf.dll")) diff --git a/src/settings-ui/Settings.UI.Library/EnabledModules.cs b/src/settings-ui/Settings.UI.Library/EnabledModules.cs index 09f0015277..3e074c2aae 100644 --- a/src/settings-ui/Settings.UI.Library/EnabledModules.cs +++ b/src/settings-ui/Settings.UI.Library/EnabledModules.cs @@ -223,6 +223,22 @@ namespace Microsoft.PowerToys.Settings.UI.Library } } + private bool mousePointerCrosshair = true; + + [JsonPropertyName("MousePointerCrosshair")] + public bool MousePointerCrosshair + { + get => mousePointerCrosshair; + set + { + if (mousePointerCrosshair != value) + { + LogTelemetryEvent(value); + mousePointerCrosshair = value; + } + } + } + public string ToJsonString() { return JsonSerializer.Serialize(this); diff --git a/src/settings-ui/Settings.UI.Library/MousePointerCrosshairProperties.cs b/src/settings-ui/Settings.UI.Library/MousePointerCrosshairProperties.cs new file mode 100644 index 0000000000..155add7396 --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/MousePointerCrosshairProperties.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 MousePointerCrosshairProperties + { + [JsonPropertyName("activation_shortcut")] + public HotkeySettings ActivationShortcut { get; set; } + + [JsonPropertyName("crosshair_color")] + public StringProperty CrosshairColor { get; set; } + + [JsonPropertyName("crosshair_opacity")] + public IntProperty CrosshairOpacity { get; set; } + + [JsonPropertyName("crosshair_radius")] + public IntProperty CrosshairRadius { get; set; } + + [JsonPropertyName("crosshair_thickness")] + public IntProperty CrosshairThickness { get; set; } + + [JsonPropertyName("crosshair_border_color")] + public StringProperty CrosshairBorderColor { get; set; } + + [JsonPropertyName("crosshair_border_size")] + public IntProperty CrosshairBorderSize { get; set; } + + public MousePointerCrosshairProperties() + { + ActivationShortcut = new HotkeySettings(false, true, true, false, 0x50); // Ctrl + Alt + P + CrosshairColor = new StringProperty("#FF0000"); + CrosshairOpacity = new IntProperty(75); + CrosshairRadius = new IntProperty(20); + CrosshairThickness = new IntProperty(5); + CrosshairBorderColor = new StringProperty("#FFFFFF"); + CrosshairBorderSize = new IntProperty(1); + } + } +} diff --git a/src/settings-ui/Settings.UI.Library/MousePointerCrosshairSettings.cs b/src/settings-ui/Settings.UI.Library/MousePointerCrosshairSettings.cs new file mode 100644 index 0000000000..08c0aa2a43 --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/MousePointerCrosshairSettings.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 MousePointerCrosshairSettings : BasePTModuleSettings, ISettingsConfig + { + public const string ModuleName = "MousePointerCrosshair"; + + [JsonPropertyName("properties")] + public MousePointerCrosshairProperties Properties { get; set; } + + public MousePointerCrosshairSettings() + { + Name = ModuleName; + Properties = new MousePointerCrosshairProperties(); + 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/Settings.UI.Library/MousePointerCrosshairSettingsIPCMessage.cs b/src/settings-ui/Settings.UI.Library/MousePointerCrosshairSettingsIPCMessage.cs new file mode 100644 index 0000000000..18fa5e21cb --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/MousePointerCrosshairSettingsIPCMessage.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 MousePointerCrosshairSettingsIPCMessage + { + [JsonPropertyName("powertoys")] + public SndMousePointerCrosshairSettings Powertoys { get; set; } + + public MousePointerCrosshairSettingsIPCMessage() + { + } + + public MousePointerCrosshairSettingsIPCMessage(SndMousePointerCrosshairSettings settings) + { + this.Powertoys = settings; + } + + public string ToJsonString() + { + return JsonSerializer.Serialize(this); + } + } +} diff --git a/src/settings-ui/Settings.UI.Library/SndMousePointerCrosshairSettings.cs b/src/settings-ui/Settings.UI.Library/SndMousePointerCrosshairSettings.cs new file mode 100644 index 0000000000..18576b2c38 --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/SndMousePointerCrosshairSettings.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 SndMousePointerCrosshairSettings + { + [JsonPropertyName("MousePointerCrosshair")] + public MousePointerCrosshairSettings MousePointerCrosshair { get; set; } + + public SndMousePointerCrosshairSettings() + { + } + + public SndMousePointerCrosshairSettings(MousePointerCrosshairSettings settings) + { + MousePointerCrosshair = settings; + } + + public string ToJsonString() + { + return JsonSerializer.Serialize(this); + } + } +} diff --git a/src/settings-ui/Settings.UI.Library/ViewModels/MouseUtilsViewModel.cs b/src/settings-ui/Settings.UI.Library/ViewModels/MouseUtilsViewModel.cs index d2c9a6a864..befc0ddd36 100644 --- a/src/settings-ui/Settings.UI.Library/ViewModels/MouseUtilsViewModel.cs +++ b/src/settings-ui/Settings.UI.Library/ViewModels/MouseUtilsViewModel.cs @@ -19,7 +19,9 @@ namespace Microsoft.PowerToys.Settings.UI.Library.ViewModels private MouseHighlighterSettings MouseHighlighterSettingsConfig { get; set; } - public MouseUtilsViewModel(ISettingsUtils settingsUtils, ISettingsRepository settingsRepository, ISettingsRepository findMyMouseSettingsRepository, ISettingsRepository mouseHighlighterSettingsRepository, Func ipcMSGCallBackFunc) + private MousePointerCrosshairSettings MousePointerCrosshairSettingsConfig { get; set; } + + public MouseUtilsViewModel(ISettingsUtils settingsUtils, ISettingsRepository settingsRepository, ISettingsRepository findMyMouseSettingsRepository, ISettingsRepository mouseHighlighterSettingsRepository, ISettingsRepository mousePointerCrosshairSettingsRepository, Func ipcMSGCallBackFunc) { SettingsUtils = settingsUtils; @@ -35,6 +37,8 @@ namespace Microsoft.PowerToys.Settings.UI.Library.ViewModels _isMouseHighlighterEnabled = GeneralSettingsConfig.Enabled.MouseHighlighter; + _isMousePointerCrosshairEnabled = GeneralSettingsConfig.Enabled.MousePointerCrosshair; + // 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) @@ -73,6 +77,24 @@ namespace Microsoft.PowerToys.Settings.UI.Library.ViewModels _highlightFadeDelayMs = MouseHighlighterSettingsConfig.Properties.HighlightFadeDelayMs.Value; _highlightFadeDurationMs = MouseHighlighterSettingsConfig.Properties.HighlightFadeDurationMs.Value; + if (mousePointerCrosshairSettingsRepository == null) + { + throw new ArgumentNullException(nameof(mousePointerCrosshairSettingsRepository)); + } + + MousePointerCrosshairSettingsConfig = mousePointerCrosshairSettingsRepository.SettingsConfig; + + string crosshairColor = MousePointerCrosshairSettingsConfig.Properties.CrosshairColor.Value; + _mousePointerCrosshairColor = !string.IsNullOrEmpty(crosshairColor) ? crosshairColor : "#FF0000"; + + string crosshairBorderColor = MousePointerCrosshairSettingsConfig.Properties.CrosshairBorderColor.Value; + _mousePointerCrosshairBorderColor = !string.IsNullOrEmpty(crosshairBorderColor) ? crosshairBorderColor : "#FFFFFF"; + + _mousePointerCrosshairOpacity = MousePointerCrosshairSettingsConfig.Properties.CrosshairOpacity.Value; + _mousePointerCrosshairRadius = MousePointerCrosshairSettingsConfig.Properties.CrosshairRadius.Value; + _mousePointerCrosshairThickness = MousePointerCrosshairSettingsConfig.Properties.CrosshairThickness.Value; + _mousePointerCrosshairBorderSize = MousePointerCrosshairSettingsConfig.Properties.CrosshairBorderSize.Value; + // set the callback functions value to handle outgoing IPC message. SendConfigMSG = ipcMSGCallBackFunc; } @@ -398,6 +420,169 @@ namespace Microsoft.PowerToys.Settings.UI.Library.ViewModels SettingsUtils.SaveSettings(MouseHighlighterSettingsConfig.ToJsonString(), MouseHighlighterSettings.ModuleName); } + public bool IsMousePointerCrosshairEnabled + { + get => _isMousePointerCrosshairEnabled; + set + { + if (_isMousePointerCrosshairEnabled != value) + { + _isMousePointerCrosshairEnabled = value; + + GeneralSettingsConfig.Enabled.MousePointerCrosshair = value; + OnPropertyChanged(nameof(_isMousePointerCrosshairEnabled)); + + OutGoingGeneralSettings outgoing = new OutGoingGeneralSettings(GeneralSettingsConfig); + SendConfigMSG(outgoing.ToString()); + + NotifyMousePointerCrosshairPropertyChanged(); + } + } + } + + public HotkeySettings MousePointerCrosshairActivationShortcut + { + get + { + return MousePointerCrosshairSettingsConfig.Properties.ActivationShortcut; + } + + set + { + if (MousePointerCrosshairSettingsConfig.Properties.ActivationShortcut != value) + { + MousePointerCrosshairSettingsConfig.Properties.ActivationShortcut = value; + NotifyMousePointerCrosshairPropertyChanged(); + } + } + } + + public string MousePointerCrosshairColor + { + get + { + return _mousePointerCrosshairColor; + } + + 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(_mousePointerCrosshairColor, StringComparison.OrdinalIgnoreCase)) + { + _mousePointerCrosshairColor = value; + MousePointerCrosshairSettingsConfig.Properties.CrosshairColor.Value = value; + NotifyMousePointerCrosshairPropertyChanged(); + } + } + } + + public int MousePointerCrosshairOpacity + { + get + { + return _mousePointerCrosshairOpacity; + } + + set + { + if (value != _mousePointerCrosshairOpacity) + { + _mousePointerCrosshairOpacity = value; + MousePointerCrosshairSettingsConfig.Properties.CrosshairOpacity.Value = value; + NotifyMousePointerCrosshairPropertyChanged(); + } + } + } + + public int MousePointerCrosshairRadius + { + get + { + return _mousePointerCrosshairRadius; + } + + set + { + if (value != _mousePointerCrosshairRadius) + { + _mousePointerCrosshairRadius = value; + MousePointerCrosshairSettingsConfig.Properties.CrosshairRadius.Value = value; + NotifyMousePointerCrosshairPropertyChanged(); + } + } + } + + public int MousePointerCrosshairThickness + { + get + { + return _mousePointerCrosshairThickness; + } + + set + { + if (value != _mousePointerCrosshairThickness) + { + _mousePointerCrosshairThickness = value; + MousePointerCrosshairSettingsConfig.Properties.CrosshairThickness.Value = value; + NotifyMousePointerCrosshairPropertyChanged(); + } + } + } + + public string MousePointerCrosshairBorderColor + { + get + { + return _mousePointerCrosshairBorderColor; + } + + 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(_mousePointerCrosshairBorderColor, StringComparison.OrdinalIgnoreCase)) + { + _mousePointerCrosshairBorderColor = value; + MousePointerCrosshairSettingsConfig.Properties.CrosshairBorderColor.Value = value; + NotifyMousePointerCrosshairPropertyChanged(); + } + } + } + + public int MousePointerCrosshairBorderSize + { + get + { + return _mousePointerCrosshairBorderSize; + } + + set + { + if (value != _mousePointerCrosshairBorderSize) + { + _mousePointerCrosshairBorderSize = value; + MousePointerCrosshairSettingsConfig.Properties.CrosshairBorderSize.Value = value; + NotifyMousePointerCrosshairPropertyChanged(); + } + } + } + + public void NotifyMousePointerCrosshairPropertyChanged([CallerMemberName] string propertyName = null) + { + OnPropertyChanged(propertyName); + + SndMousePointerCrosshairSettings outsettings = new SndMousePointerCrosshairSettings(MousePointerCrosshairSettingsConfig); + SndModuleSettings ipcMessage = new SndModuleSettings(outsettings); + SendConfigMSG(ipcMessage.ToJsonString()); + SettingsUtils.SaveSettings(MousePointerCrosshairSettingsConfig.ToJsonString(), MousePointerCrosshairSettings.ModuleName); + } + private Func SendConfigMSG { get; } private bool _isFindMyMouseEnabled; @@ -416,5 +601,13 @@ namespace Microsoft.PowerToys.Settings.UI.Library.ViewModels private int _highlighterRadius; private int _highlightFadeDelayMs; private int _highlightFadeDurationMs; + + private bool _isMousePointerCrosshairEnabled; + private string _mousePointerCrosshairColor; + private int _mousePointerCrosshairOpacity; + private int _mousePointerCrosshairRadius; + private int _mousePointerCrosshairThickness; + private string _mousePointerCrosshairBorderColor; + private int _mousePointerCrosshairBorderSize; } } diff --git a/src/settings-ui/Settings.UI/OOBE/Views/OobeMouseUtils.xaml b/src/settings-ui/Settings.UI/OOBE/Views/OobeMouseUtils.xaml index 5ac7945097..5f1cca7e33 100644 --- a/src/settings-ui/Settings.UI/OOBE/Views/OobeMouseUtils.xaml +++ b/src/settings-ui/Settings.UI/OOBE/Views/OobeMouseUtils.xaml @@ -22,6 +22,10 @@ Style="{ThemeResource OobeSubtitleStyle}" /> + + +