2d5276f742
* New PowerToys template * Add CppWinRt to empty PowerToy * Add Settings reference to empty PowerToy * Use proper output dir * Proper WindowsTargetPlatformVersion * Add filters to vcxproj * Proper resource file generation * Add MouseHighlighter proof of concept code * Abstract implementation into a struct * Enable module * Disable module * Add enable module to settings page * Can change the hotkey in settings * Remove remaining boilerplate code * Add logging * Add telemetry * Add Oobe entry * Add installer instructions * Add dll to pipelines * fix spellchecker * Add more configurability * Make settings a bit prettier * Fix spellchecker * Fix wrong default fade timers * Fix user facing strings * Tweak default duration values * Fix to appear in every virtual desktop * [Mouse Highlighter] Show highlight on mouse drag (#14529) * [Mouse Highlighter]show pointer on mouse drag * fix spellchecker * [MU] UI tweaks (#14544) * UI tweaks * Update Resources.resw * Updated text strings * fix spellcheck Co-authored-by: Laute <Niels.Laute@philips.com> * tweak default values * PR feedback: use wstring_view * PR feedback: Log error on json error * PR feedback: don't throw 1 * PR feedback: fix copy-pasta leftColor->rightColor * PR feedback:Add another error message on exception * PR feedback: add todo to use commons/utils/json.h Co-authored-by: Niels Laute <niels.laute@live.nl> Co-authored-by: Laute <Niels.Laute@philips.com>
444 lines
13 KiB
C++
444 lines
13 KiB
C++
// 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<ABI::ICompositorDesktopInterop>()->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<winrt::Windows::UI::Composition::CompositionColorBrush>().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<int, std::ratio<1, 1000>>;
|
|
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
|