Compare commits

...

1 Commits

Author SHA1 Message Date
Niels Laute
c291abc32c [MouseHighlighter] Add Input Highlighter (on-screen keystroke overlay)
Extends the Mouse Highlighter module with an Input Highlighter that shows
the keys and shortcuts you press as an on-screen overlay, with display
modes (recent keys, single key, shortcuts, text stream), configurable
position, colors, size, timeout, draggable overlay, and monitor/display-
mode switch shortcuts. Includes the native module changes, Settings UI
(new Input Highlighter page, MouseUtils settings + resources) and unit
tests.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-07-06 17:05:10 +02:00
30 changed files with 3395 additions and 136 deletions

View File

@@ -0,0 +1,113 @@
# Input Highlighter (dev notes)
> Working design doc for merging the keystroke overlay (team4 `KeystrokeOverlay`) into
> the existing **Mouse Highlighter** module to form a single utility, **Input Highlighter**.
> Status: in progress. See the session plan for phase/todo tracking.
## Summary
Input Highlighter is the existing Mouse Highlighter module extended to also visualize
keyboard input. It keeps Mouse Highlighter's internal identity for a frictionless upgrade
(see *Identity & migration*) and only changes the user-facing display name.
All rendering is native: **C++ + Windows.UI.Composition + Direct2D/DirectWrite**, in a
single in-process module. team4's *capture* and *display logic* are reused (ported to
C++); team4's WinUI UI, named-pipe IPC, and separate-process design are dropped.
## Identity & migration (Option A)
- Internal module key/GUID stays `MouseHighlighter`; DLL stays
`PowerToys.MouseHighlighter.dll`. Runner (`src/runner/main.cpp` known-DLL list), the GPO
key (`getConfiguredMouseHighlighterEnabledValue`), and the installer component are
therefore unchanged.
- Only display strings become "Input Highlighter" (`Resources.resw`, dashboard, OOBE,
`ModuleHelper`).
- Settings migrate in place via `MouseHighlighterSettings.UpgradeSettingsConfiguration()`
(`ISettingsConfig`) with a `Version` bump — same pattern as `ColorPickerSettings`.
New keystroke fields get defaults; existing mouse settings + enabled state are
preserved. Migrated users default to **mouse-only** (`show_keystrokes = false`); fresh
installs enable both.
## Architecture
Single native DLL, evolved from `src/modules/MouseUtils/MouseHighlighter`.
```
WH_MOUSE_LL hook ─┐ (existing, unchanged)
├─► Highlighter (Composition ShapeVisual) ─► ripples/spotlight
WH_KEYBOARD_LL hook ─┐ │
(new) │ │
▼ │
in-process SPSC queue │ (ported EventQueue.h — no named pipe)
│ │
▼ ▼
KeystrokeProcessor (pure C++) ── display-mode state machine
Keystroke renderer (Composition + D2D/DirectWrite key "pills")
```
### Threading
- The LL keyboard hook callback stays lean: format nothing, allocate nothing — just push
a POD `KeystrokeEvent` onto a lock-free SPSC ring (ported from team4 `EventQueue.h`).
- The Composition/dispatcher thread (the module already owns a `DispatcherQueue` — see
`Highlighter::CreateHighlighter`) drains the queue, runs `KeystrokeProcessor`, and
updates the visual tree.
### Rendering keystroke "pills" in Composition
Mouse Highlighter already builds a `Compositor` + `DesktopWindowTarget` + `ContainerVisual`
root with a `ShapeVisual` (`MouseHighlighter.cpp: CreateHighlighter`). Text/glyph content
is added as follows:
- Create a `CompositionGraphicsDevice` from the compositor
(`ICompositorInterop::CreateGraphicsDevice`) backed by a D3D/D2D device.
- Each key pill = a `SpriteVisual` whose brush is a `CompositionSurfaceBrush` over a
`CompositionDrawingSurface`. Draw with Direct2D: rounded-rect chrome + text/glyph via
DirectWrite (**Segoe Fluent Icons** for glyphs, matching team4's KeyVisual).
- Pills are arranged in a horizontal container; newest at one end. Per-position opacity
fade + expiry animations via Composition (`ScalarKeyFrameAnimation`), mirroring team4's
opacity ordering and `TimeoutMs` removal.
### Overlay behavior
- Reuse Mouse Highlighter's layered, click-through, no-activate overlay window
(`WS_EX_TRANSPARENT | WS_EX_NOACTIVATE | WS_EX_LAYERED`). This fixes team4's WinUI
click-interception problem for free.
- Positioning: corner + per-monitor (settings), with a monitor-switch hotkey. Dragging
is an **opt-in "move mode"** hotkey that temporarily clears `WS_EX_TRANSPARENT`, rather
than an always-draggable window (which would defeat click-through).
## Logic ported from team4 (reference: `origin/feature/kbhighlighter-team4`)
| team4 source | Ported to | Notes |
|---|---|---|
| `KeyboardService/KeyboardListener.cpp` | native capture in-module | `WH_KEYBOARD_LL`, `ToUnicodeEx`, modifier snapshot. Drop the pipe/batcher; push to in-process queue. |
| `KeyboardService/EventQueue.h` | in-process queue | lock-free SPSC ring; keep as-is. |
| `Controls/KeystrokeEvent.cs` | `KeystrokeFormatter` (C++) | `ToString()` (display string), `IsShortcut`, `IsCommandKey`, `GetKeyName`, `GetModifierSymbol`. Pure + unit-testable. |
| `Services/KeystrokeProcessor.cs` | `KeystrokeProcessor` (C++) | `Add`/`ReplaceLast`/`RemoveLast`/`None` + stream buffer/backspace. Pure + unit-testable. |
| `Controls/KeyVisual/KeyVisual.xaml.cs` | D2D glyph map | Segoe Fluent Icons: Enter `\uE751`, Back `\uE750`, Shift `\uE752`, arrows `\uE0E2..\uE0E5`. |
| `Models/Enums/DisplayMode.cs` | `DisplayMode` enum | Last5 / SingleCharactersOnly / ShortcutsOnly / Stream. |
## Settings (extends the `MouseHighlighter` blob)
Existing mouse props unchanged. New (namespaced `keystroke_*`) props:
`show_mouse`, `show_keystrokes`, `keystroke_display_mode`, `keystroke_timeout_ms`,
`keystroke_text_size`, `keystroke_text_color`, `keystroke_background_color`,
`keystroke_text_opacity`, `keystroke_bg_opacity`, `keystroke_position`,
`keystroke_switch_monitor_hotkey`, `keystroke_switch_display_mode_hotkey`.
Touched files: `MouseHighlighterProperties.cs`, `MouseHighlighterSettings.cs` (+Version,
+`UpgradeSettingsConfiguration`), `SndMouseHighlighterSettings.cs`,
`MouseHighlighterSettingsIPCMessage.cs`, `SettingsSerializationContext`, native
`MouseHighlighter.h` settings struct + `dllmain.cpp` parse/apply.
## Testing
- Unit tests for `KeystrokeFormatter` (glyph/shortcut/char formatting) and
`KeystrokeProcessor` (mode transitions, stream backspace, expiry ordering) — factor the
pure logic so it builds without Win32.
- **Fuzz test** for keystroke input parsing/formatting (required for a new user-input
surface per repo guidelines).
## Open risks
- Direct2D/DirectWrite text quality vs. the XAML KeyVisual look — **de-risk with a spike
before full parity** (render one pill on the existing overlay window).
- Segoe Fluent Icons availability/fallback across supported OS versions.
- Keeping the keyboard-hook hot path allocation-free while marshaling to the Composition
thread.

View File

@@ -0,0 +1,105 @@
// 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.
#include "pch.h"
#include "KeystrokeFormatter.h"
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
using namespace InputHighlighter;
namespace
{
#pragma warning(push)
#pragma warning(disable : 26497) // test helper; constexpr not required
KeystrokeEvent MakeKey(uint32_t vk, char32_t ch, bool ctrl = false, bool alt = false, bool shift = false, bool win = false)
{
KeystrokeEvent e{};
e.type = KeystrokeEventType::Down;
e.vk = vk;
e.ch = ch;
e.mods = { ctrl, alt, shift, win };
return e;
}
#pragma warning(pop)
}
namespace MouseHighlighterKeystrokeTests
{
TEST_CLASS(KeystrokeFormatterTests)
{
public:
TEST_METHOD(PlainLetter_ShowsCharacter)
{
const auto e = MakeKey('A', U'a');
Assert::AreEqual(std::wstring(L"a"), Formatter::Format(e));
Assert::IsFalse(Formatter::IsShortcut(e));
}
TEST_METHOD(ShiftedCharacter_HidesShiftModifier)
{
// Shift is held to produce "!", but the glyph already implies it.
const auto e = MakeKey('1', U'!', false, false, true, false);
Assert::AreEqual(std::wstring(L"!"), Formatter::Format(e));
// Any held modifier still classifies it as a shortcut.
Assert::IsTrue(Formatter::IsShortcut(e));
}
TEST_METHOD(CtrlC_ShowsCombination)
{
// Ctrl+C yields a non-printable char, so ch is 0.
const auto e = MakeKey('C', 0, true, false, false, false);
Assert::AreEqual(std::wstring(L"Ctrl + C"), Formatter::Format(e));
Assert::IsTrue(Formatter::IsShortcut(e));
}
TEST_METHOD(EnterKey_ShowsName_AndIsShortcut)
{
const auto e = MakeKey(VK_RETURN, 0);
Assert::AreEqual(std::wstring(L"Enter"), Formatter::Format(e));
Assert::IsTrue(Formatter::IsShortcut(e));
}
TEST_METHOD(ModifierKeyAlone_IsNotDuplicated)
{
// Pressing Ctrl by itself: modifier snapshot has Ctrl, key is Ctrl.
const auto e = MakeKey(VK_CONTROL, 0, true, false, false, false);
Assert::AreEqual(std::wstring(L"Ctrl"), Formatter::Format(e));
}
TEST_METHOD(ArrowKey_UsesGlyph)
{
const auto e = MakeKey(VK_LEFT, 0);
Assert::AreEqual(std::wstring(L"\u2190"), Formatter::Format(e));
}
TEST_METHOD(WinPlusLetter_ShowsWinGlyph)
{
const auto e = MakeKey('D', U'd', false, false, false, true);
Assert::AreEqual(std::wstring(L"\u229E + D"), Formatter::Format(e));
}
TEST_METHOD(KeyUp_ReturnsEmpty)
{
auto e = MakeKey('A', U'a');
e.type = KeystrokeEventType::Up;
Assert::AreEqual(std::wstring(L""), Formatter::Format(e));
}
TEST_METHOD(Whitespace_ReturnsEmpty)
{
const auto e = MakeKey(VK_SPACE, U' ');
Assert::AreEqual(std::wstring(L""), Formatter::Format(e));
}
TEST_METHOD(IsCommandKey_Classification)
{
Assert::IsTrue(Formatter::IsCommandKey(VK_RETURN));
Assert::IsTrue(Formatter::IsCommandKey(VK_F5));
Assert::IsTrue(Formatter::IsCommandKey(VK_LEFT));
Assert::IsFalse(Formatter::IsCommandKey('A'));
Assert::IsFalse(Formatter::IsCommandKey('1'));
}
};
}

View File

@@ -0,0 +1,106 @@
// 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.
#include "pch.h"
#include "KeystrokeProcessor.h"
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
using namespace InputHighlighter;
namespace
{
#pragma warning(push)
#pragma warning(disable : 26497) // test helper; constexpr not required
KeystrokeEvent MakeKey(uint32_t vk, char32_t ch, bool ctrl = false, bool alt = false, bool shift = false, bool win = false)
{
KeystrokeEvent e{};
e.type = KeystrokeEventType::Down;
e.vk = vk;
e.ch = ch;
e.mods = { ctrl, alt, shift, win };
return e;
}
#pragma warning(pop)
}
namespace MouseHighlighterKeystrokeTests
{
TEST_CLASS(KeystrokeProcessorTests)
{
public:
TEST_METHOD(Last5_AlwaysAdds)
{
KeystrokeProcessor p;
auto r = p.Process(MakeKey('A', U'a'), DisplayMode::Last5);
Assert::IsTrue(r.action == KeystrokeAction::Add);
Assert::AreEqual(std::wstring(L"a"), r.text);
}
TEST_METHOD(SingleCharactersOnly_IgnoresShortcuts)
{
KeystrokeProcessor p;
auto plain = p.Process(MakeKey('A', U'a'), DisplayMode::SingleCharactersOnly);
Assert::IsTrue(plain.action == KeystrokeAction::Add);
auto shortcut = p.Process(MakeKey('C', 0, true), DisplayMode::SingleCharactersOnly);
Assert::IsTrue(shortcut.action == KeystrokeAction::None);
}
TEST_METHOD(ShortcutsOnly_IgnoresPlainCharacters)
{
KeystrokeProcessor p;
auto plain = p.Process(MakeKey('A', U'a'), DisplayMode::ShortcutsOnly);
Assert::IsTrue(plain.action == KeystrokeAction::None);
auto shortcut = p.Process(MakeKey('C', 0, true), DisplayMode::ShortcutsOnly);
Assert::IsTrue(shortcut.action == KeystrokeAction::Add);
Assert::AreEqual(std::wstring(L"Ctrl + C"), shortcut.text);
}
TEST_METHOD(Stream_BuildsAndReplacesWord)
{
KeystrokeProcessor p;
auto h = p.Process(MakeKey('H', U'h'), DisplayMode::Stream);
Assert::IsTrue(h.action == KeystrokeAction::Add);
Assert::AreEqual(std::wstring(L"h"), h.text);
auto i = p.Process(MakeKey('I', U'i'), DisplayMode::Stream);
Assert::IsTrue(i.action == KeystrokeAction::ReplaceLast);
Assert::AreEqual(std::wstring(L"hi"), i.text);
}
TEST_METHOD(Stream_BackspaceEditsThenRemoves)
{
KeystrokeProcessor p;
p.Process(MakeKey('H', U'h'), DisplayMode::Stream);
p.Process(MakeKey('I', U'i'), DisplayMode::Stream);
auto back1 = p.Process(MakeKey(VK_BACK, 0), DisplayMode::Stream);
Assert::IsTrue(back1.action == KeystrokeAction::ReplaceLast);
Assert::AreEqual(std::wstring(L"h"), back1.text);
auto back2 = p.Process(MakeKey(VK_BACK, 0), DisplayMode::Stream);
Assert::IsTrue(back2.action == KeystrokeAction::RemoveLast);
auto back3 = p.Process(MakeKey(VK_BACK, 0), DisplayMode::Stream);
Assert::IsTrue(back3.action == KeystrokeAction::None);
}
TEST_METHOD(Stream_ShortcutResetsBuffer)
{
KeystrokeProcessor p;
p.Process(MakeKey('A', U'a'), DisplayMode::Stream); // buffer "a"
auto shortcut = p.Process(MakeKey('S', 0, true), DisplayMode::Stream);
Assert::IsTrue(shortcut.action == KeystrokeAction::Add);
Assert::AreEqual(std::wstring(L"Ctrl + S"), shortcut.text);
// Buffer was reset, so the next character starts a new pill.
auto b = p.Process(MakeKey('B', U'b'), DisplayMode::Stream);
Assert::IsTrue(b.action == KeystrokeAction::Add);
Assert::AreEqual(std::wstring(L"b"), b.text);
}
};
}

View File

@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>16.0</VCProjectVersion>
<ProjectGuid>{4E2D9B71-3C6A-4F1E-9D2B-7A8C5E1F0B34}</ProjectGuid>
<Keyword>Win32Proj</Keyword>
<RootNamespace>MouseHighlighterKeystrokeTests</RootNamespace>
<ProjectSubType>NativeUnitTestProject</ProjectSubType>
<ProjectName>MouseHighlighter.UnitTests</ProjectName>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseOfMfc>false</UseOfMfc>
<PlatformToolset>v143</PlatformToolset>
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\tests\MouseHighlighterUnitTests\</OutDir>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<ItemDefinitionGroup>
<ClCompile>
<AdditionalIncludeDirectories>..\MouseHighlighter\Keystrokes;$(VCInstallDir)UnitTest\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<LanguageStandard>stdcpp20</LanguageStandard>
</ClCompile>
<Link>
<AdditionalLibraryDirectories>$(VCInstallDir)UnitTest\lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="pch.cpp">
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
</ClCompile>
<ClCompile Include="KeystrokeFormatter.Tests.cpp" />
<ClCompile Include="KeystrokeProcessor.Tests.cpp" />
<ClCompile Include="..\MouseHighlighter\Keystrokes\KeystrokeFormatter.cpp">
<PrecompiledHeader>NotUsing</PrecompiledHeader>
</ClCompile>
<ClCompile Include="..\MouseHighlighter\Keystrokes\KeystrokeProcessor.cpp">
<PrecompiledHeader>NotUsing</PrecompiledHeader>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="pch.h" />
<ClInclude Include="..\MouseHighlighter\Keystrokes\KeystrokeTypes.h" />
<ClInclude Include="..\MouseHighlighter\Keystrokes\KeystrokeFormatter.h" />
<ClInclude Include="..\MouseHighlighter\Keystrokes\KeystrokeProcessor.h" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
</Project>

View File

@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Source Files">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
</Filter>
<Filter Include="Header Files">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
</Filter>
<Filter Include="Module Source">
<UniqueIdentifier>{7E4A9C21-1D3B-4E5F-8A6C-2B9D0E1F3A45}</UniqueIdentifier>
</Filter>
</ItemGroup>
<ItemGroup>
<ClCompile Include="pch.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="KeystrokeFormatter.Tests.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="KeystrokeProcessor.Tests.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\MouseHighlighter\Keystrokes\KeystrokeFormatter.cpp">
<Filter>Module Source</Filter>
</ClCompile>
<ClCompile Include="..\MouseHighlighter\Keystrokes\KeystrokeProcessor.cpp">
<Filter>Module Source</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="pch.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="..\MouseHighlighter\Keystrokes\KeystrokeTypes.h">
<Filter>Module Source</Filter>
</ClInclude>
<ClInclude Include="..\MouseHighlighter\Keystrokes\KeystrokeFormatter.h">
<Filter>Module Source</Filter>
</ClInclude>
<ClInclude Include="..\MouseHighlighter\Keystrokes\KeystrokeProcessor.h">
<Filter>Module Source</Filter>
</ClInclude>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,2 @@
// pch.cpp: source file corresponding to the pre-compiled header.
#include "pch.h"

View File

@@ -0,0 +1,17 @@
// pch.h: precompiled header for the Input Highlighter keystroke unit tests.
#ifndef PCH_H
#define PCH_H
#include <Windows.h>
#include <array>
#include <string>
#include <vector>
// Suppressing 26466 - Don't use static_cast downcasts - in CppUnitTest.h
#pragma warning(push)
#pragma warning(disable : 26466)
#include "CppUnitTest.h"
#pragma warning(pop)
#endif // PCH_H

View File

@@ -0,0 +1,254 @@
// 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.
#include "KeyboardCapture.h"
#include <array>
namespace InputHighlighter
{
KeyboardCapture* KeyboardCapture::s_instance = nullptr;
namespace
{
uint64_t NowMicros()
{
static LARGE_INTEGER freq = [] {
LARGE_INTEGER f;
QueryPerformanceFrequency(&f);
return f;
}();
LARGE_INTEGER c;
QueryPerformanceCounter(&c);
return static_cast<uint64_t>((c.QuadPart * 1'000'000) / freq.QuadPart);
}
#pragma warning(push)
#pragma warning(disable : 26497) // runtime key-state calls; cannot be constexpr
// Read the *physical* key state. A low-level keyboard hook runs on a
// dedicated background thread that never owns keyboard focus, so the
// per-thread state (GetKeyState/GetKeyboardState) is not synchronized
// with the real keys. That makes held modifiers read as up whenever focus
// is on another process (e.g. the desktop), which breaks shortcut
// detection. GetAsyncKeyState reports the true hardware state regardless
// of focus.
std::array<bool, 4> SnapshotMods()
{
auto down = [](int vk) { return (GetAsyncKeyState(vk) & 0x8000) != 0; };
return std::array<bool, 4>{
down(VK_CONTROL),
down(VK_MENU),
down(VK_SHIFT),
(down(VK_LWIN) || down(VK_RWIN)),
};
}
// The keyboard layout of whatever window currently has focus, falling
// back to the hook thread's own layout. Ensures characters translate
// using the layout the user is actually typing against.
HKL ForegroundLayout()
{
const HWND fg = GetForegroundWindow();
const DWORD tid = fg ? GetWindowThreadProcessId(fg, nullptr) : 0;
return GetKeyboardLayout(tid);
}
// Translate a virtual key + scan code into a printable character, if any.
// Non-printable keys (Enter, arrows, control combos, ...) yield 0.
char32_t VkToChar(UINT vk, UINT scanCode)
{
// Seed toggle keys (Caps/Num/Scroll Lock) from the cached state, then
// overlay the live physical modifier state so Shift/AltGr characters
// are correct even when another process (e.g. the desktop) has focus.
BYTE keyState[256] = {};
GetKeyboardState(keyState);
auto syncAsync = [&keyState](int key) {
keyState[key] = static_cast<BYTE>((GetAsyncKeyState(key) & 0x8000) ? 0x80 : 0x00);
};
for (const int key : { VK_SHIFT, VK_LSHIFT, VK_RSHIFT,
VK_CONTROL, VK_LCONTROL, VK_RCONTROL,
VK_MENU, VK_LMENU, VK_RMENU })
{
syncAsync(key);
}
WCHAR buf[4] = { 0 };
HKL layout = ForegroundLayout();
// ToUnicodeEx can mutate dead-key state; acceptable for an overlay.
const int rc = ToUnicodeEx(vk, scanCode, keyState, buf, ARRAYSIZE(buf), 0, layout);
if (rc <= 0)
{
return 0;
}
if (!iswprint(buf[0]))
{
return 0;
}
return static_cast<char32_t>(buf[0]);
}
#pragma warning(pop)
}
KeyboardCapture::~KeyboardCapture()
{
Stop();
}
bool KeyboardCapture::Start(std::function<void()> notify)
{
if (m_running.load(std::memory_order_acquire))
{
return true;
}
m_notify = std::move(notify);
s_instance = this;
m_running.store(true, std::memory_order_release);
m_thread = std::thread(&KeyboardCapture::ThreadMain, this);
return true;
}
void KeyboardCapture::Stop()
{
if (!m_running.exchange(false))
{
if (m_thread.joinable())
{
m_thread.join();
}
return;
}
if (m_threadId != 0)
{
PostThreadMessageW(m_threadId, WM_QUIT, 0, 0);
}
if (m_thread.joinable())
{
m_thread.join();
}
if (s_instance == this)
{
s_instance = nullptr;
}
}
void KeyboardCapture::ThreadMain()
{
m_threadId = GetCurrentThreadId();
m_hook = SetWindowsHookExW(WH_KEYBOARD_LL, HookProc, GetModuleHandleW(nullptr), 0);
if (m_hook == nullptr)
{
m_running.store(false, std::memory_order_release);
return;
}
MSG msg;
while (GetMessageW(&msg, nullptr, 0, 0) > 0)
{
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
if (m_hook != nullptr)
{
UnhookWindowsHookEx(m_hook);
m_hook = nullptr;
}
m_threadId = 0;
}
void KeyboardCapture::EmitDown(UINT vk, UINT scanCode)
{
KeystrokeEvent e{};
e.type = KeystrokeEventType::Down;
e.vk = vk;
e.ch = VkToChar(vk, scanCode);
e.mods = SnapshotMods();
e.tsMicros = NowMicros();
m_queue.try_push(e);
if (m_notify)
{
m_notify();
}
}
bool KeyboardCapture::HandleKeyDown(UINT vk, UINT scanCode)
{
const auto mods = SnapshotMods();
int hotkeyId = -1;
if (m_switchMonitorHotkey.Matches(vk, mods))
{
hotkeyId = 0;
}
else if (m_cycleDisplayModeHotkey.Matches(vk, mods))
{
hotkeyId = 1;
}
if (hotkeyId >= 0)
{
// Fire once per physical press; swallow auto-repeat while held.
if (m_activeHotkeyVk != vk)
{
m_activeHotkeyVk = vk;
if (m_onHotkey)
{
m_onHotkey(hotkeyId);
}
}
return true; // swallow: don't type or display the shortcut key
}
EmitDown(vk, scanCode);
return false;
}
void KeyboardCapture::HandleKeyUp(UINT vk)
{
if (m_activeHotkeyVk == vk)
{
m_activeHotkeyVk = 0;
}
}
LRESULT CALLBACK KeyboardCapture::HookProc(int nCode, WPARAM wParam, LPARAM lParam)
{
if (nCode == HC_ACTION && s_instance != nullptr)
{
const KBDLLHOOKSTRUCT* p = reinterpret_cast<KBDLLHOOKSTRUCT*>(lParam);
switch (wParam)
{
case WM_KEYDOWN:
case WM_SYSKEYDOWN:
if (s_instance->HandleKeyDown(p->vkCode, p->scanCode))
{
return 1; // swallow matched control shortcut
}
break;
case WM_KEYUP:
case WM_SYSKEYUP:
s_instance->HandleKeyUp(p->vkCode);
break;
default:
break;
}
}
return CallNextHookEx(nullptr, nCode, wParam, lParam);
}
}

View File

@@ -0,0 +1,79 @@
// 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.
// Installs a low-level keyboard hook on a dedicated message-loop thread and
// translates raw input into KeystrokeEvent values on an in-process lock-free
// queue. This replaces team4's separate keyboard service + named-pipe IPC with
// direct in-process capture, mirroring how Mouse Highlighter hooks the mouse.
#pragma once
#include <functional>
#include <thread>
#include <Windows.h>
#include "KeystrokeTypes.h"
namespace InputHighlighter
{
class KeyboardCapture
{
public:
KeyboardCapture() = default;
~KeyboardCapture();
KeyboardCapture(const KeyboardCapture&) = delete;
KeyboardCapture& operator=(const KeyboardCapture&) = delete;
// Starts capturing. 'notify' is invoked (on the capture thread) after events
// are enqueued so the consumer can drain via TryPop; keep it lean (e.g. post
// a message or set an event). Returns false if the hook could not be set.
bool Start(std::function<void()> notify);
// Registers a callback (invoked on the capture thread) fired when one of the
// configured overlay control shortcuts is pressed. The int argument is the
// hotkey id: 0 = switch monitor, 1 = cycle display mode. Set before Start.
void SetHotkeyHandler(std::function<void(int)> onHotkey) { m_onHotkey = std::move(onHotkey); }
// Updates the overlay control shortcuts. Safe to call at any time; a matched
// chord is swallowed (not shown as a keystroke).
void SetHotkeys(const HotkeyChord& switchMonitor, const HotkeyChord& cycleDisplayMode)
{
m_switchMonitorHotkey = switchMonitor;
m_cycleDisplayModeHotkey = cycleDisplayMode;
}
// Stops capturing and joins the capture thread.
void Stop();
// Consumer-side drain. Safe to call from a single consumer thread.
bool TryPop(KeystrokeEvent& out) { return m_queue.try_pop(out); }
bool IsRunning() const { return m_running.load(std::memory_order_acquire); }
private:
static LRESULT CALLBACK HookProc(int nCode, WPARAM wParam, LPARAM lParam);
void ThreadMain();
void EmitDown(UINT vk, UINT scanCode);
// Returns true if the key-down matched a control shortcut and should be
// swallowed (not forwarded, not shown). Runs on the capture thread.
bool HandleKeyDown(UINT vk, UINT scanCode);
void HandleKeyUp(UINT vk);
static KeyboardCapture* s_instance;
SpscRing<KeystrokeEvent, 1024> m_queue;
std::function<void()> m_notify;
std::function<void(int)> m_onHotkey;
HotkeyChord m_switchMonitorHotkey;
HotkeyChord m_cycleDisplayModeHotkey;
// vk of the control shortcut currently held down, so auto-repeat fires the
// action only once. Read/written only on the capture thread.
UINT m_activeHotkeyVk = 0;
std::thread m_thread;
HHOOK m_hook = nullptr;
DWORD m_threadId = 0;
std::atomic<bool> m_running{ false };
};
}

View File

@@ -0,0 +1,365 @@
// 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.
#include "KeystrokeFormatter.h"
#include <algorithm>
#include <cwctype>
#include <vector>
#include <Windows.h>
namespace InputHighlighter::Formatter
{
namespace
{
// Modifier glyphs (Segoe UI Symbol compatible).
constexpr wchar_t kShiftGlyph[] = L"\u21E7"; // upwards white arrow
constexpr wchar_t kWinGlyph[] = L"\u229E"; // squared plus
std::wstring Utf32ToWide(char32_t ch)
{
if (ch == 0)
{
return std::wstring();
}
if (ch <= 0xFFFF)
{
return std::wstring(1, static_cast<wchar_t>(ch));
}
// Encode as a UTF-16 surrogate pair.
const char32_t v = ch - 0x10000;
std::wstring result;
result.push_back(static_cast<wchar_t>(0xD800 + (v >> 10)));
result.push_back(static_cast<wchar_t>(0xDC00 + (v & 0x3FF)));
return result;
}
bool IsAllWhitespace(const std::wstring& s)
{
if (s.empty())
{
return true;
}
for (const wchar_t c : s)
{
if (!std::iswspace(c))
{
return false;
}
}
return true;
}
// Present modifiers in a stable order matching the capture snapshot.
std::vector<std::wstring> GetModifierList(const std::array<bool, 4>& mods)
{
std::vector<std::wstring> list;
if (mods[Mod_Ctrl])
{
list.push_back(L"Ctrl");
}
if (mods[Mod_Alt])
{
list.push_back(L"Alt");
}
if (mods[Mod_Shift])
{
list.push_back(L"Shift");
}
if (mods[Mod_Win])
{
list.push_back(L"Win");
}
return list;
}
bool Contains(const std::vector<std::wstring>& v, const std::wstring& s)
{
return std::find(v.begin(), v.end(), s) != v.end();
}
}
bool IsCommandKey(uint32_t vk)
{
switch (vk)
{
case VK_SPACE:
case VK_RETURN:
case VK_TAB:
case VK_BACK:
case VK_ESCAPE:
case VK_DELETE:
case VK_INSERT:
case VK_HOME:
case VK_END:
case VK_PRIOR: // Page Up
case VK_NEXT: // Page Down
case VK_LEFT:
case VK_RIGHT:
case VK_UP:
case VK_DOWN:
case VK_SNAPSHOT: // Print Screen
case VK_PAUSE:
case VK_CAPITAL: // Caps Lock
case VK_LWIN:
case VK_RWIN:
return true;
default:
break;
}
// Function keys F1..F24.
if (vk >= VK_F1 && vk <= VK_F24)
{
return true;
}
return false;
}
std::wstring GetModifierSymbol(const std::wstring& modifier)
{
if (modifier == L"Ctrl")
{
return L"Ctrl";
}
if (modifier == L"Alt")
{
return L"Alt";
}
if (modifier == L"Shift")
{
return kShiftGlyph;
}
if (modifier == L"Win")
{
return kWinGlyph;
}
return modifier;
}
std::wstring GetKeyName(uint32_t vk)
{
switch (vk)
{
case VK_LSHIFT:
case VK_RSHIFT:
case VK_SHIFT:
return kShiftGlyph;
case VK_CONTROL:
case VK_LCONTROL:
case VK_RCONTROL:
return L"Ctrl";
case VK_MENU:
case VK_LMENU:
case VK_RMENU:
return L"Alt";
case VK_LWIN:
case VK_RWIN:
return kWinGlyph;
case VK_SPACE:
return L"Space";
case VK_RETURN:
return L"Enter";
case VK_BACK:
return L"Backspace";
case VK_TAB:
return L"Tab";
case VK_ESCAPE:
return L"Esc";
case VK_DELETE:
return L"Del";
case VK_INSERT:
return L"Ins";
case VK_LEFT:
return L"\u2190"; // left arrow
case VK_RIGHT:
return L"\u2192"; // right arrow
case VK_UP:
return L"\u2191"; // up arrow
case VK_DOWN:
return L"\u2193"; // down arrow
default:
break;
}
// Letters A-Z.
if (vk >= 'A' && vk <= 'Z')
{
return std::wstring(1, static_cast<wchar_t>(vk));
}
// Top-row digits 0-9.
if (vk >= '0' && vk <= '9')
{
return std::wstring(1, static_cast<wchar_t>(vk));
}
// Numpad 0-9.
if (vk >= VK_NUMPAD0 && vk <= VK_NUMPAD9)
{
return L"Num " + std::wstring(1, static_cast<wchar_t>(L'0' + (vk - VK_NUMPAD0)));
}
// Function keys.
if (vk >= VK_F1 && vk <= VK_F24)
{
return L"F" + std::to_wstring(vk - VK_F1 + 1);
}
// Punctuation / OEM keys (US layout labels).
switch (vk)
{
case VK_OEM_1:
return L";";
case VK_OEM_PLUS:
return L"=";
case VK_OEM_COMMA:
return L",";
case VK_OEM_MINUS:
return L"-";
case VK_OEM_PERIOD:
return L".";
case VK_OEM_2:
return L"/";
case VK_OEM_3:
return L"`";
case VK_OEM_4:
return L"[";
case VK_OEM_5:
return L"\\";
case VK_OEM_6:
return L"]";
case VK_OEM_7:
return L"'";
case VK_VOLUME_MUTE:
return L"Mute";
case VK_VOLUME_DOWN:
return L"Vol -";
case VK_VOLUME_UP:
return L"Vol +";
case VK_MEDIA_NEXT_TRACK:
return L"Next";
case VK_MEDIA_PREV_TRACK:
return L"Prev";
case VK_MEDIA_PLAY_PAUSE:
return L"Play/Pause";
default:
break;
}
return std::wstring();
}
bool IsShortcut(const KeystrokeEvent& e)
{
// Any modifier held makes this a shortcut.
for (const bool held : e.mods)
{
if (held)
{
return true;
}
}
// Command keys (Enter, Esc, F1, ...) count as shortcuts too.
if (IsCommandKey(e.vk))
{
return true;
}
return false;
}
std::wstring Format(const KeystrokeEvent& e)
{
if (e.type == KeystrokeEventType::Up)
{
return std::wstring();
}
const bool isCharEvent = e.ch != 0;
const std::wstring text = isCharEvent ? Utf32ToWide(e.ch) : std::wstring();
const bool hasCtrl = e.mods[Mod_Ctrl];
const bool hasAlt = e.mods[Mod_Alt];
const bool hasWin = e.mods[Mod_Win];
std::wstring keyName;
bool haveKeyName = false;
if (isCharEvent && !hasWin)
{
if (IsAllWhitespace(text))
{
return std::wstring();
}
keyName = text;
haveKeyName = true;
}
else
{
if (IsCommandKey(e.vk) || hasCtrl || hasAlt || hasWin)
{
keyName = GetKeyName(e.vk);
haveKeyName = !keyName.empty();
}
}
if (!haveKeyName)
{
return std::wstring();
}
std::vector<std::wstring> displayParts;
for (const auto& mod : GetModifierList(e.mods))
{
// Don't show Shift when a shifted character already implies it (e.g. "!").
if (isCharEvent && !hasWin && mod == L"Shift" && !hasCtrl && !hasAlt)
{
continue;
}
const std::wstring symbol = GetModifierSymbol(mod);
if (!Contains(displayParts, symbol))
{
displayParts.push_back(symbol);
}
}
// Avoid duplicating the key with an already-shown modifier (e.g. Ctrl + Ctrl).
const std::wstring modSym = GetModifierSymbol(keyName);
if (!Contains(displayParts, keyName) && !Contains(displayParts, modSym))
{
displayParts.push_back(keyName);
}
std::wstring result;
for (size_t i = 0; i < displayParts.size(); ++i)
{
if (i != 0)
{
result += L" + ";
}
result += displayParts[i];
}
return result;
}
}

View File

@@ -0,0 +1,32 @@
// 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.
// Pure keystroke -> display-string formatting, ported from the team4
// KeystrokeOverlay KeystrokeEvent.cs (ToString / IsShortcut / GetKeyName / ...).
// No UI dependencies so it can be unit tested directly.
#pragma once
#include <string>
#include "KeystrokeTypes.h"
namespace InputHighlighter::Formatter
{
// Human-readable display string for the keystroke, or an empty string when
// there is nothing meaningful to show (e.g. key-up, whitespace char).
std::wstring Format(const KeystrokeEvent& e);
// A keystroke is a "shortcut" if it carries a modifier or is a command key.
bool IsShortcut(const KeystrokeEvent& e);
// Command keys are non-character keys we still want to surface (Enter, arrows,
// function keys, ...). Plain letters/digits/punctuation are handled as chars.
bool IsCommandKey(uint32_t vk);
// Friendly name/glyph for a virtual key (e.g. "Ctrl", "Enter", arrow glyphs).
std::wstring GetKeyName(uint32_t vk);
// Symbol used for a modifier label ("Shift" -> the shift glyph, etc.).
std::wstring GetModifierSymbol(const std::wstring& modifier);
}

View File

@@ -0,0 +1,141 @@
// 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.
#include "KeystrokeProcessor.h"
#include <cwctype>
#include <Windows.h>
#include "KeystrokeFormatter.h"
namespace InputHighlighter
{
namespace
{
std::wstring CharText(const KeystrokeEvent& e)
{
if (e.ch == 0)
{
return std::wstring();
}
if (e.ch <= 0xFFFF)
{
return std::wstring(1, static_cast<wchar_t>(e.ch));
}
const char32_t v = e.ch - 0x10000;
std::wstring result;
result.push_back(static_cast<wchar_t>(0xD800 + (v >> 10)));
result.push_back(static_cast<wchar_t>(0xDC00 + (v & 0x3FF)));
return result;
}
bool IsNullOrWhiteSpace(const std::wstring& s)
{
if (s.empty())
{
return true;
}
for (const wchar_t c : s)
{
if (!std::iswspace(c))
{
return false;
}
}
return true;
}
}
KeystrokeResult KeystrokeProcessor::Process(const KeystrokeEvent& e, DisplayMode displayMode)
{
const std::wstring formattedText = Formatter::Format(e);
// Nothing to display.
if (formattedText.empty())
{
return KeystrokeResult{ KeystrokeAction::None, std::wstring() };
}
const bool isShortcut = Formatter::IsShortcut(e);
switch (displayMode)
{
case DisplayMode::Last5:
return KeystrokeResult{ KeystrokeAction::Add, formattedText };
case DisplayMode::SingleCharactersOnly:
if (isShortcut)
{
return KeystrokeResult{ KeystrokeAction::None, std::wstring() };
}
return KeystrokeResult{ KeystrokeAction::Add, formattedText };
case DisplayMode::ShortcutsOnly:
if (!isShortcut)
{
return KeystrokeResult{ KeystrokeAction::None, std::wstring() };
}
return KeystrokeResult{ KeystrokeAction::Add, formattedText };
case DisplayMode::Stream:
return ProcessStreamMode(e, isShortcut, formattedText);
default:
return KeystrokeResult{ KeystrokeAction::None, std::wstring() };
}
}
KeystrokeResult KeystrokeProcessor::ProcessStreamMode(const KeystrokeEvent& e, bool isShortcut, const std::wstring& formattedText)
{
// Backspace edits the current stream buffer.
if (e.vk == VK_BACK)
{
if (!m_streamBuffer.empty())
{
m_streamBuffer.pop_back();
if (m_streamBuffer.empty())
{
return KeystrokeResult{ KeystrokeAction::RemoveLast, std::wstring() };
}
return KeystrokeResult{ KeystrokeAction::ReplaceLast, m_streamBuffer };
}
return KeystrokeResult{ KeystrokeAction::None, std::wstring() };
}
// A shortcut (other than Space) resets the stream and shows itself.
if (isShortcut && e.vk != VK_SPACE)
{
ResetBuffer();
return KeystrokeResult{ KeystrokeAction::Add, formattedText };
}
const std::wstring text = CharText(e);
// Whitespace ends the current word so the next character starts fresh.
if (IsNullOrWhiteSpace(text))
{
ResetBuffer();
return KeystrokeResult{ KeystrokeAction::None, std::wstring() };
}
m_streamBuffer += text;
if (m_streamBuffer.size() == 1)
{
return KeystrokeResult{ KeystrokeAction::Add, m_streamBuffer };
}
return KeystrokeResult{ KeystrokeAction::ReplaceLast, m_streamBuffer };
}
}

View File

@@ -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.
// Pure keystroke display state machine, ported from the team4 KeystrokeOverlay
// KeystrokeProcessor.cs. Decides whether an incoming key adds a new pill,
// replaces/removes the current one, or is ignored, based on the display mode.
#pragma once
#include <string>
#include "KeystrokeTypes.h"
namespace InputHighlighter
{
class KeystrokeProcessor
{
public:
// Determine the visual action for an incoming key in the given display mode.
KeystrokeResult Process(const KeystrokeEvent& e, DisplayMode displayMode);
void ResetBuffer() { m_streamBuffer.clear(); }
private:
KeystrokeResult ProcessStreamMode(const KeystrokeEvent& e, bool isShortcut, const std::wstring& formattedText);
std::wstring m_streamBuffer;
};
}

View File

@@ -0,0 +1,512 @@
// 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.
#include "KeystrokeRenderer.h"
#include <windows.ui.composition.interop.h>
#include <winrt/Windows.Graphics.DirectX.h>
#include <algorithm>
#include <cmath>
#pragma comment(lib, "d3d11.lib")
#pragma comment(lib, "d2d1.lib")
#pragma comment(lib, "dwrite.lib")
#pragma comment(lib, "dxgi.lib")
// This file performs a lot of low-level D2D/DWrite/Composition interop; several
// C++ Core Guidelines analysis checks are noisy here and are suppressed to match
// the pragmatic style used elsewhere in the native rendering code.
#pragma warning(disable : 26451 26429 26446 26447 26461 26472 26481 26490 26493 26496 26497 26812)
namespace ABI
{
using namespace ABI::Windows::UI::Composition;
}
using namespace winrt::Windows::UI;
using namespace winrt::Windows::UI::Composition;
using namespace winrt::Windows::Foundation;
using namespace winrt::Windows::Foundation::Numerics;
namespace WGDX = winrt::Windows::Graphics::DirectX;
namespace InputHighlighter
{
namespace
{
constexpr float kMarginDip = 24.0f;
constexpr float kGapDip = 10.0f;
constexpr wchar_t kFontFamily[] = L"Segoe UI";
}
KeystrokeRenderer::~KeystrokeRenderer()
{
// Drop references without touching the (possibly torn-down) parent tree.
m_pills.clear();
m_container = nullptr;
m_graphicsDevice = nullptr;
}
bool KeystrokeRenderer::CreateDevices()
{
UINT flags = D3D11_CREATE_DEVICE_BGRA_SUPPORT;
HRESULT hr = D3D11CreateDevice(
nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, flags, nullptr, 0, D3D11_SDK_VERSION, m_d3dDevice.put(), nullptr, nullptr);
if (hr == DXGI_ERROR_UNSUPPORTED)
{
hr = D3D11CreateDevice(
nullptr, D3D_DRIVER_TYPE_WARP, nullptr, flags, nullptr, 0, D3D11_SDK_VERSION, m_d3dDevice.put(), nullptr, nullptr);
}
if (FAILED(hr))
{
return false;
}
auto dxgiDevice = m_d3dDevice.as<IDXGIDevice>();
D2D1_FACTORY_OPTIONS d2dOptions{};
hr = D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, __uuidof(ID2D1Factory1), &d2dOptions, m_d2dFactory.put_void());
if (FAILED(hr))
{
return false;
}
hr = m_d2dFactory->CreateDevice(dxgiDevice.get(), m_d2dDevice.put());
if (FAILED(hr))
{
return false;
}
hr = DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED, __uuidof(IDWriteFactory), reinterpret_cast<::IUnknown**>(m_dwriteFactory.put()));
if (FAILED(hr))
{
return false;
}
// Bind the D2D device to the compositor to create Composition-hosted surfaces.
auto interop = m_compositor.as<ABI::ICompositorInterop>();
ABI::ICompositionGraphicsDevice* rawDevice = nullptr;
hr = interop->CreateGraphicsDevice(m_d2dDevice.get(), &rawDevice);
if (FAILED(hr))
{
return false;
}
winrt::attach_abi(m_graphicsDevice, rawDevice);
return true;
}
bool KeystrokeRenderer::Initialize(const Compositor& compositor, const ContainerVisual& parentRoot, HWND hwnd)
{
if (m_initialized)
{
return true;
}
m_compositor = compositor;
m_hwnd = hwnd;
try
{
if (!CreateDevices())
{
return false;
}
m_container = m_compositor.CreateContainerVisual();
m_container.RelativeSizeAdjustment({ 1.0f, 1.0f });
parentRoot.Children().InsertAtTop(m_container);
}
catch (...)
{
return false;
}
m_initialized = true;
return true;
}
void KeystrokeRenderer::Uninitialize()
{
if (!m_initialized)
{
return;
}
Clear();
if (m_container)
{
try
{
auto parent = m_container.Parent();
if (parent)
{
parent.Children().Remove(m_container);
}
}
catch (...)
{
}
}
m_container = nullptr;
m_graphicsDevice = nullptr;
m_d2dDevice = nullptr;
m_d2dFactory = nullptr;
m_dwriteFactory = nullptr;
m_d3dDevice = nullptr;
m_initialized = false;
}
void KeystrokeRenderer::ApplySettings(const KeystrokeRendererSettings& settings)
{
m_settings = settings;
// Mode / style changes reset the current stack; new pills pick up the new look.
Clear();
}
void KeystrokeRenderer::SetAnchorRect(const D2D1_RECT_F& clientRect)
{
m_anchorRect = clientRect;
Relayout();
}
float KeystrokeRenderer::DpiScale() const
{
const UINT dpi = m_hwnd ? GetDpiForWindow(m_hwnd) : 96;
return static_cast<float>(dpi == 0 ? 96 : dpi) / 96.0f;
}
size_t KeystrokeRenderer::MaxPills() const
{
switch (m_settings.displayMode)
{
case DisplayMode::Last5:
return 5;
case DisplayMode::Stream:
return 8;
case DisplayMode::SingleCharactersOnly:
case DisplayMode::ShortcutsOnly:
default:
return 5;
}
}
void KeystrokeRenderer::DrawPill(Pill& pill, const std::wstring& text)
{
if (!m_initialized)
{
return;
}
const float dpiScale = DpiScale();
const float fontPx = (std::max)(8.0f, m_settings.textSize) * dpiScale;
// Measure the string with DirectWrite.
winrt::com_ptr<IDWriteTextFormat> format;
if (FAILED(m_dwriteFactory->CreateTextFormat(
kFontFamily, nullptr, DWRITE_FONT_WEIGHT_SEMI_BOLD, DWRITE_FONT_STYLE_NORMAL, DWRITE_FONT_STRETCH_NORMAL, fontPx, L"", format.put())))
{
return;
}
format->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_CENTER);
format->SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_CENTER);
winrt::com_ptr<IDWriteTextLayout> layout;
if (FAILED(m_dwriteFactory->CreateTextLayout(
text.c_str(), static_cast<UINT32>(text.length()), format.get(), 4096.0f, 4096.0f, layout.put())))
{
return;
}
DWRITE_TEXT_METRICS metrics{};
layout->GetMetrics(&metrics);
const float padX = fontPx * 0.55f;
const float padY = fontPx * 0.30f;
float widthPx = std::ceil(metrics.width + 2.0f * padX);
float heightPx = std::ceil(metrics.height + 2.0f * padY);
widthPx = (std::max)(widthPx, heightPx); // keep short pills from looking cramped
layout->SetMaxWidth(widthPx);
layout->SetMaxHeight(heightPx);
// (Re)create the drawing surface at the measured pixel size.
auto surface = m_graphicsDevice.CreateDrawingSurface(
Size{ widthPx, heightPx }, WGDX::DirectXPixelFormat::B8G8R8A8UIntNormalized, WGDX::DirectXAlphaMode::Premultiplied);
auto surfaceInterop = surface.as<ABI::ICompositionDrawingSurfaceInterop>();
winrt::com_ptr<ID2D1DeviceContext> dc;
POINT offset{};
if (FAILED(surfaceInterop->BeginDraw(nullptr, __uuidof(ID2D1DeviceContext), dc.put_void(), &offset)))
{
return;
}
dc->SetDpi(96.0f, 96.0f);
dc->SetTransform(D2D1::Matrix3x2F::Translation(static_cast<float>(offset.x), static_cast<float>(offset.y)));
dc->Clear(D2D1::ColorF(0.0f, 0.0f, 0.0f, 0.0f));
const auto& bg = m_settings.backgroundColor;
const auto& fg = m_settings.textColor;
winrt::com_ptr<ID2D1SolidColorBrush> bgBrush;
dc->CreateSolidColorBrush(D2D1::ColorF(bg.R / 255.0f, bg.G / 255.0f, bg.B / 255.0f, bg.A / 255.0f), bgBrush.put());
winrt::com_ptr<ID2D1SolidColorBrush> fgBrush;
dc->CreateSolidColorBrush(D2D1::ColorF(fg.R / 255.0f, fg.G / 255.0f, fg.B / 255.0f, fg.A / 255.0f), fgBrush.put());
const float radius = heightPx * 0.28f;
const D2D1_ROUNDED_RECT rr = D2D1::RoundedRect(D2D1::RectF(0.5f, 0.5f, widthPx - 0.5f, heightPx - 0.5f), radius, radius);
if (bgBrush)
{
dc->FillRoundedRectangle(rr, bgBrush.get());
}
if (fgBrush)
{
dc->DrawTextLayout(D2D1::Point2F(0.0f, 0.0f), layout.get(), fgBrush.get(), D2D1_DRAW_TEXT_OPTIONS_NONE);
}
if (m_settings.strokeThickness > 0 && m_settings.strokeColor.A > 0)
{
const auto& sc = m_settings.strokeColor;
winrt::com_ptr<ID2D1SolidColorBrush> strokeBrush;
dc->CreateSolidColorBrush(D2D1::ColorF(sc.R / 255.0f, sc.G / 255.0f, sc.B / 255.0f, sc.A / 255.0f), strokeBrush.put());
if (strokeBrush)
{
const float sw = m_settings.strokeThickness * DpiScale();
const float inset = 0.5f + sw / 2.0f;
const D2D1_ROUNDED_RECT sr = D2D1::RoundedRect(D2D1::RectF(inset, inset, widthPx - inset, heightPx - inset), radius, radius);
dc->DrawRoundedRectangle(sr, strokeBrush.get(), sw);
}
}
surfaceInterop->EndDraw();
auto brush = m_compositor.CreateSurfaceBrush(surface);
brush.Stretch(CompositionStretch::Fill);
if (!pill.visual)
{
pill.visual = m_compositor.CreateSpriteVisual();
m_container.Children().InsertAtTop(pill.visual);
}
pill.visual.Brush(brush);
pill.visual.Size({ widthPx, heightPx });
pill.brush = brush;
pill.surface = surface;
pill.text = text;
pill.width = widthPx;
pill.height = heightPx;
}
void KeystrokeRenderer::AnimateEntrance(const Pill& pill, float /*targetOpacity*/)
{
if (!pill.visual)
{
return;
}
pill.visual.CenterPoint({ pill.width / 2.0f, pill.height / 2.0f, 0.0f });
auto anim = m_compositor.CreateVector3KeyFrameAnimation();
anim.InsertKeyFrame(0.0f, { 0.7f, 0.7f, 1.0f });
anim.InsertKeyFrame(1.0f, { 1.0f, 1.0f, 1.0f });
anim.Duration(std::chrono::milliseconds(120));
pill.visual.StartAnimation(L"Scale", anim);
}
void KeystrokeRenderer::EnforceCap()
{
const size_t cap = MaxPills();
while (m_pills.size() > cap)
{
auto& front = m_pills.front();
if (front.visual && m_container)
{
m_container.Children().Remove(front.visual);
}
m_pills.pop_front();
}
}
void KeystrokeRenderer::Relayout()
{
if (!m_initialized || m_pills.empty())
{
return;
}
const float dpiScale = DpiScale();
const float margin = kMarginDip * dpiScale;
const float gap = kGapDip * dpiScale;
float totalW = 0.0f;
float maxH = 0.0f;
for (const auto& p : m_pills)
{
totalW += p.width;
maxH = (std::max)(maxH, p.height);
}
totalW += gap * static_cast<float>(m_pills.size() - 1);
const float left = m_anchorRect.left + margin;
const float right = m_anchorRect.right - margin;
const float centerX = (m_anchorRect.left + m_anchorRect.right) / 2.0f;
float startX = left;
switch (m_settings.position)
{
case KeystrokePosition::TopLeft:
case KeystrokePosition::BottomLeft:
startX = left;
break;
case KeystrokePosition::TopCenter:
case KeystrokePosition::BottomCenter:
startX = centerX - totalW / 2.0f;
break;
case KeystrokePosition::TopRight:
case KeystrokePosition::BottomRight:
startX = right - totalW;
break;
}
float baseY = m_anchorRect.top + margin;
switch (m_settings.position)
{
case KeystrokePosition::BottomLeft:
case KeystrokePosition::BottomCenter:
case KeystrokePosition::BottomRight:
baseY = m_anchorRect.bottom - margin - maxH;
break;
default:
break;
}
const size_t n = m_pills.size();
float x = startX;
for (size_t i = 0; i < n; ++i)
{
auto& p = m_pills[i];
const float y = baseY + (maxH - p.height) / 2.0f;
p.visual.Offset({ x, y, 0.0f });
// Older pills (towards the front) fade out slightly.
const size_t ageFromNewest = (n - 1) - i;
const float opacity = (std::max)(0.4f, 1.0f - 0.12f * static_cast<float>(ageFromNewest));
p.visual.Opacity(opacity);
x += p.width + gap;
}
}
void KeystrokeRenderer::OnResult(const KeystrokeResult& result)
{
if (!m_initialized || result.action == KeystrokeAction::None)
{
return;
}
const ULONGLONG deadline = (m_settings.timeoutMs > 0) ? (GetTickCount64() + static_cast<ULONGLONG>(m_settings.timeoutMs)) : 0;
switch (result.action)
{
case KeystrokeAction::Add:
{
Pill pill;
DrawPill(pill, result.text);
pill.expireAt = deadline;
m_pills.push_back(std::move(pill));
EnforceCap();
Relayout();
AnimateEntrance(m_pills.back(), 1.0f);
break;
}
case KeystrokeAction::ReplaceLast:
{
if (m_pills.empty())
{
Pill pill;
DrawPill(pill, result.text);
pill.expireAt = deadline;
m_pills.push_back(std::move(pill));
EnforceCap();
Relayout();
AnimateEntrance(m_pills.back(), 1.0f);
}
else
{
auto& back = m_pills.back();
DrawPill(back, result.text);
back.expireAt = deadline;
Relayout();
}
break;
}
case KeystrokeAction::RemoveLast:
{
if (!m_pills.empty())
{
auto& back = m_pills.back();
if (back.visual && m_container)
{
m_container.Children().Remove(back.visual);
}
m_pills.pop_back();
Relayout();
}
break;
}
case KeystrokeAction::None:
default:
break;
}
}
bool KeystrokeRenderer::Tick()
{
if (m_pills.empty())
{
return false;
}
const ULONGLONG now = GetTickCount64();
bool changed = false;
// Oldest pills (front) expire first.
while (!m_pills.empty())
{
auto& front = m_pills.front();
if (front.expireAt == 0 || now < front.expireAt)
{
break;
}
if (front.visual && m_container)
{
m_container.Children().Remove(front.visual);
}
m_pills.pop_front();
changed = true;
}
if (changed)
{
Relayout();
}
return changed;
}
void KeystrokeRenderer::Clear()
{
if (m_container)
{
for (auto& p : m_pills)
{
if (p.visual)
{
m_container.Children().Remove(p.visual);
}
}
}
m_pills.clear();
}
}

