mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-07-07 11:00:32 +02:00
Compare commits
1 Commits
main
...
niels9001/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c291abc32c |
113
doc/devdocs/modules/inputhighlighter.md
Normal file
113
doc/devdocs/modules/inputhighlighter.md
Normal 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.
|
||||
@@ -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'));
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,2 @@
|
||||
// pch.cpp: source file corresponding to the pre-compiled header.
|
||||
#include "pch.h"
|
||||
17
src/modules/MouseUtils/MouseHighlighter.UnitTests/pch.h
Normal file
17
src/modules/MouseUtils/MouseHighlighter.UnitTests/pch.h
Normal 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
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 };
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -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 };
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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=}"
|
||||
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=}"
|
||||
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=}"
|
||||
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>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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=}"
|
||||
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=}"
|
||||
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">
|
||||
|
||||
@@ -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=}" />
|
||||
<NavigationViewItem
|
||||
x:Name="MouseWithoutBordersNavigationItem"
|
||||
x:Uid="Shell_MouseWithoutBorders"
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user