View File

@@ -0,0 +1,130 @@
// 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.
// KeystrokeRenderer: draws the captured keystrokes as horizontally stacked
// "key pill" visuals on the existing Mouse/Input Highlighter Composition overlay.
//
// Text is rasterized with Direct2D + DirectWrite onto CompositionDrawingSurfaces,
// which are hosted by SpriteVisuals under a dedicated container in the overlay's
// visual tree. All public methods must be called on the composition (overlay UI)
// thread.
#pragma once
#include <windows.h>
#include <deque>
#include <string>
#include <winrt/Windows.UI.h>
#include <winrt/Windows.UI.Composition.h>
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Foundation.Numerics.h>
#include <d2d1_1.h>
#include <dwrite.h>
#include <d3d11.h>
#include "KeystrokeTypes.h"
namespace InputHighlighter
{
// Anchor position of the keystroke stack, matching the settings enum (0-5).
enum class KeystrokePosition
{
TopLeft = 0,
TopCenter = 1,
TopRight = 2,
BottomLeft = 3,
BottomCenter = 4,
BottomRight = 5,
};
struct KeystrokeRendererSettings
{
DisplayMode displayMode = DisplayMode::Last5;
KeystrokePosition position = KeystrokePosition::BottomCenter;
int timeoutMs = 2000;
float textSize = 24.0f; // DIP font size
winrt::Windows::UI::Color textColor{ 255, 255, 255, 255 };
winrt::Windows::UI::Color backgroundColor{ 200, 32, 32, 32 };
winrt::Windows::UI::Color strokeColor{ 0, 255, 255, 255 };
int strokeThickness = 0; // DIP; 0 = no border
};
class KeystrokeRenderer
{
public:
KeystrokeRenderer() = default;
~KeystrokeRenderer();
KeystrokeRenderer(const KeystrokeRenderer&) = delete;
KeystrokeRenderer& operator=(const KeystrokeRenderer&) = delete;
// Creates the D2D/DWrite devices and the Composition graphics device, and
// adds a container visual to parentRoot. hwnd is used for DPI queries and
// to size/anchor the stack. Returns false on failure (rendering disabled).
bool Initialize(const winrt::Windows::UI::Composition::Compositor& compositor,
const winrt::Windows::UI::Composition::ContainerVisual& parentRoot,
HWND hwnd);
void Uninitialize();
bool IsInitialized() const noexcept { return m_initialized; }
void ApplySettings(const KeystrokeRendererSettings& settings);
// The client-space rectangle the stack is anchored within (typically the
// work area of the active monitor, translated to overlay client coords).
void SetAnchorRect(const D2D1_RECT_F& clientRect);
// Applies a processor result (Add / ReplaceLast / RemoveLast / None).
void OnResult(const KeystrokeResult& result);
// Called periodically to fade/remove expired pills. Returns true if the
// set of visible pills changed (so the caller may stop the timer when empty).
bool Tick();
bool HasVisiblePills() const noexcept { return !m_pills.empty(); }
// Removes all pills immediately (e.g. when drawing stops).
void Clear();
private:
struct Pill
{
winrt::Windows::UI::Composition::SpriteVisual visual{ nullptr };
winrt::Windows::UI::Composition::CompositionSurfaceBrush brush{ nullptr };
winrt::Windows::UI::Composition::CompositionDrawingSurface surface{ nullptr };
std::wstring text;
ULONGLONG expireAt = 0; // GetTickCount64 deadline; 0 = never
float width = 0.0f; // DIP
float height = 0.0f; // DIP
};
bool CreateDevices();
float DpiScale() const;
void DrawPill(Pill& pill, const std::wstring& text);
void Relayout();
void EnforceCap();
size_t MaxPills() const;
void AnimateEntrance(const Pill& pill, float targetOpacity);
bool m_initialized = false;
HWND m_hwnd = nullptr;
KeystrokeRendererSettings m_settings;
D2D1_RECT_F m_anchorRect{ 0, 0, 0, 0 };
winrt::Windows::UI::Composition::Compositor m_compositor{ nullptr };
winrt::Windows::UI::Composition::ContainerVisual m_container{ nullptr };
winrt::Windows::UI::Composition::CompositionGraphicsDevice m_graphicsDevice{ nullptr };
winrt::com_ptr<ID3D11Device> m_d3dDevice;
winrt::com_ptr<ID2D1Device> m_d2dDevice;
winrt::com_ptr<ID2D1Factory1> m_d2dFactory;
winrt::com_ptr<IDWriteFactory> m_dwriteFactory;
std::deque<Pill> m_pills;
};
}

View File

@@ -0,0 +1,125 @@
// 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.
// Core value types for the Input Highlighter keystroke overlay. These are pure
// (no WinRT/UI dependencies) so the formatter and processor can be unit tested.
#pragma once
#include <array>
#include <atomic>
#include <cstddef>
#include <cstdint>
#include <string>
namespace InputHighlighter
{
// Indices into KeystrokeEvent::mods (order matches the keyboard snapshot).
enum ModifierIndex
{
Mod_Ctrl = 0,
Mod_Alt = 1,
Mod_Shift = 2,
Mod_Win = 3,
};
enum class KeystrokeEventType : uint8_t
{
Down,
Up,
};
// A single captured keystroke. POD so it can be copied cheaply across the
// producer (hook thread) / consumer (composition thread) boundary.
struct KeystrokeEvent
{
KeystrokeEventType type = KeystrokeEventType::Down;
uint32_t vk = 0; // virtual key code
char32_t ch = 0; // printable character for this key (0 when non-printable)
std::array<bool, 4> mods = { false, false, false, false }; // Ctrl, Alt, Shift, Win
uint64_t tsMicros = 0;
};
// A keyboard shortcut (modifier chord + trigger key) used to drive overlay
// control actions from within the keystroke capture hook. Modifier order
// matches KeystrokeEvent::mods (Ctrl, Alt, Shift, Win).
struct HotkeyChord
{
bool ctrl = false;
bool alt = false;
bool shift = false;
bool win = false;
uint32_t vk = 0; // 0 = unbound
bool IsBound() const { return vk != 0; }
bool Matches(uint32_t downVk, const std::array<bool, 4>& mods) const
{
return vk != 0 && vk == downVk &&
ctrl == mods[Mod_Ctrl] &&
alt == mods[Mod_Alt] &&
shift == mods[Mod_Shift] &&
win == mods[Mod_Win];
}
};
// Display modes, ported from the team4 KeystrokeOverlay DisplayMode enum.
enum class DisplayMode
{
Last5 = 0,
SingleCharactersOnly = 1,
ShortcutsOnly = 2,
Stream = 3,
};
enum class KeystrokeAction
{
None, // Do nothing
Add, // Create a new visual "pill"
ReplaceLast, // Update the current pill (e.g. "Hell" -> "Hello")
RemoveLast, // Backspace an entire pill
};
struct KeystrokeResult
{
KeystrokeAction action = KeystrokeAction::None;
std::wstring text;
};
// Single-producer / single-consumer lock-free ring buffer. The producer is the
// low-level keyboard hook thread; the consumer is the composition thread.
template<typename T, size_t N>
class SpscRing
{
public:
bool try_push(const T& v)
{
const auto head = _head.load(std::memory_order_relaxed);
const auto next = (head + 1) % N;
if (next == _tail.load(std::memory_order_acquire))
{
return false; // full
}
_buf[head] = v;
_head.store(next, std::memory_order_release);
return true;
}
bool try_pop(T& out)
{
const auto tail = _tail.load(std::memory_order_relaxed);
if (tail == _head.load(std::memory_order_acquire))
{
return false; // empty
}
out = _buf[tail];
_tail.store((tail + 1) % N, std::memory_order_release);
return true;
}
private:
std::array<T, N> _buf{};
std::atomic<size_t> _head{ 0 };
std::atomic<size_t> _tail{ 0 };
};
}

View File

@@ -4,6 +4,9 @@
#include "pch.h"
#include "MouseHighlighter.h"
#include "trace.h"
#include "Keystrokes/KeyboardCapture.h"
#include "Keystrokes/KeystrokeProcessor.h"
#include "Keystrokes/KeystrokeRenderer.h"
#include <cmath>
#include <algorithm>
#include <memory>
@@ -62,6 +65,17 @@ private:
// a mouse button is held and restores it on release.
void SpotlightAnimatePress();
void SpotlightAnimateRelease();
// Keystroke overlay (Input Highlighter) lifecycle + draining.
void StartKeystrokes();
void StopKeystrokes();
void DrainKeystrokes();
D2D1_RECT_F ComputeKeystrokeAnchorRect() const;
// Overlay control-shortcut actions (posted from the capture hook).
void CycleKeystrokeMonitor();
void CycleKeystrokeDisplayMode();
static BOOL CALLBACK MonitorEnumProc(HMONITOR monitor, HDC hdc, LPRECT rect, LPARAM data) noexcept;
HHOOK m_mouseHook = NULL;
static LRESULT CALLBACK MouseHookProc(int nCode, WPARAM wParam, LPARAM lParam) noexcept;
// Helpers for spotlight overlay
@@ -74,6 +88,10 @@ private:
HWND m_hwnd = NULL;
HINSTANCE m_hinstance = NULL;
static constexpr DWORD WM_SWITCH_ACTIVATION_MODE = WM_APP;
static constexpr DWORD WM_DRAIN_KEYSTROKES = WM_APP + 1;
static constexpr DWORD WM_KEYSTROKE_HOTKEY = WM_APP + 2;
static constexpr UINT_PTR KEYSTROKE_TICK_TIMER_ID = 130;
static constexpr UINT KEYSTROKE_TICK_INTERVAL_MS = 100;
winrt::DispatcherQueueController m_dispatcherQueueController{ nullptr };
winrt::Compositor m_compositor{ nullptr };
@@ -137,6 +155,18 @@ private:
winrt::Windows::UI::Color m_leftClickColor = MOUSE_HIGHLIGHTER_DEFAULT_LEFT_BUTTON_COLOR;
winrt::Windows::UI::Color m_rightClickColor = MOUSE_HIGHLIGHTER_DEFAULT_RIGHT_BUTTON_COLOR;
winrt::Windows::UI::Color m_alwaysColor = MOUSE_HIGHLIGHTER_DEFAULT_ALWAYS_COLOR;
// Input Highlighter: sub-toggles + keystroke overlay pipeline.
bool m_showMouse = INPUT_HIGHLIGHTER_DEFAULT_SHOW_MOUSE;
bool m_showKeystrokes = INPUT_HIGHLIGHTER_DEFAULT_SHOW_KEYSTROKES;
InputHighlighter::KeystrokeRendererSettings m_keystrokeSettings;
InputHighlighter::KeyboardCapture m_keyboardCapture;
InputHighlighter::KeystrokeProcessor m_keystrokeProcessor;
InputHighlighter::KeystrokeRenderer m_keystrokeRenderer;
// Overlay control shortcuts + current monitor override (-1 = follow cursor).
InputHighlighter::HotkeyChord m_switchMonitorHotkey;
InputHighlighter::HotkeyChord m_switchDisplayModeHotkey;
int m_keystrokeMonitorOverride = -1;
};
static const uint32_t BRING_TO_FRONT_TIMER_ID = 123;
static const uint32_t HOLD_RIPPLE_TIMER_LEFT = 124;
@@ -205,6 +235,18 @@ bool Highlighter::CreateHighlighter()
m_overlay.IsVisible(false);
m_root.Children().InsertAtTop(m_overlay);
// Initialize the keystroke overlay renderer on the same visual tree. If it
// fails (e.g. no D3D device), keystroke display is disabled but mouse
// highlighting continues to work.
if (!m_keystrokeRenderer.Initialize(m_compositor, m_root, m_hwnd))
{
Logger::warn("Keystroke renderer initialization failed; keystroke overlay disabled.");
}
else
{
m_keystrokeRenderer.ApplySettings(m_keystrokeSettings);
}
return true;
}
catch (...)
@@ -638,9 +680,16 @@ void Highlighter::StartDrawing()
ClearDrawing();
ShowWindow(m_hwnd, SW_SHOWNOACTIVATE);
instance->AddDrawingPoint(Highlighter::MouseButton::None);
if (m_showMouse)
{
instance->AddDrawingPoint(Highlighter::MouseButton::None);
m_mouseHook = SetWindowsHookEx(WH_MOUSE_LL, MouseHookProc, m_hinstance, 0);
}
m_mouseHook = SetWindowsHookEx(WH_MOUSE_LL, MouseHookProc, m_hinstance, 0);
if (m_showKeystrokes)
{
StartKeystrokes();
}
}
void Highlighter::StopDrawing()
@@ -664,9 +713,13 @@ void Highlighter::StopDrawing()
m_overlay.IsVisible(false);
}
ShowWindow(m_hwnd, SW_HIDE);
UnhookWindowsHookEx(m_mouseHook);
if (m_mouseHook)
{
UnhookWindowsHookEx(m_mouseHook);
m_mouseHook = NULL;
}
StopKeystrokes();
ClearDrawing();
m_mouseHook = NULL;
}
void Highlighter::SwitchActivationMode()
@@ -674,6 +727,181 @@ void Highlighter::SwitchActivationMode()
PostMessage(m_hwnd, WM_SWITCH_ACTIVATION_MODE, 0, 0);
}
// Compute the anchor rectangle (in overlay client coordinates) for the keystroke
// stack. Anchors to the override monitor if the user cycled displays, otherwise
// to the monitor under the cursor. The overlay's client origin is the
// virtual-screen top-left (+1px, see StartDrawing).
D2D1_RECT_F Highlighter::ComputeKeystrokeAnchorRect() const
{
const int originX = GetSystemMetrics(SM_XVIRTUALSCREEN) + 1;
const int originY = GetSystemMetrics(SM_YVIRTUALSCREEN) + 1;
RECT work{};
// If the user cycled to a specific monitor, anchor there.
if (m_keystrokeMonitorOverride >= 0)
{
std::vector<MONITORINFO> monitors;
EnumDisplayMonitors(nullptr, nullptr, MonitorEnumProc, reinterpret_cast<LPARAM>(&monitors));
if (m_keystrokeMonitorOverride < static_cast<int>(monitors.size()))
{
work = monitors[m_keystrokeMonitorOverride].rcWork;
}
}
// Otherwise anchor to the monitor under the cursor so the overlay follows the
// user across displays; fall back to the primary work area.
if (work.right == 0 && work.bottom == 0)
{
POINT pt{};
HMONITOR mon = nullptr;
if (GetCursorPos(&pt))
{
mon = MonitorFromPoint(pt, MONITOR_DEFAULTTOPRIMARY);
}
if (mon)
{
MONITORINFO mi{ sizeof(mi) };
if (GetMonitorInfo(mon, &mi))
{
work = mi.rcWork;
}
}
}
if (work.right == 0 && work.bottom == 0)
{
if (!SystemParametersInfo(SPI_GETWORKAREA, 0, &work, 0))
{
work = { 0, 0, GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN) };
}
}
return D2D1_RECT_F{
static_cast<float>(work.left - originX),
static_cast<float>(work.top - originY),
static_cast<float>(work.right - originX),
static_cast<float>(work.bottom - originY),
};
}
BOOL CALLBACK Highlighter::MonitorEnumProc(HMONITOR monitor, HDC /*hdc*/, LPRECT /*rect*/, LPARAM data) noexcept
{
auto* monitors = reinterpret_cast<std::vector<MONITORINFO>*>(data);
MONITORINFO mi{ sizeof(mi) };
if (GetMonitorInfo(monitor, &mi))
{
monitors->push_back(mi);
}
return TRUE;
}
// Move the keystroke overlay to the next monitor (wrapping). Starts from the
// monitor under the cursor the first time the user cycles.
void Highlighter::CycleKeystrokeMonitor()
{
std::vector<MONITORINFO> monitors;
EnumDisplayMonitors(nullptr, nullptr, MonitorEnumProc, reinterpret_cast<LPARAM>(&monitors));
const int count = static_cast<int>(monitors.size());
if (count == 0)
{
return;
}
int index = m_keystrokeMonitorOverride;
if (index < 0 || index >= count)
{
// Seed from the monitor currently under the cursor.
index = 0;
POINT pt{};
if (GetCursorPos(&pt))
{
for (int i = 0; i < count; ++i)
{
if (PtInRect(&monitors[i].rcMonitor, pt))
{
index = i;
break;
}
}
}
}
m_keystrokeMonitorOverride = (index + 1) % count;
m_keystrokeRenderer.SetAnchorRect(ComputeKeystrokeAnchorRect());
}
// Cycle to the next keystroke display mode at runtime (not persisted).
void Highlighter::CycleKeystrokeDisplayMode()
{
const int next = (static_cast<int>(m_keystrokeSettings.displayMode) + 1) % 4;
m_keystrokeSettings.displayMode = static_cast<InputHighlighter::DisplayMode>(next);
m_keystrokeProcessor.ResetBuffer();
m_keystrokeRenderer.Clear();
m_keystrokeRenderer.ApplySettings(m_keystrokeSettings);
}
void Highlighter::StartKeystrokes()
{
if (!m_keystrokeRenderer.IsInitialized())
{
return;
}
m_keystrokeProcessor.ResetBuffer();
m_keystrokeRenderer.ApplySettings(m_keystrokeSettings);
m_keystrokeRenderer.SetAnchorRect(ComputeKeystrokeAnchorRect());
HWND hwnd = m_hwnd;
m_keyboardCapture.SetHotkeyHandler([hwnd](int hotkeyId) {
// Runs on the capture thread: hand off to the composition thread.
PostMessage(hwnd, WM_KEYSTROKE_HOTKEY, static_cast<WPARAM>(hotkeyId), 0);
});
m_keyboardCapture.SetHotkeys(m_switchMonitorHotkey, m_switchDisplayModeHotkey);
const bool started = m_keyboardCapture.Start([hwnd]() {
// Runs on the capture thread: hand off to the composition thread to drain.
PostMessage(hwnd, WM_DRAIN_KEYSTROKES, 0, 0);
});
if (started)
{
SetTimer(m_hwnd, KEYSTROKE_TICK_TIMER_ID, KEYSTROKE_TICK_INTERVAL_MS, nullptr);
}
else
{
Logger::warn("Failed to start keyboard capture for keystroke overlay.");
}
}
void Highlighter::StopKeystrokes()
{
m_keyboardCapture.Stop();
KillTimer(m_hwnd, KEYSTROKE_TICK_TIMER_ID);
m_keystrokeRenderer.Clear();
m_keystrokeProcessor.ResetBuffer();
m_keystrokeMonitorOverride = -1;
}
void Highlighter::DrainKeystrokes()
{
if (!m_keystrokeRenderer.IsInitialized())
{
return;
}
const auto mode = static_cast<InputHighlighter::DisplayMode>(m_keystrokeSettings.displayMode);
InputHighlighter::KeystrokeEvent e;
while (m_keyboardCapture.TryPop(e))
{
const auto result = m_keystrokeProcessor.Process(e, mode);
if (result.action != InputHighlighter::KeystrokeAction::None)
{
m_keystrokeRenderer.OnResult(result);
}
}
}
void Highlighter::ApplySettings(MouseHighlighterSettings settings)
{
m_radius = static_cast<float>(settings.radius);
@@ -693,6 +921,41 @@ void Highlighter::ApplySettings(MouseHighlighterSettings settings)
m_rippleShowDragTrail = settings.rippleShowDragTrail;
m_rippleShowReleasePulse = settings.rippleShowReleasePulse;
// Input Highlighter sub-toggles.
m_showMouse = settings.showMouse;
m_showKeystrokes = settings.showKeystrokes;
// Translate keystroke settings into the renderer's settings snapshot.
m_keystrokeSettings.displayMode = static_cast<InputHighlighter::DisplayMode>(settings.keystrokeDisplayMode);
m_keystrokeSettings.position = static_cast<InputHighlighter::KeystrokePosition>(settings.keystrokePosition);
m_keystrokeSettings.timeoutMs = settings.keystrokeTimeoutMs;
m_keystrokeSettings.textSize = static_cast<float>(settings.keystrokeTextSize);
m_keystrokeSettings.textColor = settings.keystrokeTextColor;
m_keystrokeSettings.backgroundColor = settings.keystrokeBackgroundColor;
m_keystrokeSettings.strokeColor = settings.keystrokeStrokeColor;
m_keystrokeSettings.strokeThickness = settings.keystrokeStrokeThickness;
// Overlay control shortcuts; push live so changes take effect without restart.
m_switchMonitorHotkey = settings.keystrokeSwitchMonitorHotkey;
m_switchDisplayModeHotkey = settings.keystrokeSwitchDisplayModeHotkey;
m_keyboardCapture.SetHotkeys(m_switchMonitorHotkey, m_switchDisplayModeHotkey);
if (m_keystrokeRenderer.IsInitialized())
{
m_keystrokeRenderer.ApplySettings(m_keystrokeSettings);
}
// When the mouse half is disabled, force all pointer visuals off so only the
// keystroke overlay is active.
if (!m_showMouse)
{
m_leftPointerEnabled = false;
m_rightPointerEnabled = false;
m_alwaysPointerEnabled = false;
m_spotlightMode = false;
m_rippleMode = false;
}
// Reset transient pressed-state flag so a settings change while a button
// happens to be down doesn't leave the spotlight stuck at a shrunken size.
m_spotlightPressed = false;
@@ -753,6 +1016,19 @@ LRESULT CALLBACK Highlighter::WndProc(HWND hWnd, UINT message, WPARAM wParam, LP
instance->StartDrawing();
}
break;
case WM_DRAIN_KEYSTROKES:
instance->DrainKeystrokes();
break;
case WM_KEYSTROKE_HOTKEY:
if (wParam == 0)
{
instance->CycleKeystrokeMonitor();
}
else if (wParam == 1)
{
instance->CycleKeystrokeDisplayMode();
}
break;
case WM_DESTROY:
instance->DestroyHighlighter();
break;
@@ -794,6 +1070,9 @@ LRESULT CALLBACK Highlighter::WndProc(HWND hWnd, UINT message, WPARAM wParam, LP
instance->SpawnRippleHoldDot(MouseButton::Right);
}
break;
case KEYSTROKE_TICK_TIMER_ID:
instance->m_keystrokeRenderer.Tick();
break;
}
break;
}

View File

@@ -1,6 +1,8 @@
#pragma once
#include "pch.h"
#include "Keystrokes/KeystrokeTypes.h"
const winrt::Windows::UI::Color MOUSE_HIGHLIGHTER_DEFAULT_LEFT_BUTTON_COLOR = winrt::Windows::UI::ColorHelper::FromArgb(166, 255, 255, 0);
const winrt::Windows::UI::Color MOUSE_HIGHLIGHTER_DEFAULT_RIGHT_BUTTON_COLOR = winrt::Windows::UI::ColorHelper::FromArgb(166, 0, 0, 255);
const winrt::Windows::UI::Color MOUSE_HIGHLIGHTER_DEFAULT_ALWAYS_COLOR = winrt::Windows::UI::ColorHelper::FromArgb(0, 255, 0, 0);
@@ -14,6 +16,18 @@ constexpr double MOUSE_HIGHLIGHTER_DEFAULT_RIPPLE_INTENSITY = 0.7;
constexpr int MOUSE_HIGHLIGHTER_DEFAULT_RIPPLE_DURATION_MS = 480;
constexpr bool MOUSE_HIGHLIGHTER_DEFAULT_RIPPLE_SHOW_DRAG_TRAIL = true;
constexpr bool MOUSE_HIGHLIGHTER_DEFAULT_RIPPLE_SHOW_RELEASE_PULSE = true;
// Input Highlighter keystroke-overlay defaults.
const winrt::Windows::UI::Color INPUT_HIGHLIGHTER_DEFAULT_KEYSTROKE_TEXT_COLOR = winrt::Windows::UI::ColorHelper::FromArgb(255, 255, 255, 255);
const winrt::Windows::UI::Color INPUT_HIGHLIGHTER_DEFAULT_KEYSTROKE_BACKGROUND_COLOR = winrt::Windows::UI::ColorHelper::FromArgb(128, 0, 0, 0);
const winrt::Windows::UI::Color INPUT_HIGHLIGHTER_DEFAULT_KEYSTROKE_STROKE_COLOR = winrt::Windows::UI::ColorHelper::FromArgb(0, 255, 255, 255);
constexpr int INPUT_HIGHLIGHTER_DEFAULT_KEYSTROKE_STROKE_THICKNESS = 0;
constexpr bool INPUT_HIGHLIGHTER_DEFAULT_SHOW_MOUSE = true;
constexpr bool INPUT_HIGHLIGHTER_DEFAULT_SHOW_KEYSTROKES = true;
constexpr int INPUT_HIGHLIGHTER_DEFAULT_KEYSTROKE_DISPLAY_MODE = 0; // Last5
constexpr int INPUT_HIGHLIGHTER_DEFAULT_KEYSTROKE_POSITION = 4; // BottomCenter
constexpr int INPUT_HIGHLIGHTER_DEFAULT_KEYSTROKE_TIMEOUT_MS = 3000;
constexpr int INPUT_HIGHLIGHTER_DEFAULT_KEYSTROKE_TEXT_SIZE = 24;
constexpr bool INPUT_HIGHLIGHTER_DEFAULT_KEYSTROKE_DRAGGABLE = true;
struct MouseHighlighterSettings
{
@@ -31,6 +45,22 @@ struct MouseHighlighterSettings
int rippleDurationMs = MOUSE_HIGHLIGHTER_DEFAULT_RIPPLE_DURATION_MS;
bool rippleShowDragTrail = MOUSE_HIGHLIGHTER_DEFAULT_RIPPLE_SHOW_DRAG_TRAIL;
bool rippleShowReleasePulse = MOUSE_HIGHLIGHTER_DEFAULT_RIPPLE_SHOW_RELEASE_PULSE;
// Input Highlighter sub-toggles + keystroke overlay settings.
bool showMouse = INPUT_HIGHLIGHTER_DEFAULT_SHOW_MOUSE;
bool showKeystrokes = INPUT_HIGHLIGHTER_DEFAULT_SHOW_KEYSTROKES;
int keystrokeDisplayMode = INPUT_HIGHLIGHTER_DEFAULT_KEYSTROKE_DISPLAY_MODE;
int keystrokePosition = INPUT_HIGHLIGHTER_DEFAULT_KEYSTROKE_POSITION;
int keystrokeTimeoutMs = INPUT_HIGHLIGHTER_DEFAULT_KEYSTROKE_TIMEOUT_MS;
int keystrokeTextSize = INPUT_HIGHLIGHTER_DEFAULT_KEYSTROKE_TEXT_SIZE;
winrt::Windows::UI::Color keystrokeTextColor = INPUT_HIGHLIGHTER_DEFAULT_KEYSTROKE_TEXT_COLOR;
winrt::Windows::UI::Color keystrokeBackgroundColor = INPUT_HIGHLIGHTER_DEFAULT_KEYSTROKE_BACKGROUND_COLOR;
winrt::Windows::UI::Color keystrokeStrokeColor = INPUT_HIGHLIGHTER_DEFAULT_KEYSTROKE_STROKE_COLOR;
int keystrokeStrokeThickness = INPUT_HIGHLIGHTER_DEFAULT_KEYSTROKE_STROKE_THICKNESS;
bool keystrokeDraggable = INPUT_HIGHLIGHTER_DEFAULT_KEYSTROKE_DRAGGABLE;
// Overlay control shortcuts (handled inside the keystroke capture hook).
// Defaults mirror the C# model: Ctrl+Win+/ and Ctrl+Win+D.
InputHighlighter::HotkeyChord keystrokeSwitchMonitorHotkey{ true, false, false, true, 0xBF };
InputHighlighter::HotkeyChord keystrokeSwitchDisplayModeHotkey{ true, false, false, true, 0x44 };
};
int MouseHighlighterMain(HINSTANCE hinst, MouseHighlighterSettings settings);

View File

@@ -87,6 +87,11 @@
<ClInclude Include="pch.h" />
<ClInclude Include="trace.h" />
<ClInclude Include="resource.h" />
<ClInclude Include="Keystrokes\KeystrokeTypes.h" />
<ClInclude Include="Keystrokes\KeystrokeFormatter.h" />
<ClInclude Include="Keystrokes\KeystrokeProcessor.h" />
<ClInclude Include="Keystrokes\KeyboardCapture.h" />
<ClInclude Include="Keystrokes\KeystrokeRenderer.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="dllmain.cpp" />
@@ -95,6 +100,18 @@
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
</ClCompile>
<ClCompile Include="trace.cpp" />
<ClCompile Include="Keystrokes\KeystrokeFormatter.cpp">
<PrecompiledHeader>NotUsing</PrecompiledHeader>
</ClCompile>
<ClCompile Include="Keystrokes\KeystrokeProcessor.cpp">
<PrecompiledHeader>NotUsing</PrecompiledHeader>
</ClCompile>
<ClCompile Include="Keystrokes\KeyboardCapture.cpp">
<PrecompiledHeader>NotUsing</PrecompiledHeader>
</ClCompile>
<ClCompile Include="Keystrokes\KeystrokeRenderer.cpp">
<PrecompiledHeader>NotUsing</PrecompiledHeader>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="MouseHighlighter.rc" />

View File

@@ -13,6 +13,18 @@
<ClCompile Include="MouseHighlighter.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="Keystrokes\KeystrokeFormatter.cpp">
<Filter>Keystrokes</Filter>
</ClCompile>
<ClCompile Include="Keystrokes\KeystrokeProcessor.cpp">
<Filter>Keystrokes</Filter>
</ClCompile>
<ClCompile Include="Keystrokes\KeyboardCapture.cpp">
<Filter>Keystrokes</Filter>
</ClCompile>
<ClCompile Include="Keystrokes\KeystrokeRenderer.cpp">
<Filter>Keystrokes</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="pch.h">
@@ -27,6 +39,21 @@
<ClInclude Include="MouseHighlighter.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Keystrokes\KeystrokeTypes.h">
<Filter>Keystrokes</Filter>
</ClInclude>
<ClInclude Include="Keystrokes\KeystrokeFormatter.h">
<Filter>Keystrokes</Filter>
</ClInclude>
<ClInclude Include="Keystrokes\KeystrokeProcessor.h">
<Filter>Keystrokes</Filter>
</ClInclude>
<ClInclude Include="Keystrokes\KeyboardCapture.h">
<Filter>Keystrokes</Filter>
</ClInclude>
<ClInclude Include="Keystrokes\KeystrokeRenderer.h">
<Filter>Keystrokes</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<Filter Include="Source Files">
@@ -37,6 +64,9 @@
<UniqueIdentifier>{c8345550-9836-40a0-b473-0f4bf6129568}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions>
</Filter>
<Filter Include="Keystrokes">
<UniqueIdentifier>{a4f2d3e1-6b7c-4c9a-8f2e-2d1c3b4a5e6f}</UniqueIdentifier>
</Filter>
<Filter Include="Resource Files">
<UniqueIdentifier>{7934ee5b-8427-486d-9324-73b6bcf60eed}</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>

View File

@@ -27,6 +27,20 @@ namespace
const wchar_t JSON_KEY_RIPPLE_DURATION_MS[] = L"ripple_duration_ms";
const wchar_t JSON_KEY_RIPPLE_SHOW_DRAG_TRAIL[] = L"ripple_show_drag_trail";
const wchar_t JSON_KEY_RIPPLE_SHOW_RELEASE_PULSE[] = L"ripple_show_release_pulse";
// Input Highlighter keystroke-overlay keys.
const wchar_t JSON_KEY_SHOW_MOUSE[] = L"show_mouse";
const wchar_t JSON_KEY_SHOW_KEYSTROKES[] = L"show_keystrokes";
const wchar_t JSON_KEY_KEYSTROKE_DISPLAY_MODE[] = L"keystroke_display_mode";
const wchar_t JSON_KEY_KEYSTROKE_POSITION[] = L"keystroke_position";
const wchar_t JSON_KEY_KEYSTROKE_TIMEOUT_MS[] = L"keystroke_timeout_ms";
const wchar_t JSON_KEY_KEYSTROKE_TEXT_SIZE[] = L"keystroke_text_size";
const wchar_t JSON_KEY_KEYSTROKE_TEXT_COLOR[] = L"keystroke_text_color";
const wchar_t JSON_KEY_KEYSTROKE_BACKGROUND_COLOR[] = L"keystroke_background_color";
const wchar_t JSON_KEY_KEYSTROKE_STROKE_COLOR[] = L"keystroke_stroke_color";
const wchar_t JSON_KEY_KEYSTROKE_STROKE_THICKNESS[] = L"keystroke_stroke_thickness";
const wchar_t JSON_KEY_KEYSTROKE_DRAGGABLE[] = L"keystroke_draggable";
const wchar_t JSON_KEY_KEYSTROKE_SWITCH_MONITOR_HOTKEY[] = L"keystroke_switch_monitor_hotkey";
const wchar_t JSON_KEY_KEYSTROKE_SWITCH_DISPLAY_MODE_HOTKEY[] = L"keystroke_switch_display_mode_hotkey";
}
extern "C" IMAGE_DOS_HEADER __ImageBase;
@@ -482,6 +496,199 @@ public:
{
Logger::warn("Failed to initialize ripple show release pulse from settings. Will use default value");
}
// ---- Input Highlighter sub-toggles + keystroke overlay ----
try
{
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_SHOW_MOUSE);
highlightSettings.showMouse = jsonPropertiesObject.GetNamedBoolean(JSON_KEY_VALUE);
}
catch (...)
{
Logger::warn("Failed to initialize show mouse from settings. Will use default value");
}
try
{
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_SHOW_KEYSTROKES);
highlightSettings.showKeystrokes = jsonPropertiesObject.GetNamedBoolean(JSON_KEY_VALUE);
}
catch (...)
{
Logger::warn("Failed to initialize show keystrokes from settings. Will use default value");
}
try
{
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_KEYSTROKE_DISPLAY_MODE);
int value = static_cast<int>(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
if (value >= 0 && value <= 3)
{
highlightSettings.keystrokeDisplayMode = value;
}
else
{
throw std::runtime_error("Invalid keystroke display mode value");
}
}
catch (...)
{
Logger::warn("Failed to initialize keystroke display mode from settings. Will use default value");
}
try
{
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_KEYSTROKE_POSITION);
int value = static_cast<int>(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
if (value >= 0 && value <= 5)
{
highlightSettings.keystrokePosition = value;
}
else
{
throw std::runtime_error("Invalid keystroke position value");
}
}
catch (...)
{
Logger::warn("Failed to initialize keystroke position from settings. Will use default value");
}
try
{
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_KEYSTROKE_TIMEOUT_MS);
int value = static_cast<int>(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
if (value > 0)
{
highlightSettings.keystrokeTimeoutMs = value;
}
else
{
throw std::runtime_error("Invalid keystroke timeout value");
}
}
catch (...)
{
Logger::warn("Failed to initialize keystroke timeout from settings. Will use default value");
}
try
{
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_KEYSTROKE_TEXT_SIZE);
int value = static_cast<int>(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
if (value > 0)
{
highlightSettings.keystrokeTextSize = value;
}
else
{
throw std::runtime_error("Invalid keystroke text size value");
}
}
catch (...)
{
Logger::warn("Failed to initialize keystroke text size from settings. Will use default value");
}
// Text/background/stroke colors are stored as "#AARRGGBB" (alpha baked in).
try
{
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_KEYSTROKE_TEXT_COLOR);
auto textColor = (std::wstring)jsonPropertiesObject.GetNamedString(JSON_KEY_VALUE);
uint8_t a, r, g, b;
if (!checkValidARGB(textColor, &a, &r, &g, &b))
{
Logger::error("Keystroke text color ARGB value is invalid. Will use default value");
}
else
{
highlightSettings.keystrokeTextColor = winrt::Windows::UI::ColorHelper::FromArgb(a, r, g, b);
}
}
catch (...)
{
Logger::warn("Failed to initialize keystroke text color from settings. Will use default value");
}
try
{
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_KEYSTROKE_BACKGROUND_COLOR);
auto backgroundColor = (std::wstring)jsonPropertiesObject.GetNamedString(JSON_KEY_VALUE);
uint8_t a, r, g, b;
if (!checkValidARGB(backgroundColor, &a, &r, &g, &b))
{
Logger::error("Keystroke background color ARGB value is invalid. Will use default value");
}
else
{
highlightSettings.keystrokeBackgroundColor = winrt::Windows::UI::ColorHelper::FromArgb(a, r, g, b);
}
}
catch (...)
{
Logger::warn("Failed to initialize keystroke background color from settings. Will use default value");
}
try
{
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_KEYSTROKE_STROKE_COLOR);
auto strokeColor = (std::wstring)jsonPropertiesObject.GetNamedString(JSON_KEY_VALUE);
uint8_t a, r, g, b;
if (!checkValidARGB(strokeColor, &a, &r, &g, &b))
{
Logger::error("Keystroke stroke color ARGB value is invalid. Will use default value");
}
else
{
highlightSettings.keystrokeStrokeColor = winrt::Windows::UI::ColorHelper::FromArgb(a, r, g, b);
}
}
catch (...)
{
Logger::warn("Failed to initialize keystroke stroke color from settings. Will use default value");
}
try
{
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_KEYSTROKE_STROKE_THICKNESS);
int value = static_cast<int>(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
if (value >= 0)
{
highlightSettings.keystrokeStrokeThickness = value;
}
}
catch (...)
{
Logger::warn("Failed to initialize keystroke stroke thickness from settings. Will use default value");
}
try
{
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_KEYSTROKE_DRAGGABLE);
highlightSettings.keystrokeDraggable = jsonPropertiesObject.GetNamedBoolean(JSON_KEY_VALUE);
}
catch (...)
{
Logger::warn("Failed to initialize keystroke draggable from settings. Will use default value");
}
try
{
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_KEYSTROKE_SWITCH_MONITOR_HOTKEY);
auto hotkey = PowerToysSettings::HotkeyObject::from_json(jsonPropertiesObject);
highlightSettings.keystrokeSwitchMonitorHotkey.win = hotkey.win_pressed();
highlightSettings.keystrokeSwitchMonitorHotkey.ctrl = hotkey.ctrl_pressed();
highlightSettings.keystrokeSwitchMonitorHotkey.shift = hotkey.shift_pressed();
highlightSettings.keystrokeSwitchMonitorHotkey.alt = hotkey.alt_pressed();
highlightSettings.keystrokeSwitchMonitorHotkey.vk = hotkey.get_code();
}
catch (...)
{
Logger::warn("Failed to initialize keystroke switch-monitor shortcut. Will use default value");
}
try
{
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_KEYSTROKE_SWITCH_DISPLAY_MODE_HOTKEY);
auto hotkey = PowerToysSettings::HotkeyObject::from_json(jsonPropertiesObject);
highlightSettings.keystrokeSwitchDisplayModeHotkey.win = hotkey.win_pressed();
highlightSettings.keystrokeSwitchDisplayModeHotkey.ctrl = hotkey.ctrl_pressed();
highlightSettings.keystrokeSwitchDisplayModeHotkey.shift = hotkey.shift_pressed();
highlightSettings.keystrokeSwitchDisplayModeHotkey.alt = hotkey.alt_pressed();
highlightSettings.keystrokeSwitchDisplayModeHotkey.vk = hotkey.get_code();
}
catch (...)
{
Logger::warn("Failed to initialize keystroke switch-display-mode shortcut. Will use default value");
}
}
else
{

View File

@@ -13,6 +13,13 @@ namespace Microsoft.PowerToys.Settings.UI.Library
[CmdConfigureIgnore]
public HotkeySettings DefaultActivationShortcut => new HotkeySettings(true, false, false, true, 0x48);
// Input Highlighter keystroke-overlay defaults.
[CmdConfigureIgnore]
public HotkeySettings DefaultKeystrokeSwitchMonitorHotkey => new HotkeySettings(true, true, false, false, 0xBF); // Win+Ctrl+/
[CmdConfigureIgnore]
public HotkeySettings DefaultKeystrokeSwitchDisplayModeHotkey => new HotkeySettings(true, true, false, false, 0x44); // Win+Ctrl+D
[JsonPropertyName("activation_shortcut")]
public HotkeySettings ActivationShortcut { get; set; }
@@ -62,6 +69,49 @@ namespace Microsoft.PowerToys.Settings.UI.Library
[JsonPropertyName("ripple_show_release_pulse")]
public BoolProperty RippleShowReleasePulse { get; set; }
// ---- Input Highlighter sub-toggles ----
[JsonPropertyName("show_mouse")]
public BoolProperty ShowMouse { get; set; }
[JsonPropertyName("show_keystrokes")]
public BoolProperty ShowKeystrokes { get; set; }
// ---- Keystroke overlay properties ----
[JsonPropertyName("keystroke_switch_monitor_hotkey")]
public HotkeySettings KeystrokeSwitchMonitorHotkey { get; set; }
[JsonPropertyName("keystroke_switch_display_mode_hotkey")]
public HotkeySettings KeystrokeSwitchDisplayModeHotkey { get; set; }
// 0 = Last5, 1 = SingleCharactersOnly, 2 = ShortcutsOnly, 3 = Stream
[JsonPropertyName("keystroke_display_mode")]
public IntProperty KeystrokeDisplayMode { get; set; }
// 0 = TopLeft, 1 = TopCenter, 2 = TopRight, 3 = BottomLeft, 4 = BottomCenter, 5 = BottomRight
[JsonPropertyName("keystroke_position")]
public IntProperty KeystrokePosition { get; set; }
[JsonPropertyName("keystroke_timeout_ms")]
public IntProperty KeystrokeTimeoutMs { get; set; }
[JsonPropertyName("keystroke_text_size")]
public IntProperty KeystrokeTextSize { get; set; }
[JsonPropertyName("keystroke_text_color")]
public StringProperty KeystrokeTextColor { get; set; }
[JsonPropertyName("keystroke_background_color")]
public StringProperty KeystrokeBackgroundColor { get; set; }
[JsonPropertyName("keystroke_stroke_color")]
public StringProperty KeystrokeStrokeColor { get; set; }
[JsonPropertyName("keystroke_stroke_thickness")]
public IntProperty KeystrokeStrokeThickness { get; set; }
[JsonPropertyName("keystroke_draggable")]
public BoolProperty KeystrokeDraggable { get; set; }
public MouseHighlighterProperties()
{
ActivationShortcut = DefaultActivationShortcut;
@@ -80,6 +130,24 @@ namespace Microsoft.PowerToys.Settings.UI.Library
RippleDurationMs = new IntProperty(480);
RippleShowDragTrail = new BoolProperty(true);
RippleShowReleasePulse = new BoolProperty(true);
// Input Highlighter sub-toggles. Fresh installs get both halves enabled;
// existing Mouse Highlighter users are migrated to mouse-only in
// MouseHighlighterSettings.UpgradeSettingsConfiguration to avoid surprise.
ShowMouse = new BoolProperty(true);
ShowKeystrokes = new BoolProperty(true);
KeystrokeSwitchMonitorHotkey = DefaultKeystrokeSwitchMonitorHotkey;
KeystrokeSwitchDisplayModeHotkey = DefaultKeystrokeSwitchDisplayModeHotkey;
KeystrokeDisplayMode = new IntProperty(0); // Last5
KeystrokePosition = new IntProperty(4); // BottomCenter
KeystrokeTimeoutMs = new IntProperty(3000);
KeystrokeTextSize = new IntProperty(24);
KeystrokeTextColor = new StringProperty("#FFFFFFFF");
KeystrokeBackgroundColor = new StringProperty("#80000000");
KeystrokeStrokeColor = new StringProperty("#00FFFFFF");
KeystrokeStrokeThickness = new IntProperty(0);
KeystrokeDraggable = new BoolProperty(true);
}
}
}

View File

@@ -23,7 +23,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
{
Name = ModuleName;
Properties = new MouseHighlighterProperties();
Version = "1.2";
Version = "1.3";
}
public string GetModuleName()
@@ -49,6 +49,8 @@ namespace Microsoft.PowerToys.Settings.UI.Library
// This can be utilized in the future if the settings.json file is to be modified/deleted.
public bool UpgradeSettingsConfiguration()
{
bool upgraded = false;
if (Version == "1.0" || Version == "1.1")
{
string opacity;
@@ -65,10 +67,20 @@ namespace Microsoft.PowerToys.Settings.UI.Library
Properties.LeftButtonClickColor = new StringProperty(string.Concat("#", opacity, Properties.LeftButtonClickColor.Value.ToString().Substring(1, 6)));
Properties.RightButtonClickColor = new StringProperty(string.Concat("#", opacity, Properties.RightButtonClickColor.Value.ToString().Substring(1, 6)));
Version = "1.2";
return true;
upgraded = true;
}
return false;
// 1.2 -> 1.3: Mouse Highlighter becomes "Input Highlighter" (adds keystroke
// overlay). Existing users keep their current mouse-only behavior; the new
// keystroke half stays off until they opt in.
if (Version == "1.2")
{
Properties.ShowKeystrokes = new BoolProperty(false);
Version = "1.3";
upgraded = true;
}
return upgraded;
}
}
}

View File

@@ -427,6 +427,7 @@ namespace Microsoft.PowerToys.Settings.UI
case "ImageResizer": return typeof(ImageResizerPage);
case "KBM": return typeof(KeyboardManagerPage);
case "MouseUtils": return typeof(MouseUtilsPage);
case "InputHighlighter": return typeof(InputHighlighterPage);
case "MouseWithoutBorders": return typeof(MouseWithoutBordersPage);
case "Peek": return typeof(PeekPage);
case "PowerAccent": return typeof(PowerAccentPage);

View File

@@ -0,0 +1,239 @@
<local:NavigablePage
x:Class="Microsoft.PowerToys.Settings.UI.Views.InputHighlighterPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls"
xmlns:converters="using:Microsoft.PowerToys.Settings.UI.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:Microsoft.PowerToys.Settings.UI.Helpers"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ptcontrols="using:Microsoft.PowerToys.Common.UI.Controls"
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
xmlns:ui="using:CommunityToolkit.WinUI"
AutomationProperties.LandmarkType="Main"
mc:Ignorable="d">
<local:NavigablePage.Resources>
<converters:IndexBitFieldToVisibilityConverter x:Key="IndexBitFieldToVisibilityConverter" />
</local:NavigablePage.Resources>
<controls:SettingsPageControl x:Uid="InputHighlighter" ModuleImageSource="ms-appx:///Assets/Settings/Modules/MouseUtils.png">
<controls:SettingsPageControl.ModuleContent>
<StackPanel ChildrenTransitions="{StaticResource SettingsCardsAnimations}" Orientation="Vertical">
<controls:SettingsGroup x:Uid="InputHighlighter_Activation" AutomationProperties.AutomationId="InputHighlighter_ActivationId">
<controls:GPOInfoControl ShowWarning="{x:Bind ViewModel.IsHighlighterEnabledGpoConfigured, Mode=OneWay}">
<tkcontrols:SettingsCard
Name="InputHighlighterEnableMouseHighlighter"
x:Uid="MouseUtils_Enable_MouseHighlighter"
HeaderIcon="{ui:BitmapIcon Source=/Assets/Settings/Icons/MouseHighlighter.png}">
<ToggleSwitch
x:Uid="ToggleSwitch"
AutomationProperties.AutomationId="InputHighlighter_MouseHighlighterToggleId"
IsOn="{x:Bind ViewModel.IsMouseHighlighterEnabled, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
</controls:GPOInfoControl>
<tkcontrols:SettingsCard
x:Uid="InputHighlighter_Enable"
HeaderIcon="{ui:FontIcon Glyph=&#xE765;}"
IsEnabled="{x:Bind ViewModel.IsMouseHighlighterEnabled, Mode=OneWay}">
<ToggleSwitch
x:Uid="ToggleSwitch"
AutomationProperties.AutomationId="InputHighlighter_ShowKeystrokesId"
IsOn="{x:Bind ViewModel.ShowKeystrokes, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
</controls:SettingsGroup>
<controls:SettingsGroup x:Uid="MouseUtils_MouseHighlighter" AutomationProperties.AutomationId="MouseUtils_MouseHighlighterTestId">
<tkcontrols:SettingsExpander
Name="MouseUtilsMouseHighlighterActivationShortcut"
x:Uid="MouseUtils_MouseHighlighter_ActivationShortcut"
AutomationProperties.AutomationId="MouseUtils_MouseHighlighterActivationShortcutId"
HeaderIcon="{ui:FontIcon Glyph=&#xEDA7;}"
IsEnabled="{x:Bind ViewModel.IsMouseHighlighterEnabled, Mode=OneWay}">
<controls:ShortcutControl HotkeySettings="{x:Bind Path=ViewModel.MouseHighlighterActivationShortcut, Mode=TwoWay}" />
<tkcontrols:SettingsExpander.Items>
<tkcontrols:SettingsCard ContentAlignment="Left">
<CheckBox x:Uid="MouseUtils_AutoActivate" IsChecked="{x:Bind ViewModel.MouseHighlighterAutoActivate, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
</tkcontrols:SettingsExpander.Items>
</tkcontrols:SettingsExpander>
<tkcontrols:SettingsExpander
Name="MouseHighlighterAppearanceBehavior"
x:Uid="MouseUtils_MouseHighlighter_Appearance"
AutomationProperties.AutomationId="MouseUtils_MouseHighlighterAppearanceBehaviorId"
HeaderIcon="{ui:FontIcon Glyph=&#xE790;}"
IsEnabled="{x:Bind ViewModel.IsMouseHighlighterEnabled, Mode=OneWay}">
<ComboBox
x:Uid="MouseUtils_MouseHighlighter_SpotlightModeType"
MinWidth="{StaticResource SettingActionControlMinWidth}"
SelectedIndex="{x:Bind ViewModel.HighlightModeIndex, Mode=TwoWay}">
<ComboBoxItem x:Uid="HighlightMode_Spotlight_Mode" />
<ComboBoxItem x:Uid="HighlightMode_Circle_Highlight_Mode" />
<ComboBoxItem x:Uid="HighlightMode_Ripple_Mode" />
</ComboBox>
<tkcontrols:SettingsExpander.Items>
<tkcontrols:SettingsCard ContentAlignment="Left">
<ptcontrols:CheckBoxWithDescriptionControl x:Uid="MouseUtils_InputHighlighter_ShowMouse" IsChecked="{x:Bind ViewModel.ShowMouse, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard x:Uid="MouseUtils_MouseHighlighter_PrimaryButtonClickColor" Visibility="{x:Bind ViewModel.HighlightModeIndex, Mode=OneWay, Converter={StaticResource IndexBitFieldToVisibilityConverter}, ConverterParameter=0x6}">
<controls:ColorPickerButton IsAlphaEnabled="True" SelectedColor="{x:Bind Path=ViewModel.MouseHighlighterLeftButtonClickColor, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard x:Uid="MouseUtils_MouseHighlighter_SecondaryButtonClickColor" Visibility="{x:Bind ViewModel.HighlightModeIndex, Mode=OneWay, Converter={StaticResource IndexBitFieldToVisibilityConverter}, ConverterParameter=0x6}">
<controls:ColorPickerButton IsAlphaEnabled="True" SelectedColor="{x:Bind Path=ViewModel.MouseHighlighterRightButtonClickColor, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard
Name="MouseUtilsMouseHighlighterAlwaysColor"
x:Uid="MouseUtils_MouseHighlighter_AlwaysColor"
Visibility="{x:Bind ViewModel.HighlightModeIndex, Mode=OneWay, Converter={StaticResource IndexBitFieldToVisibilityConverter}, ConverterParameter=0x3}">
<controls:ColorPickerButton IsAlphaEnabled="True" SelectedColor="{x:Bind Path=ViewModel.MouseHighlighterAlwaysColor, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard x:Uid="MouseUtils_MouseHighlighter_HighlightRadius" Visibility="{x:Bind ViewModel.HighlightModeIndex, Mode=OneWay, Converter={StaticResource IndexBitFieldToVisibilityConverter}, ConverterParameter=0x3}">
<NumberBox
MinWidth="{StaticResource SettingActionControlMinWidth}"
LargeChange="10"
Minimum="5"
SmallChange="1"
SpinButtonPlacementMode="Compact"
Value="{x:Bind ViewModel.MouseHighlighterRadius, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard
Name="MouseUtilsMouseHighlighterFadeDelayMs"
x:Uid="MouseUtils_MouseHighlighter_FadeDelayMs"
Visibility="{x:Bind ViewModel.HighlightModeIndex, Mode=OneWay, Converter={StaticResource IndexBitFieldToVisibilityConverter}, ConverterParameter=0x3}">
<NumberBox
MinWidth="{StaticResource SettingActionControlMinWidth}"
LargeChange="100"
Minimum="0"
SmallChange="10"
SpinButtonPlacementMode="Compact"
Value="{x:Bind ViewModel.MouseHighlighterFadeDelayMs, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard
Name="MouseUtilsMouseHighlighterFadeDurationMs"
x:Uid="MouseUtils_MouseHighlighter_FadeDurationMs"
Visibility="{x:Bind ViewModel.HighlightModeIndex, Mode=OneWay, Converter={StaticResource IndexBitFieldToVisibilityConverter}, ConverterParameter=0x3}">
<NumberBox
MinWidth="{StaticResource SettingActionControlMinWidth}"
LargeChange="100"
Minimum="0"
SmallChange="10"
SpinButtonPlacementMode="Compact"
Value="{x:Bind ViewModel.MouseHighlighterFadeDurationMs, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard x:Uid="MouseUtils_MouseHighlighter_RippleSize" Visibility="{x:Bind ViewModel.HighlightModeIndex, Mode=OneWay, Converter={StaticResource IndexBitFieldToVisibilityConverter}, ConverterParameter=0x4}">
<NumberBox
MinWidth="{StaticResource SettingActionControlMinWidth}"
LargeChange="10"
Maximum="300"
Minimum="10"
SmallChange="1"
SpinButtonPlacementMode="Compact"
Value="{x:Bind ViewModel.RippleSize, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard x:Uid="MouseUtils_MouseHighlighter_RippleIntensity" Visibility="{x:Bind ViewModel.HighlightModeIndex, Mode=OneWay, Converter={StaticResource IndexBitFieldToVisibilityConverter}, ConverterParameter=0x4}">
<Slider
MinWidth="{StaticResource SettingActionControlMinWidth}"
LargeChange="0.1"
Maximum="1.35"
Minimum="0.15"
SmallChange="0.05"
StepFrequency="0.05"
Value="{x:Bind ViewModel.RippleIntensity, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard x:Uid="MouseUtils_MouseHighlighter_RippleDurationMs" Visibility="{x:Bind ViewModel.HighlightModeIndex, Mode=OneWay, Converter={StaticResource IndexBitFieldToVisibilityConverter}, ConverterParameter=0x4}">
<NumberBox
MinWidth="{StaticResource SettingActionControlMinWidth}"
LargeChange="100"
Maximum="2000"
Minimum="60"
SmallChange="10"
SpinButtonPlacementMode="Compact"
Value="{x:Bind ViewModel.RippleDurationMs, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard ContentAlignment="Left" Visibility="{x:Bind ViewModel.HighlightModeIndex, Mode=OneWay, Converter={StaticResource IndexBitFieldToVisibilityConverter}, ConverterParameter=0x4}">
<ptcontrols:CheckBoxWithDescriptionControl x:Uid="MouseUtils_MouseHighlighter_RippleShowDragTrail" IsChecked="{x:Bind ViewModel.RippleShowDragTrail, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard ContentAlignment="Left" Visibility="{x:Bind ViewModel.HighlightModeIndex, Mode=OneWay, Converter={StaticResource IndexBitFieldToVisibilityConverter}, ConverterParameter=0x4}">
<ptcontrols:CheckBoxWithDescriptionControl x:Uid="MouseUtils_MouseHighlighter_RippleShowReleasePulse" IsChecked="{x:Bind ViewModel.RippleShowReleasePulse, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
</tkcontrols:SettingsExpander.Items>
</tkcontrols:SettingsExpander>
</controls:SettingsGroup>
<controls:SettingsGroup x:Uid="InputHighlighter_Appearance" AutomationProperties.AutomationId="InputHighlighter_AppearanceId">
<tkcontrols:SettingsCard x:Uid="MouseUtils_InputHighlighter_DisplayMode" IsEnabled="{x:Bind ViewModel.ShowKeystrokes, Mode=OneWay}">
<ComboBox
MinWidth="{StaticResource SettingActionControlMinWidth}"
SelectedIndex="{x:Bind ViewModel.KeystrokeDisplayMode, Mode=TwoWay}">
<ComboBoxItem x:Uid="MouseUtils_InputHighlighter_DisplayMode_Last5" />
<ComboBoxItem x:Uid="MouseUtils_InputHighlighter_DisplayMode_SingleChars" />
<ComboBoxItem x:Uid="MouseUtils_InputHighlighter_DisplayMode_Shortcuts" />
<ComboBoxItem x:Uid="MouseUtils_InputHighlighter_DisplayMode_Stream" />
</ComboBox>
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard x:Uid="MouseUtils_InputHighlighter_Position" IsEnabled="{x:Bind ViewModel.ShowKeystrokes, Mode=OneWay}">
<ComboBox
MinWidth="{StaticResource SettingActionControlMinWidth}"
SelectedIndex="{x:Bind ViewModel.KeystrokePosition, Mode=TwoWay}">
<ComboBoxItem x:Uid="MouseUtils_InputHighlighter_Position_TopLeft" />
<ComboBoxItem x:Uid="MouseUtils_InputHighlighter_Position_TopCenter" />
<ComboBoxItem x:Uid="MouseUtils_InputHighlighter_Position_TopRight" />
<ComboBoxItem x:Uid="MouseUtils_InputHighlighter_Position_BottomLeft" />
<ComboBoxItem x:Uid="MouseUtils_InputHighlighter_Position_BottomCenter" />
<ComboBoxItem x:Uid="MouseUtils_InputHighlighter_Position_BottomRight" />
</ComboBox>
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard x:Uid="MouseUtils_InputHighlighter_TextColor" IsEnabled="{x:Bind ViewModel.ShowKeystrokes, Mode=OneWay}">
<controls:ColorPickerButton IsAlphaEnabled="True" SelectedColor="{x:Bind Path=ViewModel.KeystrokeTextColor, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard x:Uid="MouseUtils_InputHighlighter_BackgroundColor" IsEnabled="{x:Bind ViewModel.ShowKeystrokes, Mode=OneWay}">
<controls:ColorPickerButton IsAlphaEnabled="True" SelectedColor="{x:Bind Path=ViewModel.KeystrokeBackgroundColor, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard x:Uid="MouseUtils_InputHighlighter_StrokeColor" IsEnabled="{x:Bind ViewModel.ShowKeystrokes, Mode=OneWay}">
<controls:ColorPickerButton IsAlphaEnabled="True" SelectedColor="{x:Bind Path=ViewModel.KeystrokeStrokeColor, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard x:Uid="MouseUtils_InputHighlighter_StrokeThickness" IsEnabled="{x:Bind ViewModel.ShowKeystrokes, Mode=OneWay}">
<NumberBox
MinWidth="{StaticResource SettingActionControlMinWidth}"
LargeChange="1"
Maximum="10"
Minimum="0"
SmallChange="1"
SpinButtonPlacementMode="Compact"
Value="{x:Bind ViewModel.KeystrokeStrokeThickness, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard x:Uid="MouseUtils_InputHighlighter_TextSize" IsEnabled="{x:Bind ViewModel.ShowKeystrokes, Mode=OneWay}">
<NumberBox
MinWidth="{StaticResource SettingActionControlMinWidth}"
LargeChange="4"
Maximum="96"
Minimum="8"
SmallChange="1"
SpinButtonPlacementMode="Compact"
Value="{x:Bind ViewModel.KeystrokeTextSize, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard x:Uid="MouseUtils_InputHighlighter_Timeout" IsEnabled="{x:Bind ViewModel.ShowKeystrokes, Mode=OneWay}">
<NumberBox
MinWidth="{StaticResource SettingActionControlMinWidth}"
LargeChange="500"
Maximum="10000"
Minimum="200"
SmallChange="100"
SpinButtonPlacementMode="Compact"
Value="{x:Bind ViewModel.KeystrokeTimeoutMs, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard ContentAlignment="Left" IsEnabled="{x:Bind ViewModel.ShowKeystrokes, Mode=OneWay}">
<ptcontrols:CheckBoxWithDescriptionControl x:Uid="MouseUtils_InputHighlighter_Draggable" IsChecked="{x:Bind ViewModel.KeystrokeDraggable, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
</controls:SettingsGroup>
<controls:SettingsGroup x:Uid="InputHighlighter_Shortcuts" AutomationProperties.AutomationId="InputHighlighter_ShortcutsId">
<tkcontrols:SettingsCard x:Uid="MouseUtils_InputHighlighter_SwitchMonitorHotkey" IsEnabled="{x:Bind ViewModel.ShowKeystrokes, Mode=OneWay}">
<controls:ShortcutControl HotkeySettings="{x:Bind Path=ViewModel.KeystrokeSwitchMonitorHotkey, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard x:Uid="MouseUtils_InputHighlighter_SwitchDisplayModeHotkey" IsEnabled="{x:Bind ViewModel.ShowKeystrokes, Mode=OneWay}">
<controls:ShortcutControl HotkeySettings="{x:Bind Path=ViewModel.KeystrokeSwitchDisplayModeHotkey, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
</controls:SettingsGroup>
</StackPanel>
</controls:SettingsPageControl.ModuleContent>
</controls:SettingsPageControl>
</local:NavigablePage>

View File

@@ -0,0 +1,44 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.PowerToys.Settings.UI.Helpers;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.ViewModels;
namespace Microsoft.PowerToys.Settings.UI.Views
{
/// <summary>
/// Settings page for Input Highlighter (keystroke visualization). It currently
/// rides on the Mouse Highlighter module/settings, so it reuses
/// <see cref="MouseUtilsViewModel"/> and binds only the keystroke properties.
/// </summary>
public sealed partial class InputHighlighterPage : NavigablePage, IRefreshablePage
{
private MouseUtilsViewModel ViewModel { get; set; }
public InputHighlighterPage()
{
var settingsUtils = SettingsUtils.Default;
ViewModel = new MouseUtilsViewModel(
settingsUtils,
SettingsRepository<GeneralSettings>.GetInstance(settingsUtils),
SettingsRepository<FindMyMouseSettings>.GetInstance(settingsUtils),
SettingsRepository<MouseHighlighterSettings>.GetInstance(settingsUtils),
SettingsRepository<MouseJumpSettings>.GetInstance(settingsUtils),
SettingsRepository<MousePointerCrosshairsSettings>.GetInstance(settingsUtils),
SettingsRepository<CursorWrapSettings>.GetInstance(settingsUtils),
ShellPage.SendDefaultIPCMessage);
DataContext = ViewModel;
InitializeComponent();
Loaded += (s, e) => ViewModel.OnPageLoaded();
}
public void RefreshEnabledState()
{
ViewModel.RefreshEnabledState();
}
}
}

View File

@@ -239,131 +239,6 @@
</tkcontrols:SettingsExpander>
</controls:SettingsGroup>
<controls:SettingsGroup x:Uid="MouseUtils_MouseHighlighter" AutomationProperties.AutomationId="MouseUtils_MouseHighlighterTestId">
<controls:GPOInfoControl ShowWarning="{x:Bind ViewModel.IsHighlighterEnabledGpoConfigured, Mode=OneWay}">
<tkcontrols:SettingsCard
Name="MouseUtilsEnableMouseHighlighter"
x:Uid="MouseUtils_Enable_MouseHighlighter"
HeaderIcon="{ui:BitmapIcon Source=/Assets/Settings/Icons/MouseHighlighter.png}">
<ToggleSwitch
x:Uid="ToggleSwitch"
AutomationProperties.AutomationId="MouseUtils_MouseHighlighterToggleId"
IsOn="{x:Bind ViewModel.IsMouseHighlighterEnabled, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
</controls:GPOInfoControl>
<tkcontrols:SettingsExpander
Name="MouseUtilsMouseHighlighterActivationShortcut"
x:Uid="MouseUtils_MouseHighlighter_ActivationShortcut"
AutomationProperties.AutomationId="MouseUtils_MouseHighlighterActivationShortcutId"
HeaderIcon="{ui:FontIcon Glyph=&#xEDA7;}"
IsEnabled="{x:Bind ViewModel.IsMouseHighlighterEnabled, Mode=OneWay}">
<controls:ShortcutControl HotkeySettings="{x:Bind Path=ViewModel.MouseHighlighterActivationShortcut, Mode=TwoWay}" />
<tkcontrols:SettingsExpander.Items>
<tkcontrols:SettingsCard ContentAlignment="Left">
<CheckBox x:Uid="MouseUtils_AutoActivate" IsChecked="{x:Bind ViewModel.MouseHighlighterAutoActivate, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
</tkcontrols:SettingsExpander.Items>
</tkcontrols:SettingsExpander>
<tkcontrols:SettingsExpander
Name="MouseHighlighterAppearanceBehavior"
x:Uid="MouseUtils_MouseHighlighter_Appearance"
AutomationProperties.AutomationId="MouseUtils_MouseHighlighterAppearanceBehaviorId"
HeaderIcon="{ui:FontIcon Glyph=&#xE790;}"
IsEnabled="{x:Bind ViewModel.IsMouseHighlighterEnabled, Mode=OneWay}">
<ComboBox
x:Uid="MouseUtils_MouseHighlighter_SpotlightModeType"
MinWidth="{StaticResource SettingActionControlMinWidth}"
SelectedIndex="{x:Bind ViewModel.HighlightModeIndex, Mode=TwoWay}">
<ComboBoxItem x:Uid="HighlightMode_Spotlight_Mode" />
<ComboBoxItem x:Uid="HighlightMode_Circle_Highlight_Mode" />
<ComboBoxItem x:Uid="HighlightMode_Ripple_Mode" />
</ComboBox>
<tkcontrols:SettingsExpander.Items>
<tkcontrols:SettingsCard x:Uid="MouseUtils_MouseHighlighter_PrimaryButtonClickColor" Visibility="{x:Bind ViewModel.HighlightModeIndex, Mode=OneWay, Converter={StaticResource IndexBitFieldToVisibilityConverter}, ConverterParameter=0x6}">
<controls:ColorPickerButton IsAlphaEnabled="True" SelectedColor="{x:Bind Path=ViewModel.MouseHighlighterLeftButtonClickColor, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard x:Uid="MouseUtils_MouseHighlighter_SecondaryButtonClickColor" Visibility="{x:Bind ViewModel.HighlightModeIndex, Mode=OneWay, Converter={StaticResource IndexBitFieldToVisibilityConverter}, ConverterParameter=0x6}">
<controls:ColorPickerButton IsAlphaEnabled="True" SelectedColor="{x:Bind Path=ViewModel.MouseHighlighterRightButtonClickColor, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard
Name="MouseUtilsMouseHighlighterAlwaysColor"
x:Uid="MouseUtils_MouseHighlighter_AlwaysColor"
Visibility="{x:Bind ViewModel.HighlightModeIndex, Mode=OneWay, Converter={StaticResource IndexBitFieldToVisibilityConverter}, ConverterParameter=0x3}">
<controls:ColorPickerButton IsAlphaEnabled="True" SelectedColor="{x:Bind Path=ViewModel.MouseHighlighterAlwaysColor, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard x:Uid="MouseUtils_MouseHighlighter_HighlightRadius" Visibility="{x:Bind ViewModel.HighlightModeIndex, Mode=OneWay, Converter={StaticResource IndexBitFieldToVisibilityConverter}, ConverterParameter=0x3}">
<NumberBox
MinWidth="{StaticResource SettingActionControlMinWidth}"
LargeChange="10"
Minimum="5"
SmallChange="1"
SpinButtonPlacementMode="Compact"
Value="{x:Bind ViewModel.MouseHighlighterRadius, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard
Name="MouseUtilsMouseHighlighterFadeDelayMs"
x:Uid="MouseUtils_MouseHighlighter_FadeDelayMs"
Visibility="{x:Bind ViewModel.HighlightModeIndex, Mode=OneWay, Converter={StaticResource IndexBitFieldToVisibilityConverter}, ConverterParameter=0x3}">
<NumberBox
MinWidth="{StaticResource SettingActionControlMinWidth}"
LargeChange="100"
Minimum="0"
SmallChange="10"
SpinButtonPlacementMode="Compact"
Value="{x:Bind ViewModel.MouseHighlighterFadeDelayMs, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard
Name="MouseUtilsMouseHighlighterFadeDurationMs"
x:Uid="MouseUtils_MouseHighlighter_FadeDurationMs"
Visibility="{x:Bind ViewModel.HighlightModeIndex, Mode=OneWay, Converter={StaticResource IndexBitFieldToVisibilityConverter}, ConverterParameter=0x3}">
<NumberBox
MinWidth="{StaticResource SettingActionControlMinWidth}"
LargeChange="100"
Minimum="0"
SmallChange="10"
SpinButtonPlacementMode="Compact"
Value="{x:Bind ViewModel.MouseHighlighterFadeDurationMs, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard x:Uid="MouseUtils_MouseHighlighter_RippleSize" Visibility="{x:Bind ViewModel.HighlightModeIndex, Mode=OneWay, Converter={StaticResource IndexBitFieldToVisibilityConverter}, ConverterParameter=0x4}">
<NumberBox
MinWidth="{StaticResource SettingActionControlMinWidth}"
LargeChange="10"
Maximum="300"
Minimum="10"
SmallChange="1"
SpinButtonPlacementMode="Compact"
Value="{x:Bind ViewModel.RippleSize, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard x:Uid="MouseUtils_MouseHighlighter_RippleIntensity" Visibility="{x:Bind ViewModel.HighlightModeIndex, Mode=OneWay, Converter={StaticResource IndexBitFieldToVisibilityConverter}, ConverterParameter=0x4}">
<Slider
MinWidth="{StaticResource SettingActionControlMinWidth}"
LargeChange="0.1"
Maximum="1.35"
Minimum="0.15"
SmallChange="0.05"
StepFrequency="0.05"
Value="{x:Bind ViewModel.RippleIntensity, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard x:Uid="MouseUtils_MouseHighlighter_RippleDurationMs" Visibility="{x:Bind ViewModel.HighlightModeIndex, Mode=OneWay, Converter={StaticResource IndexBitFieldToVisibilityConverter}, ConverterParameter=0x4}">
<NumberBox
MinWidth="{StaticResource SettingActionControlMinWidth}"
LargeChange="100"
Maximum="2000"
Minimum="60"
SmallChange="10"
SpinButtonPlacementMode="Compact"
Value="{x:Bind ViewModel.RippleDurationMs, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard ContentAlignment="Left" Visibility="{x:Bind ViewModel.HighlightModeIndex, Mode=OneWay, Converter={StaticResource IndexBitFieldToVisibilityConverter}, ConverterParameter=0x4}">
<ptcontrols:CheckBoxWithDescriptionControl x:Uid="MouseUtils_MouseHighlighter_RippleShowDragTrail" IsChecked="{x:Bind ViewModel.RippleShowDragTrail, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard ContentAlignment="Left" Visibility="{x:Bind ViewModel.HighlightModeIndex, Mode=OneWay, Converter={StaticResource IndexBitFieldToVisibilityConverter}, ConverterParameter=0x4}">
<ptcontrols:CheckBoxWithDescriptionControl x:Uid="MouseUtils_MouseHighlighter_RippleShowReleasePulse" IsChecked="{x:Bind ViewModel.RippleShowReleasePulse, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
</tkcontrols:SettingsExpander.Items>
</tkcontrols:SettingsExpander>
</controls:SettingsGroup>
<panels:MouseJumpPanel x:Name="MouseUtils_MouseJump_Panel" x:Uid="MouseUtils_MouseJump_Panel" />
<controls:SettingsGroup x:Uid="MouseUtils_MousePointerCrosshairs" AutomationProperties.AutomationId="MouseUtils_MousePointerCrosshairsTestId">

View File

@@ -317,6 +317,12 @@
helpers:NavHelper.NavigateTo="views:MouseUtilsPage"
AutomationProperties.AutomationId="MouseUtilitiesNavItem"
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/MouseUtils.png}" />
<NavigationViewItem
x:Name="InputHighlighterNavigationItem"
x:Uid="Shell_InputHighlighter"
helpers:NavHelper.NavigateTo="views:InputHighlighterPage"
AutomationProperties.AutomationId="InputHighlighterNavItem"
Icon="{ui:FontIcon Glyph=&#xE765;}" />
<NavigationViewItem
x:Name="MouseWithoutBordersNavigationItem"
x:Uid="Shell_MouseWithoutBorders"

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
@@ -542,6 +542,10 @@ opera.exe</value>
<value>Mouse Utilities</value>
<comment>Product name: Navigation view item name for Mouse utilities</comment>
</data>
<data name="Shell_InputHighlighter.Content" xml:space="preserve">
<value>Input Highlighter</value>
<comment>Product name: Navigation view item name for Input Highlighter</comment>
</data>
<data name="Shell_NavigationMenu_Announce_Collapse" xml:space="preserve">
<value>Navigation closed</value>
<comment>Accessibility announcement when the navigation pane collapses</comment>
@@ -2785,15 +2789,15 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
<value>Determines how far the pointer must move, relative to the screen diagonal, to count as a shake. Lower values make it more sensitive.</value>
</data>
<data name="MouseUtils_MouseHighlighter.Header" xml:space="preserve">
<value>Mouse Highlighter</value>
<value>Input Highlighter</value>
<comment>Refers to the utility name</comment>
</data>
<data name="MouseUtils_MouseHighlighter.Description" xml:space="preserve">
<value>Highlight mouse clicks.</value>
<value>Highlight mouse clicks and visualize keystrokes on screen.</value>
<comment>"Mouse Highlighter" is the name of the utility. Mouse is the hardware mouse.</comment>
</data>
<data name="MouseUtils_Enable_MouseHighlighter.Header" xml:space="preserve">
<value>Mouse Highlighter</value>
<value>Input Highlighter</value>
<comment>"Find My Mouse" is the name of the utility.</comment>
</data>
<data name="MouseUtils_MouseHighlighter_ActivationShortcut.Header" xml:space="preserve">
@@ -5541,6 +5545,36 @@ The break timer font matches the text font.</value>
<value>Power Display</value>
<comment>{Locked="Power Display"}</comment>
</data>
<data name="InputHighlighter.ModuleTitle" xml:space="preserve">
<value>Input Highlighter</value>
<comment>{Locked="Input Highlighter"}</comment>
</data>
<data name="InputHighlighter.ModuleDescription" xml:space="preserve">
<value>Show keys and shortcuts on screen as you type</value>
</data>
<data name="InputHighlighter_ModuleDisabledInfoBar.Title" xml:space="preserve">
<value>Mouse Highlighter is turned off</value>
<comment>{Locked="Mouse Highlighter"}</comment>
</data>
<data name="InputHighlighter_ModuleDisabledInfoBar.Message" xml:space="preserve">
<value>Input Highlighter runs as part of the Mouse Highlighter module. Turn on Mouse Highlighter to use it.</value>
<comment>{Locked="Input Highlighter","Mouse Highlighter"}</comment>
</data>
<data name="InputHighlighter_Activation.Header" xml:space="preserve">
<value>Activation</value>
</data>
<data name="InputHighlighter_Enable.Header" xml:space="preserve">
<value>Show keystrokes</value>
</data>
<data name="InputHighlighter_Enable.Description" xml:space="preserve">
<value>Display the keys and shortcuts you press as an on-screen overlay</value>
</data>
<data name="InputHighlighter_Appearance.Header" xml:space="preserve">
<value>Appearance</value>
</data>
<data name="InputHighlighter_Shortcuts.Header" xml:space="preserve">
<value>Overlay shortcuts</value>
</data>
<data name="PowerDisplay.ModuleDescription" xml:space="preserve">
<value>A display management utility for brightness and power control</value>
</data>
@@ -6250,4 +6284,88 @@ Text uses the current drawing color.</value>
<value>Example: outlook</value>
<comment>{Locked="outlook"}</comment>
</data>
<data name="MouseUtils_InputHighlighter_ShowMouse.Header" xml:space="preserve">
<value>Highlight mouse clicks</value>
</data>
<data name="MouseUtils_InputHighlighter_ShowMouse.Description" xml:space="preserve">
<value>Show visual highlights when mouse buttons are clicked</value>
</data>
<data name="MouseUtils_InputHighlighter_Keystrokes.Header" xml:space="preserve">
<value>Show keystrokes</value>
</data>
<data name="MouseUtils_InputHighlighter_Keystrokes.Description" xml:space="preserve">
<value>Display the keys you press as an on-screen overlay</value>
</data>
<data name="MouseUtils_InputHighlighter_DisplayMode.Header" xml:space="preserve">
<value>Display mode</value>
</data>
<data name="MouseUtils_InputHighlighter_DisplayMode_Last5.Content" xml:space="preserve">
<value>Recent keys</value>
</data>
<data name="MouseUtils_InputHighlighter_DisplayMode_SingleChars.Content" xml:space="preserve">
<value>Single key</value>
</data>
<data name="MouseUtils_InputHighlighter_DisplayMode_Shortcuts.Content" xml:space="preserve">
<value>Shortcuts</value>
</data>
<data name="MouseUtils_InputHighlighter_DisplayMode_Stream.Content" xml:space="preserve">
<value>Text stream</value>
</data>
<data name="MouseUtils_InputHighlighter_Position.Header" xml:space="preserve">
<value>Position</value>
</data>
<data name="MouseUtils_InputHighlighter_Position_TopLeft.Content" xml:space="preserve">
<value>Top left</value>
</data>
<data name="MouseUtils_InputHighlighter_Position_TopCenter.Content" xml:space="preserve">
<value>Top center</value>
</data>
<data name="MouseUtils_InputHighlighter_Position_TopRight.Content" xml:space="preserve">
<value>Top right</value>
</data>
<data name="MouseUtils_InputHighlighter_Position_BottomLeft.Content" xml:space="preserve">
<value>Bottom left</value>
</data>
<data name="MouseUtils_InputHighlighter_Position_BottomCenter.Content" xml:space="preserve">
<value>Bottom center</value>
</data>
<data name="MouseUtils_InputHighlighter_Position_BottomRight.Content" xml:space="preserve">
<value>Bottom right</value>
</data>
<data name="MouseUtils_InputHighlighter_TextColor.Header" xml:space="preserve">
<value>Text color</value>
</data>
<data name="MouseUtils_InputHighlighter_BackgroundColor.Header" xml:space="preserve">
<value>Background color</value>
</data>
<data name="MouseUtils_InputHighlighter_StrokeColor.Header" xml:space="preserve">
<value>Border color</value>
</data>
<data name="MouseUtils_InputHighlighter_StrokeThickness.Header" xml:space="preserve">
<value>Border thickness</value>
</data>
<data name="MouseUtils_InputHighlighter_StrokeThickness.Description" xml:space="preserve">
<value>Set to 0 to hide the border</value>
</data>
<data name="MouseUtils_InputHighlighter_TextSize.Header" xml:space="preserve">
<value>Font size</value>
</data>
<data name="MouseUtils_InputHighlighter_Timeout.Header" xml:space="preserve">
<value>Duration (ms)</value>
</data>
<data name="MouseUtils_InputHighlighter_Timeout.Description" xml:space="preserve">
<value>How long each keystroke stays on screen</value>
</data>
<data name="MouseUtils_InputHighlighter_Draggable.Header" xml:space="preserve">
<value>Allow dragging the overlay</value>
</data>
<data name="MouseUtils_InputHighlighter_Draggable.Description" xml:space="preserve">
<value>Let you reposition the keystroke overlay by dragging it</value>
</data>
<data name="MouseUtils_InputHighlighter_SwitchMonitorHotkey.Header" xml:space="preserve">
<value>Move to next monitor shortcut</value>
</data>
<data name="MouseUtils_InputHighlighter_SwitchDisplayModeHotkey.Header" xml:space="preserve">
<value>Cycle display mode shortcut</value>
</data>
</root>

View File

@@ -89,6 +89,22 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
_highlightFadeDurationMs = MouseHighlighterSettingsConfig.Properties.HighlightFadeDurationMs.Value;
_highlighterAutoActivate = MouseHighlighterSettingsConfig.Properties.AutoActivate.Value;
// Input Highlighter keystroke-overlay settings.
_showMouse = MouseHighlighterSettingsConfig.Properties.ShowMouse.Value;
_showKeystrokes = MouseHighlighterSettingsConfig.Properties.ShowKeystrokes.Value;
_keystrokeDisplayMode = MouseHighlighterSettingsConfig.Properties.KeystrokeDisplayMode.Value;
_keystrokePosition = MouseHighlighterSettingsConfig.Properties.KeystrokePosition.Value;
_keystrokeTimeoutMs = MouseHighlighterSettingsConfig.Properties.KeystrokeTimeoutMs.Value;
_keystrokeTextSize = MouseHighlighterSettingsConfig.Properties.KeystrokeTextSize.Value;
string keystrokeTextColor = MouseHighlighterSettingsConfig.Properties.KeystrokeTextColor.Value;
_keystrokeTextColor = !string.IsNullOrEmpty(keystrokeTextColor) ? keystrokeTextColor : "#FFFFFFFF";
string keystrokeBackgroundColor = MouseHighlighterSettingsConfig.Properties.KeystrokeBackgroundColor.Value;
_keystrokeBackgroundColor = !string.IsNullOrEmpty(keystrokeBackgroundColor) ? keystrokeBackgroundColor : "#80000000";
string keystrokeStrokeColor = MouseHighlighterSettingsConfig.Properties.KeystrokeStrokeColor.Value;
_keystrokeStrokeColor = !string.IsNullOrEmpty(keystrokeStrokeColor) ? keystrokeStrokeColor : "#00FFFFFF";
_keystrokeStrokeThickness = MouseHighlighterSettingsConfig.Properties.KeystrokeStrokeThickness.Value;
_keystrokeDraggable = MouseHighlighterSettingsConfig.Properties.KeystrokeDraggable.Value;
this.InitializeMouseJumpSettings(mouseJumpSettingsRepository);
ArgumentNullException.ThrowIfNull(mousePointerCrosshairsSettingsRepository);
@@ -814,6 +830,189 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
}
}
public bool ShowMouse
{
get => _showMouse;
set
{
if (value != _showMouse)
{
_showMouse = value;
MouseHighlighterSettingsConfig.Properties.ShowMouse.Value = value;
NotifyMouseHighlighterPropertyChanged();
}
}
}
public bool ShowKeystrokes
{
get => _showKeystrokes;
set
{
if (value != _showKeystrokes)
{
_showKeystrokes = value;
MouseHighlighterSettingsConfig.Properties.ShowKeystrokes.Value = value;
NotifyMouseHighlighterPropertyChanged();
}
}
}
public HotkeySettings KeystrokeSwitchMonitorHotkey
{
get => MouseHighlighterSettingsConfig.Properties.KeystrokeSwitchMonitorHotkey;
set
{
if (MouseHighlighterSettingsConfig.Properties.KeystrokeSwitchMonitorHotkey != value)
{
MouseHighlighterSettingsConfig.Properties.KeystrokeSwitchMonitorHotkey = value ?? MouseHighlighterSettingsConfig.Properties.DefaultKeystrokeSwitchMonitorHotkey;
NotifyMouseHighlighterPropertyChanged();
}
}
}
public HotkeySettings KeystrokeSwitchDisplayModeHotkey
{
get => MouseHighlighterSettingsConfig.Properties.KeystrokeSwitchDisplayModeHotkey;
set
{
if (MouseHighlighterSettingsConfig.Properties.KeystrokeSwitchDisplayModeHotkey != value)
{
MouseHighlighterSettingsConfig.Properties.KeystrokeSwitchDisplayModeHotkey = value ?? MouseHighlighterSettingsConfig.Properties.DefaultKeystrokeSwitchDisplayModeHotkey;
NotifyMouseHighlighterPropertyChanged();
}
}
}
public int KeystrokeDisplayMode
{
get => _keystrokeDisplayMode;
set
{
if (value != _keystrokeDisplayMode)
{
_keystrokeDisplayMode = value;
MouseHighlighterSettingsConfig.Properties.KeystrokeDisplayMode.Value = value;
NotifyMouseHighlighterPropertyChanged();
}
}
}
public int KeystrokePosition
{
get => _keystrokePosition;
set
{
if (value != _keystrokePosition)
{
_keystrokePosition = value;
MouseHighlighterSettingsConfig.Properties.KeystrokePosition.Value = value;
NotifyMouseHighlighterPropertyChanged();
}
}
}
public int KeystrokeTimeoutMs
{
get => _keystrokeTimeoutMs;
set
{
if (value != _keystrokeTimeoutMs)
{
_keystrokeTimeoutMs = value;
MouseHighlighterSettingsConfig.Properties.KeystrokeTimeoutMs.Value = value;
NotifyMouseHighlighterPropertyChanged();
}
}
}
public int KeystrokeTextSize
{
get => _keystrokeTextSize;
set
{
if (value != _keystrokeTextSize)
{
_keystrokeTextSize = value;
MouseHighlighterSettingsConfig.Properties.KeystrokeTextSize.Value = value;
NotifyMouseHighlighterPropertyChanged();
}
}
}
public string KeystrokeTextColor
{
get => _keystrokeTextColor;
set
{
value = SettingsUtilities.ToARGBHex(value);
if (!value.Equals(_keystrokeTextColor, StringComparison.OrdinalIgnoreCase))
{
_keystrokeTextColor = value;
MouseHighlighterSettingsConfig.Properties.KeystrokeTextColor.Value = value;
NotifyMouseHighlighterPropertyChanged();
}
}
}
public string KeystrokeBackgroundColor
{
get => _keystrokeBackgroundColor;
set
{
value = SettingsUtilities.ToARGBHex(value);
if (!value.Equals(_keystrokeBackgroundColor, StringComparison.OrdinalIgnoreCase))
{
_keystrokeBackgroundColor = value;
MouseHighlighterSettingsConfig.Properties.KeystrokeBackgroundColor.Value = value;
NotifyMouseHighlighterPropertyChanged();
}
}
}
public string KeystrokeStrokeColor
{
get => _keystrokeStrokeColor;
set
{
value = SettingsUtilities.ToARGBHex(value);
if (!value.Equals(_keystrokeStrokeColor, StringComparison.OrdinalIgnoreCase))
{
_keystrokeStrokeColor = value;
MouseHighlighterSettingsConfig.Properties.KeystrokeStrokeColor.Value = value;
NotifyMouseHighlighterPropertyChanged();
}
}
}
public int KeystrokeStrokeThickness
{
get => _keystrokeStrokeThickness;
set
{
if (value != _keystrokeStrokeThickness)
{
_keystrokeStrokeThickness = value;
MouseHighlighterSettingsConfig.Properties.KeystrokeStrokeThickness.Value = value;
NotifyMouseHighlighterPropertyChanged();
}
}
}
public bool KeystrokeDraggable
{
get => _keystrokeDraggable;
set
{
if (value != _keystrokeDraggable)
{
_keystrokeDraggable = value;
MouseHighlighterSettingsConfig.Properties.KeystrokeDraggable.Value = value;
NotifyMouseHighlighterPropertyChanged();
}
}
}
public void NotifyMouseHighlighterPropertyChanged([CallerMemberName] string propertyName = null)
{
OnPropertyChanged(propertyName);
@@ -1359,6 +1558,19 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
private int _highlightFadeDurationMs;
private bool _highlighterAutoActivate;
// Input Highlighter keystroke-overlay backing fields.
private bool _showMouse;
private bool _showKeystrokes;
private int _keystrokeDisplayMode;
private int _keystrokePosition;
private int _keystrokeTimeoutMs;
private int _keystrokeTextSize;
private string _keystrokeTextColor;
private string _keystrokeBackgroundColor;
private string _keystrokeStrokeColor;
private int _keystrokeStrokeThickness;
private bool _keystrokeDraggable;
private GpoRuleConfigured _mousePointerCrosshairsEnabledGpoRuleConfiguration;
private bool _mousePointerCrosshairsEnabledStateGPOConfigured;
private bool _isMousePointerCrosshairsEnabled;