KeystrokeOverlay: New PowerToys module for on screen key strokes (#44250)

<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
This pull request introduces the new KeystrokeOverlay module to
PowerToys, including its documentation, build configuration, installer
setup, and spell-check dictionary updates. KeystrokeOverlay can be used
to show keys pressed on the screen like Visual Studio Code's Screencast
Mode.

It provides customization options for text size and background, offers
four types of display modes, and three shortcuts for toggling on/off the
display, cycling through monitors, and through different display modes
(see below for more details).

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] Closes: #981 
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [x] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [x] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments
**KeystrokeOverlay Module Integration:**

* Added KeystrokeOverlay as a new module in the solution, with
corresponding projects for the keyboard service, UI, and module
interface (`PowerToys.slnx`).
* Added documentation for the Keystroke Overlay module, including
architecture, usage, and debugging instructions
(`doc/devdocs/modules/keystrokeoverlay.md`).
* Updated installer configuration to include Keystroke Overlay, adding
new WiX include and setup files (`installer/PowerToysSetup/Common.wxi`,
`installer/PowerToysSetup/KeystrokeOverlay.wxs`).
[[1]](diffhunk://#diff-ecd2ee19d18433ed47b8f13b44cfff7b00c8009c17bc71139cec0d8571f8f607R1-R59)
[[2]](diffhunk://#diff-af48b984b168acbe5aeb021e97a9e826ab9b52c20c08b36f40dd3e5ad00342b1R1-R9)
* Registered Keystroke Overlay in the settings documentation and issue
templates (`doc/dsc/Settings.md`,
`.github/ISSUE_TEMPLATE/bug_report.yml`,
`.github/ISSUE_TEMPLATE/translation_issue.yml`).
[[1]](diffhunk://#diff-bde46f469b76ba994ea938853f169553846221b329a21e9b59d37c5a0b7d63aeR35)
[[2]](diffhunk://#diff-637f7b97bba458badb691a1557c3d4648686292e948dbe3e8360564378b653efR85)
[[3]](diffhunk://#diff-135b470e9875068a1085599402d6f89bea163068568c426b22f493f35fbfbea6R58)
* Added KeystrokeOverlay binaries to the signing pipeline
(`.pipelines/ESRPSigning_core.json`).

**Customization options**
| Category | Setting | Description | Default / Example |
| :--- | :--- | :--- | :--- |
| **General** | **Enable Keystroke Overlay** | Master toggle to turn the
module on or off. | `On` |
| **Shortcuts** | **Activation Shortcut** | Hotkey to toggle the overlay
visibility. | `Win` + `Shift` + `K` |
| | **Switch Monitor Shortcut** | Hotkey to move the overlay to another
display. | `Win` + `Shift` + `/` |
| | **Switch Display Mode** | Hotkey to cycle through different
visualization modes. | `Win` + `Shift` + `D` |
| **Behavior** | **Draggable Overlay** | Allows you to manually move the
overlay position with your mouse. | `On` |
| | **Display Mode** | Controls what history of keys is shown (e.g.,
history vs. single). | `Last Five Keystrokes` |
| | **Overlay Timeout** | How long (in ms) the keys stay visible before
fading. | `2000` ms |
| **Appearance** | **Text Size** | Adjusts the font size of the keys
inside the overlay. | `37` |
| | **Text Color** | Sets the font color and transparency level. |
`White` (Example) |
| | **Background Color** | Sets the background box color and
transparency level. | `Purple` (Example) |

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
The new module was tested on multiple different Windows Devices and all
configurations work as expected.

---------

Signed-off-by: Shawn Yuan (from Dev Box) <shuaiyuan@microsoft.com>
Co-authored-by: Sátvik Karanam <89281036+skara9@users.noreply.github.com>
Co-authored-by: Jiří Polášek <me@jiripolasek.com>
Co-authored-by: Software2 <software-2@users.noreply.github.com>
Co-authored-by: Niels Laute <niels.laute@live.nl>
Co-authored-by: Mike Griese <migrie@microsoft.com>
Co-authored-by: Jaylyn Barbee <51131738+Jaylyn-Barbee@users.noreply.github.com>
Co-authored-by: Gordon Lam (SH) <yeelam@microsoft.com>
Co-authored-by: Kai Tao <69313318+vanzue@users.noreply.github.com>
Co-authored-by: Mark Russinovich <markruss@microsoft.com>
Co-authored-by: Mark Russinovich <markruss@ntdev.microsoft.com>
Co-authored-by: Clint Rutkas <clint@rutkas.com>
Co-authored-by: Alex Mihaiuc <69110671+foxmsft@users.noreply.github.com>
Co-authored-by: Michael Jolley <mike@baldbeardedbuilder.com>
Co-authored-by: Gordon Lam <73506701+yeelam-gordon@users.noreply.github.com>
Co-authored-by: leileizhang <leilzh@microsoft.com>
Co-authored-by: Dave Rayment <dave.rayment@gmail.com>
Co-authored-by: Shawn Yuan <128874481+shuaiyuanxx@users.noreply.github.com>
Co-authored-by: Kai Tao <kaitao@microsoft.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: jiripolasek <4773077+jiripolasek@users.noreply.github.com>
Co-authored-by: Dustin L. Howett <duhowett@microsoft.com>
Co-authored-by: Sam Rueby <samrueby@gmail.com>
Co-authored-by: Lee Won Jun <dldnjs1013@nate.com>
Co-authored-by: Mason Bergstrom <13530957+MasonBergstrom@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Mike Hall <mikehall@microsoft.com>
Co-authored-by: moooyo <42196638+moooyo@users.noreply.github.com>
Co-authored-by: Yu Leng <yuleng@microsoft.com>
Co-authored-by: dsm20 <74568547+dsm20@users.noreply.github.com>
Co-authored-by: zackpaton <91792781+zackpaton@users.noreply.github.com>
Co-authored-by: Gleb Khmyznikov <gleb.khmyznikov@gmail.com>
Co-authored-by: Guilherme <57814418+DevLGuilherme@users.noreply.github.com>
This commit is contained in:
Miranda Zheng
2025-12-13 02:25:20 -05:00
committed by GitHub
parent 97c1de8bf6
commit 459dd2fa37
190 changed files with 9387 additions and 615 deletions

View File

@@ -82,6 +82,7 @@ body:
- Workspaces
- Welcome / PowerToys Tour window
- ZoomIt
- Keystroke Overlay
validations:
required: true

View File

@@ -55,6 +55,7 @@ body:
- Workspaces
- Welcome / PowerToys Tour window
- ZoomIt
- Keystroke Overlay
validations:
required: true
- type: input

View File

@@ -335,3 +335,26 @@ azp
feedbackhub
needinfo
reportbug
#ffmpeg
crf
nostdin
# KeystrokeOverlay words
dwmwa
dwmwcp
embeddedhtml
getkeyboard
installscopeperuser
JOBOBJECT
JOBOBJECTLIMIT
keystate
lgdi
Pipeserver
registryroot
regroot
spsc
swp
Timestamping
xstring
ello

View File

@@ -137,3 +137,4 @@ ignore$
^src/modules/registrypreview/RegistryPreviewUILib/Controls/HexBox/.*$
^src/common/CalculatorEngineCommon/exprtk\.hpp$
src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleMarkdownImagesPage.cs
^src/modules/KeystrokeOverlay/KeystrokeOverlayModuleInterface/resource\.h$

View File

@@ -144,6 +144,8 @@ BLENDFUNCTION
blittable
Blockquotes
blt
bluelightreduction
bluelightreductionstate
BLURBEHIND
BLURREGION
bmi
@@ -463,6 +465,7 @@ EDITKEYBOARD
EDITSHORTCUTS
EDITTEXT
EFile
ekus
eku
emojis
ENABLEDELAYEDEXPANSION
@@ -845,6 +848,7 @@ KEYIMAGE
keynum
keyremaps
keyring
keystrokeoverlay
keyvault
KILLFOCUS
killrunner
@@ -1115,6 +1119,7 @@ NEWPLUSSHELLEXTENSIONWIN
newrow
nicksnettravels
NIF
nightlight
NLog
NLSTEXT
NMAKE
@@ -1189,6 +1194,7 @@ ntfs
NTSTATUS
NTSYSAPI
NULLCURSOR
nullref
nullonfailure
nullref
numberbox
@@ -1851,6 +1857,8 @@ uitests
UITo
ULONGLONG
ums
UMax
UMin
uncompilable
UNCPRIORITY
UNDNAME

View File

@@ -230,6 +230,9 @@
"PowerToys.ZoomItModuleInterface.dll",
"PowerToys.ZoomItSettingsInterop.dll",
"WinUI3Apps\\PowerToys.KeystrokeOverlay.exe",
"WinUI3Apps\\PowerToys.KeystrokeOverlayModuleInterface.dll",
"WinUI3Apps\\PowerToys.Settings.dll",
"WinUI3Apps\\PowerToys.Settings.exe",

View File

@@ -27,7 +27,8 @@ $versionExceptions = @(
"WyHash.dll",
"Microsoft.Recognizers.Text.DataTypes.TimexExpression.dll",
"ObjectModelCsProjection.dll",
"RendererCsProjection.dll") -join '|';
"RendererCsProjection.dll",
"Microsoft.ML.OnnxRuntime.dll") -join '|';
$nullVersionExceptions = @(
"SkiaSharp.Views.WinUI.Native.dll",
"libSkiaSharp.dll",

6
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,6 @@
{
"files.associations": {
"mutex": "cpp",
"vector": "cpp"
}
}

View File

@@ -42,11 +42,6 @@
</PropertyGroup>
<ItemDefinitionGroup>
<ClCompile>
<!-- Make angle-bracket includes external and turn off code analysis for them -->
<TreatAngleIncludeAsExternal>true</TreatAngleIncludeAsExternal>
<ExternalWarningLevel>TurnOffAllWarnings</ExternalWarningLevel>
<DisableAnalyzeExternal>true</DisableAnalyzeExternal>
<MultiProcessorCompilation>true</MultiProcessorCompilation>
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Use</PrecompiledHeader>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
@@ -116,11 +111,13 @@
</PropertyGroup>
<!-- Debug/Release props -->
<PropertyGroup Condition="'$(Configuration)'=='Debug'" Label="Configuration">
<PropertyGroup Condition="'$(Configuration)'=='Debug'"
Label="Configuration">
<UseDebugLibraries>true</UseDebugLibraries>
<LinkIncremental>true</LinkIncremental>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release'" Label="Configuration">
<PropertyGroup Condition="'$(Configuration)'=='Release'"
Label="Configuration">
<UseDebugLibraries>false</UseDebugLibraries>
<WholeProgramOptimization>true</WholeProgramOptimization>
<LinkIncremental>false</LinkIncremental>

View File

@@ -1109,6 +1109,9 @@ _If you want to find diagnostic data events in the source code, these two links
</tr>
</table>
### Keystroke Overlay
<!-- TODO: Fill out table -->
<!-- back up of table
<table style="width:100%">

View File

@@ -7,8 +7,6 @@
<PackageVersion Include="AdaptiveCards.ObjectModel.WinUI3" Version="2.0.0-beta" />
<PackageVersion Include="AdaptiveCards.Rendering.WinUI3" Version="2.1.0-beta" />
<PackageVersion Include="AdaptiveCards.Templating" Version="2.0.5" />
<PackageVersion Include="boost" Version="1.87.0" TargetFramework="native" />
<PackageVersion Include="boost_regex-vc143" Version="1.87.0" TargetFramework="native" />
<PackageVersion Include="CommunityToolkit.Labs.WinUI.Controls.OpacityMaskView" Version="0.1.251101-build.2372" />
<PackageVersion Include="Microsoft.Bot.AdaptiveExpressions.Core" Version="4.23.0" />
<PackageVersion Include="Appium.WebDriver" Version="4.4.5" />
@@ -72,12 +70,10 @@
This is present due to a bug in CsWinRT where WPF projects cause the analyzer to fail.
-->
<PackageVersion Include="Microsoft.Windows.CsWinRT" Version="2.2.0" />
<PackageVersion Include="Microsoft.Windows.ImplementationLibrary" Version="1.0.231216.1"/>
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.6901" />
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.8.251106002" />
<PackageVersion Include="Microsoft.WindowsAppSDK.Foundation" Version="1.8.251104000" />
<PackageVersion Include="Microsoft.WindowsAppSDK.AI" Version="1.8.39" />
<PackageVersion Include="Microsoft.WindowsAppSDK.Runtime" Version="1.8.251106002" />
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.8.250907003" />
<PackageVersion Include="Microsoft.WindowsAppSDK.AI" Version="1.8.37" />
<PackageVersion Include="Microsoft.WindowsAppSDK.Runtime" Version="1.8.250907003" />
<PackageVersion Include="Microsoft.Xaml.Behaviors.WinUI.Managed" Version="2.0.9" />
<PackageVersion Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.39" />
<PackageVersion Include="ModernWpfUI" Version="0.9.4" />
@@ -116,7 +112,6 @@
<PackageVersion Include="System.IO.Abstractions.TestingHelpers" Version="22.0.13" />
<PackageVersion Include="System.Management" Version="9.0.10" />
<PackageVersion Include="System.Net.Http" Version="4.3.4" />
<PackageVersion Include="System.Numerics.Tensors" Version="9.0.11" />
<PackageVersion Include="System.Private.Uri" Version="4.3.2" />
<PackageVersion Include="System.Reactive" Version="6.0.1" />
<PackageVersion Include="System.Runtime.Caching" Version="9.0.10" />

View File

@@ -469,6 +469,14 @@
<Project Path="src/modules/keyboardmanager/KeyboardManagerEditorTest/KeyboardManagerEditorTest.vcxproj" Id="62173d9a-6724-4c00-a1c8-fb646480a9ec" />
<Project Path="src/modules/keyboardmanager/KeyboardManagerEngineTest/KeyboardManagerEngineTest.vcxproj" Id="7f4b3a60-bc27-45a7-8000-68b0b6ea7466" />
</Folder>
<Folder Name="/modules/KeystrokeOverlay/">
<Project Path="src/modules/KeystrokeOverlay/KeystrokeOverlayKeyboardService/KeystrokeOverlayKeyboardService.vcxproj" Id="a1b2c3d4-e5f6-4321-8765-123456789abc" />
<Project Path="src/modules/KeystrokeOverlay/KeystrokeOverlayModuleInterface/KeystrokeOverlay.vcxproj" Id="d66daaf0-84fc-477d-a980-43370f4499c2" />
<Project Path="src/modules/KeystrokeOverlay/KeystrokeOverlayXAML/KeystrokeOverlayUI.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
</Folder>
<Folder Name="/modules/launcher/">
<Project Path="src/modules/launcher/Microsoft.Launcher/Microsoft.Launcher.vcxproj" Id="e364f67b-bb12-4e91-b639-355866ebcd8b">
<BuildDependency Project="src/modules/launcher/PowerLauncher/PowerLauncher.csproj" />

View File

@@ -0,0 +1,104 @@
# Keystroke Overlay - Will live in doc/devdocs/modules
[Public overview - Microsoft Learn](#)
## Quick Links
[All Issues](#)<br>
[Bugs](#)<br>
[Pull Requests](#)
## Overview
Keystroke Overlay is a PowerToys module that displays keyboard input live. There are three different display modes. Single-key display shows the last key pressed, e.g. "o." Last five keys display shows the last five keys pressed, e.g. "h e l l o." Shortcut display supports shortcuts, e.g. "ctrl + v." This module is meant to be used as a helpful tool for educators, presenters, and/or visually impaired users.
## Architecture
The Keystroke Overlay module consists of three main components:
```
keystrokeoverlay/
├── KeystrokeOverlayKeyboardService/ # Keyboard Hook
├── KeystrokeOverlayXAML/ # The overlaying display UI
└── KeystrokeOverlayModuleInterface/ # DLL Interface
```
### Keyboard Service (KeystrokeOverlayKeyboardService)
The Keyboard Service component is responsible for:
- Compiles KeystrokeEvent.h, EventQueue.h, Batcher.cpp, and KeyboardListener.cpp into PowerToys.KeystrokeOverlayKeystrokeServer.exe
- Implements keyboard hooks to detect key presses
- Manages the trigger mechanism for displaying the keystroke overlay
- Handles keyboard input processing
### UI Layer (KeystrokeOverlayXAML)
The UI component is responsible for:
- Displaying the overlay of pressed keys
- Managing the visual positioning of the overlay
### Module Interface (KeystrokeOverlayModuleInterface)
The Module Interface, implemented in `KeystrokeOverlayModuleInterface/dllmain.cpp`, is responsible for:
- Handling communication between PowerToys Runner and the KeystrokeOverlay process
- Managing module lifecycle (enable/disable/settings)
- Launching and terminating the PowerToys.KeystrokeOverlay.exe process (as well as PowerToys.KeystrokeOverlayKeystrokeServer.exe as a child process)
## Implementation Details
### Activation Mechanism
The Keystroke Overlay is activated when:
1. A user presses or holds any key
2. After a brief delay (around 300ms per setting), the overlay appears
3. Upon releasing the key(s), the overlay hovers for around 300ms or a value specified in settings, then disappears
### Character Sets
The module supports multiple language-specific characters. Since the module uses keyboard codes to detect key presses and trigger the display, various keyboards and languages are supported by Keystroke Overlay, as long as they are supported by Windows.
### Known Behaviors
- If a key is pressed and held, this is processed as several presses of the same key in rapid succession
- Stream mode detects capital letters as shortcuts because the shift key is pressed and separates them from words; e.g. “Hello” in stream mode appears as “H ello”
### Future Considerations
- Add support for different appearances, other than left-to-right. Right-to-left or center-outwards would be beneficial, especially for users who communicate in languages that read right-to-left
- Add support for default positioning, e.g. top-left, bottom-center, middle-right, …
## Debugging
To debug the Keystroke Overlay module via **runner** approach, follow these steps:
0. Get familiar with the overall [Debugging Process](../development/debugging.md) for PowerToys.
1. **Build** the entire PowerToys solution in Visual Studio
2. Navigate to the **KeystrokeOverlay** folder in Solution Explorer
3. Open the file you want to debug and set **breakpoints** at the relevant locations
4. Find the **runner** project in the root of the solution
5. Right-click on the **runner** project and select "*Set as Startup Project*"
6. Start debugging by pressing `F5` or clicking the "*Start*" button
7. When the PowerToys Runner launches, **enable** the Keystroke Overlay module in the UI
8. Use the Visual Studio Debug menu or press `Ctrl+Alt+P` to open "*Reattach to Process*"
9. Find and select "**PowerToys.KeystrokeOverlay.exe**" in the process list
10. Trigger the action in Keystroke Overlay that should hit your breakpoint
11. Verify that the debugger breaks at your breakpoint and you can inspect variables and step through code
This process allows you to debug the Keystroke Overlay module while it's running as part of the full PowerToys application.
### Alternative Debugging Approach
To directly debug the Keystroke Overlay UI component:
0. Get familiar with the overall [Debugging Process](../development/debugging.md) for PowerToys.
1. **Build** the entire PowerToys solution in Visual Studio
2. Navigate to the **KeystrokeOverlay** folder in Solution Explorer
3. Open the file you want to debug and set **breakpoints** at the relevant locations
4. Right-click on the **KeystrokeOverlayUI** project and select "*Set as Startup Project*"
5. Start debugging by pressing `F5` or clicking the "*Start*" button
6. Verify that the debugger breaks at your breakpoint and you can inspect variables and step through code
## Any known issues with debugging here

View File

@@ -32,6 +32,7 @@ RegistryPreview
ShortcutGuide
Workspaces
ZoomIt
KeystrokeOverlay
```
### 📄 Get

View File

@@ -0,0 +1,59 @@
<Include>
<!-- Names of folders and projects -->
<?define FancyZonesProjectName="FancyZones"?>
<?define ImageResizerProjectName="ImageResizer"?>
<?define KeyboardManagerProjectName="KeyboardManager"?>
<?define PowerAccentProjectName="PowerAccent"?>
<?define PowerRenameProjectName="PowerRename"?>
<?define FileLocksmithProjectName="FileLocksmith"?>
<?define ColorPickerProjectName="ColorPicker"?>
<?define PowerOCRProjectName="PowerOCR"?>
<?define AwakeProjectName="Awake"?>
<?define MouseUtilsProjectName="MouseUtils"?>
<?define AlwaysOnTopProjectName="AlwaysOnTop"?>
<?define MeasureToolProjectName="MeasureTool"?>
<?define HostsProjectName="Hosts"?>
<?define MouseWithoutBordersProjectName="MouseWithoutBorders"?>
<?define AdvancedPasteProjectName="AdvancedPaste"?>
<?define RegistryPreviewProjectName="RegistryPreview"?>
<?define PeekProjectName="Peek"?>
<?define WorkspacesProjectName="Workspaces"?>
<?define KeystrokeOverlayProjectName="KeystrokeOverlay"?>
<?define RepoDir="$(var.ProjectDir)..\..\" ?>
<?if $(var.Platform) = x64?>
<?define PowerToysPlatform="x64"?>
<?define PlatformProgramFiles="[ProgramFiles64Folder]"?>
<?define PlatformLK="x64" ?>
<?define BinDir="$(var.RepoDir)x64\$(var.Configuration)\" ?>
<?else?>
<!-- stable WIX 3 doesn't support ARM64, so we build installers as x86 -->
<?define PowerToysPlatform="ARM64"?>
<!--TODO: define to ARM64 Program files once it's available-->
<?define PlatformProgramFiles="[ProgramFiles6432Folder]"?>
<?define PlatformLK="arm64" ?>
<?define BinDir="$(var.RepoDir)ARM64\$(var.Configuration)\" ?>
<?endif?>
<?if $(var.PerUser) = "true"?>
<?define PerMachineYesNo="no"?>
<?define MSIPath="UserSetup"?>
<?define MSIName="PowerToysUserSetup-$(var.Version)-$(var.PowerToysPlatform).msi"?>
<?define DefaultInstallDir="LocalAppDataFolder" ?>
<?define RegistryScope="HKCU" ?>
<?define InstallScope="perUser" ?>
<?define InstallPrivileges="limited" ?>
<?define UpgradeCodeGUID="D8B559DB-4C98-487A-A33F-50A8EEE42726" ?>
<?else?>
<?define PerMachineYesNo="yes"?>
<?define MSIPath="MachineSetup"?>
<?define MSIName="PowerToysSetup-$(var.Version)-$(var.PowerToysPlatform).msi"?>
<?define DefaultInstallDir="ProgramFiles64Folder" ?>
<?define RegistryScope="HKLM" ?>
<?define InstallScope="perMachine" ?>
<?define InstallPrivileges="elevated" ?>
<?define UpgradeCodeGUID="42B84BF7-5FBF-473B-9C8B-049DC16F7708" ?>
<?endif?>
<?define BinX32Dir="$(var.RepoDir)x86\$(var.Configuration)\" ?>
</Include>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"
xmlns:util="http://schemas.microsoft.com/wix/UtilExtension" >
<?include $(sys.CURRENTDIR)\Common.wxi?>
<!-- TODO: keystroke overlay -->
</Wix>

View File

@@ -0,0 +1,205 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" InitialTargets="EnsureNuGetPackageBuildImports"
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\..\src\Version.props" Condition="Exists('..\..\src\Version.props')" />
<Import Project="..\..\src\CmdPalVersion.props" Condition="Exists('..\..\src\CmdPalVersion.props')" />
<Import Project="..\wix.props" Condition="Exists('..\wix.props')" />
<PropertyGroup Condition="'$(Platform)' == 'x64'">
<DefineConstants>Version=$(Version);MonacoSRCHarvestPath=$(ProjectDir)..\..\x64\$(Configuration)\Assets\Monaco\monacoSRC;CmdPalVersion=$(CmdPalVersion)</DefineConstants>
<!-- THIS IS AN INNER LOOP OPTIMIZATION
The build pipeline builds the Settings and Launcher projects for Publication
using a specific profile. If you're doing local installer builds, this will
simulate the build pipeline doing that for you. -->
<PreBuildEvent>IF NOT DEFINED IsPipeline (
call "$([MSBuild]::GetVsInstallRoot())\Common7\Tools\VsDevCmd.bat" -arch=amd64 -host_arch=amd64 -winsdk=10.0.19041.0 -vcvars_ver=$(VCToolsVersion)
SET PTRoot=$(SolutionDir)\..
call "..\..\..\publish.cmd" x64
)
call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuildThisFileDirectory)\generateMonacoWxs.ps1 -monacoWxsFile "$(MSBuildThisFileDirectory)\MonacoSRC.wxs"
call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuildThisFileDirectory)\generateDscResourcesWxs.ps1 -dscWxsFile "$(MSBuildThisFileDirectory)\DscResources.wxs" -Platform "$(Platform)" -Configuration "$(Configuration)"
</PreBuildEvent>
</PropertyGroup>
<PropertyGroup Condition="'$(Platform)' != 'x64'">
<DefineConstants>Version=$(Version);MonacoSRCHarvestPath=$(ProjectDir)..\..\ARM64\$(Configuration)\Assets\Monaco\monacoSRC;CmdPalVersion=$(CmdPalVersion);</DefineConstants>
<PreBuildEvent>IF NOT DEFINED IsPipeline (
call "$([MSBuild]::GetVsInstallRoot())\Common7\Tools\VsDevCmd.bat" -arch=arm64 -host_arch=amd64 -winsdk=10.0.19041.0 -vcvars_ver=$(VCToolsVersion)
SET PTRoot=$(SolutionDir)\..
call "..\..\..\publish.cmd" arm64
)
call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuildThisFileDirectory)\generateMonacoWxs.ps1 -monacoWxsFile "$(MSBuildThisFileDirectory)\MonacoSRC.wxs"
call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuildThisFileDirectory)\generateDscResourcesWxs.ps1 -dscWxsFile "$(MSBuildThisFileDirectory)\DscResources.wxs" -Platform "$(Platform)" -Configuration "$(Configuration)"
</PreBuildEvent>
</PropertyGroup>
<PropertyGroup>
<RunPostBuildEvent>Always</RunPostBuildEvent>
<PostBuildEvent>
call move /Y ..\..\..\AdvancedPaste.wxs.bk ..\..\..\AdvancedPaste.wxs
call move /Y ..\..\..\Awake.wxs.bk ..\..\..\Awake.wxs
call move /Y ..\..\..\BaseApplications.wxs.bk ..\..\..\BaseApplications.wxs
call move /Y ..\..\..\CmdPal.wxs.bk ..\..\..\CmdPal.wxs
call move /Y ..\..\..\ColorPicker.wxs.bk ..\..\..\ColorPicker.wxs
call move /Y ..\..\..\Core.wxs.bk ..\..\..\Core.wxs
call move /Y ..\..\..\EnvironmentVariables.wxs.bk ..\..\..\EnvironmentVariables.wxs
call move /Y ..\..\..\FileExplorerPreview.wxs.bk ..\..\..\FileExplorerPreview.wxs
call move /Y ..\..\..\FileLocksmith.wxs.bk ..\..\..\FileLocksmith.wxs
call move /Y ..\..\..\Hosts.wxs.bk ..\..\..\Hosts.wxs
call move /Y ..\..\..\ImageResizer.wxs.bk ..\..\..\ImageResizer.wxs
call move /Y ..\..\..\KeyboardManager.wxs.bk ..\..\..\KeyboardManager.wxs
call move /Y ..\..\..\MouseWithoutBorders.wxs.bk ..\..\..\MouseWithoutBorders.wxs
call move /Y ..\..\..\NewPlus.wxs.bk ..\..\..\NewPlus.wxs
call move /Y ..\..\..\Peek.wxs.bk ..\..\..\Peek.wxs
call move /Y ..\..\..\PowerRename.wxs.bk ..\..\..\PowerRename.wxs
call move /Y ..\..\..\Product.wxs.bk ..\..\..\Product.wxs
call move /Y ..\..\..\RegistryPreview.wxs.bk ..\..\..\RegistryPreview.wxs
call move /Y ..\..\..\Resources.wxs.bk ..\..\..\Resources.wxs
call move /Y ..\..\..\Run.wxs.bk ..\..\..\Run.wxs
call move /Y ..\..\..\Settings.wxs.bk ..\..\..\Settings.wxs
call move /Y ..\..\..\ShortcutGuide.wxs.bk ..\..\..\ShortcutGuide.wxs
call move /Y ..\..\..\KeystrokeOverlay.wxs.bk ..\..\..\KeystrokeOverlay.wxs
call move /Y ..\..\..\Tools.wxs.bk ..\..\..\Tools.wxs
call move /Y ..\..\..\WinAppSDK.wxs.bk ..\..\..\WinAppSDK.wxs
call move /Y ..\..\..\WinUI3Applications.wxs.bk ..\..\..\WinUI3Applications.wxs
call move /Y ..\..\..\Workspaces.wxs.bk ..\..\..\Workspaces.wxs
</PostBuildEvent>
</PropertyGroup>
<PropertyGroup>
<Name>PowerToysInstaller</Name>
</PropertyGroup>
<PropertyGroup Label="UserMacros" Condition=" '$(PerUser)' == 'true' ">
<DefineConstants>$(DefineConstants);PerUser=true</DefineConstants>
</PropertyGroup>
<PropertyGroup Label="UserMacros" Condition=" '$(PerUser)' != 'true' ">
<DefineConstants>$(DefineConstants);PerUser=false</DefineConstants>
</PropertyGroup>
<PropertyGroup Label="UserMacros" Condition=" '$(CIBuild)' == 'true' ">
<DefineConstants>$(DefineConstants);CIBuild=true</DefineConstants>
</PropertyGroup>
<PropertyGroup Label="UserMacros" Condition=" '$(CIBuild)' != 'true' ">
<DefineConstants>$(DefineConstants);CIBuild=false</DefineConstants>
</PropertyGroup>
<PropertyGroup>
<!-- We do not support debug installer builds -->
<Configuration Condition=" '$(Configuration)' == '' ">Release</Configuration>
<Platform>$(Platform)</Platform>
<ProductVersion>3.10</ProductVersion>
<ProjectGuid>022a9d30-7c4f-416d-a9df-5ff2661cc0ad</ProjectGuid>
<SchemaVersion>2.0</SchemaVersion>
<OutputName Condition=" '$(PerUser)' != 'true' ">PowerToysSetup-$(Version)-$(Platform)</OutputName>
<OutputName Condition=" '$(PerUser)' == 'true' ">PowerToysUserSetup-$(Version)-$(Platform)</OutputName>
<OutputType>Package</OutputType>
<SuppressAclReset>True</SuppressAclReset>
<NuGetPackageImportStamp>
</NuGetPackageImportStamp>
<!-- 1076 and ICE91 - warning: using this configuration for perMachine install could cause problems. -->
<!-- 1026 - warning: file ID is too long -->
<SuppressIces>ICE91</SuppressIces>
<SuppressSpecificWarnings>1026;1076</SuppressSpecificWarnings>
</PropertyGroup>
<PropertyGroup>
<OutputPath Condition=" '$(PerUser)' != 'true' ">$(Platform)\$(Configuration)\MachineSetup</OutputPath>
<OutputPath Condition=" '$(PerUser)' == 'true' ">$(Platform)\$(Configuration)\UserSetup</OutputPath>
<IntermediateOutputPath Condition=" '$(PerUser)' != 'true' ">obj\$(Platform)\$(Configuration)\MachineSetup</IntermediateOutputPath>
<IntermediateOutputPath Condition=" '$(PerUser)' == 'true' ">obj\$(Platform)\$(Configuration)\UserSetup</IntermediateOutputPath>
<SuppressIces>ICE40</SuppressIces>
</PropertyGroup>
<PropertyGroup>
<!-- suppress warning 1108 regarding -sh being deprecated -->
<!-- -sh suppresses file information which was causing wix build to hang in CI -->
<LinkerAdditionalOptions>-v -sh -sw1108</LinkerAdditionalOptions>
</PropertyGroup>
<ItemGroup>
<Compile Include="CustomDialogs\PTInstallDirDlg.wxs" />
<Compile Include="CustomDialogs\PTLicenseDlg.wxs" />
<Compile Include="CustomDialogs\WixUI_PTInstallDir.wxs" />
<Compile Include="NewPlus.wxs" />
<Compile Include="Product.wxs" />
<Compile Include="AdvancedPaste.wxs" />
<Compile Include="Awake.wxs" />
<Compile Include="BaseApplications.wxs" />
<Compile Include="CmdPal.wxs" />
<Compile Include="ColorPicker.wxs" />
<Compile Include="EnvironmentVariables.wxs" />
<Compile Include="FileExplorerPreview.wxs" />
<Compile Include="FileLocksmith.wxs" />
<Compile Include="Hosts.wxs" />
<Compile Include="ImageResizer.wxs" />
<Compile Include="KeyboardManager.wxs" />
<Compile Include="DscResources.wxs" />
<Compile Include="Peek.wxs" />
<Compile Include="PowerRename.wxs" />
<Compile Include="RegistryPreview.wxs" />
<Compile Include="Run.wxs" />
<Compile Include="Settings.wxs" />
<Compile Include="ShortcutGuide.wxs" />
<Compile Include="Tools.wxs" />
<Compile Include="MouseWithoutBorders.wxs" />
<Compile Include="WinUI3Applications.wxs" />
<Compile Include="MonacoSRC.wxs" />
<Compile Include="Core.wxs" />
<Compile Include="Resources.wxs" />
<Compile Include="WinAppSDK.wxs" />
<Compile Include="Workspaces.wxs" />
</ItemGroup>
<ItemGroup>
<WixExtension Include="WixFirewallExtension">
<HintPath>$(WixExtDir)\WixFirewallExtension.dll</HintPath>
<Name>WixFirewallExtension</Name>
</WixExtension>
<WixExtension Include="WixUtilExtension">
<HintPath>$(WixExtDir)\WixUtilExtension.dll</HintPath>
<Name>WixUtilExtension</Name>
</WixExtension>
<WixExtension Include="WixUIExtension">
<HintPath>$(WixExtDir)\WixUIExtension.dll</HintPath>
<Name>WixUIExtension</Name>
</WixExtension>
<WixExtension Include="WixNetFxExtension">
<HintPath>$(WixExtDir)\WixNetFxExtension.dll</HintPath>
<Name>WixNetFxExtension</Name>
</WixExtension>
</ItemGroup>
<ItemGroup>
<Folder Include="CustomDialogs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PowerToysSetupCustomActions\PowerToysSetupCustomActions.vcxproj">
<Name>PowerToysSetupCustomActions</Name>
<Project>{32f3882b-f2d6-4586-b5ed-11e39e522bd3}</Project>
<Private>True</Private>
<DoNotHarvest>True</DoNotHarvest>
<RefProjectOutputGroups>Binaries;Content;Satellites</RefProjectOutputGroups>
<RefTargetDir>INSTALLFOLDER</RefTargetDir>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Content Include="packages.config" />
</ItemGroup>
<Import Project="$(WixTargetsPath)" Condition=" '$(WixTargetsPath)' != '' " />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\WiX\v3.x\Wix.targets" Condition=" '$(WixTargetsPath)' == '' AND Exists('$(MSBuildExtensionsPath32)\Microsoft\WiX\v3.x\Wix.targets') " />
<Target Name="EnsureWixToolsetInstalled" Condition=" '$(WixTargetsImported)' != 'true' ">
<Error Text="The WiX Toolset v3 build tools must be installed to build this project. To download the WiX Toolset, see http://wixtoolset.org/releases/" />
</Target>
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\wix.props')" Text="$([System.String]::Format('$(ErrorText)', '..\wix.props'))" />
</Target>
<!--
To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Wix.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target> -->
<Target Name="BeforeBuild">
<HeatDirectory Directory="..\..\src\Monaco\monacoSRC" PreprocessorVariable="var.MonacoSRCHarvestPath" OutputFile="MonacoSRC.wxs" ComponentGroupName="MonacoSRCHeatGenerated" DirectoryRefId="MonacoPreviewHandlerMonacoSRCFolder" AutogenerateGuids="false" GenerateGuidsNow="true" ToolPath="$(WixToolPath)" RunAsSeparateProcess="true" SuppressFragments="false" SuppressRegistry="false" SuppressRootDirectory="true" />
</Target>
<!-- Prevents NU1503 -->
<Target Name="_IsProjectRestoreSupported" Returns="@(_ValidProjectsForRestore)">
<ItemGroup>
<_ValidProjectsForRestore Include="$(MSBuildProjectFullPath)" />
</ItemGroup>
</Target>
<Target Name="Restore" />
</Project>

View File

@@ -0,0 +1,327 @@
[CmdletBinding()]
Param(
[Parameter(Mandatory = $True, Position = 1)]
[string]$platform,
[Parameter(Mandatory = $False, Position = 2)]
[string]$installscopeperuser = "false"
)
Function Generate-FileList() {
[CmdletBinding()]
Param(
# Can be multiple files separated by ; as long as they're on the same directory
[Parameter(Mandatory = $True, Position = 1)]
[AllowEmptyString()]
[string]$fileDepsJson,
[Parameter(Mandatory = $True, Position = 2)]
[string]$fileListName,
[Parameter(Mandatory = $True, Position = 3)]
[string]$wxsFilePath,
# If there is no deps.json file, just pass path to files
[Parameter(Mandatory = $False, Position = 4)]
[string]$depsPath,
# launcher plugins are being loaded into launcher process,
# so there are some additional dependencies to skip
[Parameter(Mandatory = $False, Position = 5)]
[bool]$isLauncherPlugin
)
$fileWxs = Get-Content $wxsFilePath;
$fileExclusionList = @("*.pdb", "*.lastcodeanalysissucceeded", "createdump.exe", "powertoys.exe")
$fileInclusionList = @("*.dll", "*.exe", "*.json", "*.msix", "*.png", "*.gif", "*.ico", "*.cur", "*.svg", "index.html", "reg.js", "gitignore.js", "srt.js", "monacoSpecialLanguages.js", "customTokenThemeRules.js", "*.pri")
$dllsToIgnore = @("System.CodeDom.dll", "WindowsBase.dll")
if ($fileDepsJson -eq [string]::Empty) {
$fileDepsRoot = $depsPath
} else {
$multipleDepsJson = $fileDepsJson.Split(";")
foreach ( $singleDepsJson in $multipleDepsJson )
{
$fileDepsRoot = (Get-ChildItem $singleDepsJson).Directory.FullName
$depsJson = Get-Content $singleDepsJson | ConvertFrom-Json
$runtimeList = ([array]$depsJson.targets.PSObject.Properties)[-1].Value.PSObject.Properties | Where-Object {
$_.Name -match "runtimepack.*Runtime"
};
$runtimeList | ForEach-Object {
$_.Value.PSObject.Properties.Value | ForEach-Object {
$fileExclusionList += $_.PSObject.Properties.Name
}
}
}
}
$fileExclusionList = $fileExclusionList | Where-Object {$_ -notin $dllsToIgnore}
if ($isLauncherPlugin -eq $True) {
$fileInclusionList += @("*.deps.json")
$fileExclusionList += @("Ijwhost.dll", "PowerToys.Common.UI.dll", "PowerToys.GPOWrapper.dll", "PowerToys.GPOWrapperProjection.dll", "PowerToys.PowerLauncher.Telemetry.dll", "PowerToys.ManagedCommon.dll", "PowerToys.Settings.UI.Lib.dll", "Wox.Infrastructure.dll", "Wox.Plugin.dll")
}
$fileList = Get-ChildItem $fileDepsRoot -Include $fileInclusionList -Exclude $fileExclusionList -File -Name
$fileWxs = $fileWxs -replace "(<\?define $($fileListName)=)", "<?define $fileListName=$($fileList -join ';')"
Set-Content -Path $wxsFilePath -Value $fileWxs
}
Function Generate-FileComponents() {
[CmdletBinding()]
Param(
[Parameter(Mandatory = $True, Position = 1)]
[string]$fileListName,
[Parameter(Mandatory = $True, Position = 2)]
[string]$wxsFilePath,
[Parameter(Mandatory = $True, Position = 3)]
[string]$regroot
)
$wxsFile = Get-Content $wxsFilePath;
$wxsFile | ForEach-Object {
if ($_ -match "(<?define $fileListName=)(.*)\?>") {
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'fileList',
Justification = 'variable is used in another scope')]
$fileList = $matches[2] -split ';'
return
}
}
$componentId = "$($fileListName)_Component"
$componentDefs = "`r`n"
$componentDefs +=
@"
<Component Id="$($componentId)" Win64="yes" Guid="$((New-Guid).ToString().ToUpper())">
<RegistryKey Root="$($regroot)" Key="Software\Classes\powertoys\components">
<RegistryValue Type="string" Name="$($componentId)" Value="" KeyPath="yes"/>
</RegistryKey>`r`n
"@
foreach ($file in $fileList) {
$fileTmp = $file -replace "-", "_"
$componentDefs +=
@"
<File Id="$($fileListName)_File_$($fileTmp)" Source="`$(var.$($fileListName)Path)\$($file)" />`r`n
"@
}
$componentDefs +=
@"
</Component>`r`n
"@
$wxsFile = $wxsFile -replace "\s+(<!--$($fileListName)_Component_Def-->)", $componentDefs
$componentRef =
@"
<ComponentRef Id="$($componentId)" />
"@
$wxsFile = $wxsFile -replace "\s+(</ComponentGroup>)", "$componentRef`r`n </ComponentGroup>"
Set-Content -Path $wxsFilePath -Value $wxsFile
}
if ($platform -ceq "arm64") {
$platform = "ARM64"
}
if ($installscopeperuser -eq "true") {
$registryroot = "HKCU"
} else {
$registryroot = "HKLM"
}
#BaseApplications
Generate-FileList -fileDepsJson "" -fileListName BaseApplicationsFiles -wxsFilePath $PSScriptRoot\BaseApplications.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release"
Generate-FileComponents -fileListName "BaseApplicationsFiles" -wxsFilePath $PSScriptRoot\BaseApplications.wxs -regroot $registryroot
#WinUI3Applications
Generate-FileList -fileDepsJson "" -fileListName WinUI3ApplicationsFiles -wxsFilePath $PSScriptRoot\WinUI3Applications.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps"
Generate-FileComponents -fileListName "WinUI3ApplicationsFiles" -wxsFilePath $PSScriptRoot\WinUI3Applications.wxs -regroot $registryroot
#AdvancedPaste
Generate-FileList -fileDepsJson "" -fileListName AdvancedPasteAssetsFiles -wxsFilePath $PSScriptRoot\AdvancedPaste.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\AdvancedPaste"
Generate-FileComponents -fileListName "AdvancedPasteAssetsFiles" -wxsFilePath $PSScriptRoot\AdvancedPaste.wxs -regroot $registryroot
#AwakeFiles
Generate-FileList -fileDepsJson "" -fileListName AwakeImagesFiles -wxsFilePath $PSScriptRoot\Awake.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\Assets\Awake"
Generate-FileComponents -fileListName "AwakeImagesFiles" -wxsFilePath $PSScriptRoot\Awake.wxs -regroot $registryroot
#ColorPicker
Generate-FileList -fileDepsJson "" -fileListName ColorPickerAssetsFiles -wxsFilePath $PSScriptRoot\ColorPicker.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\Assets\ColorPicker"
Generate-FileComponents -fileListName "ColorPickerAssetsFiles" -wxsFilePath $PSScriptRoot\ColorPicker.wxs -regroot $registryroot
#Environment Variables
Generate-FileList -fileDepsJson "" -fileListName EnvironmentVariablesAssetsFiles -wxsFilePath $PSScriptRoot\EnvironmentVariables.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\EnvironmentVariables"
Generate-FileComponents -fileListName "EnvironmentVariablesAssetsFiles" -wxsFilePath $PSScriptRoot\EnvironmentVariables.wxs -regroot $registryroot
#FileExplorerAdd-ons
Generate-FileList -fileDepsJson "" -fileListName MonacoPreviewHandlerMonacoAssetsFiles -wxsFilePath $PSScriptRoot\FileExplorerPreview.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\Assets\Monaco"
Generate-FileList -fileDepsJson "" -fileListName MonacoPreviewHandlerCustomLanguagesFiles -wxsFilePath $PSScriptRoot\FileExplorerPreview.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\Assets\Monaco\customLanguages"
Generate-FileComponents -fileListName "MonacoPreviewHandlerMonacoAssetsFiles" -wxsFilePath $PSScriptRoot\FileExplorerPreview.wxs -regroot $registryroot
Generate-FileComponents -fileListName "MonacoPreviewHandlerCustomLanguagesFiles" -wxsFilePath $PSScriptRoot\FileExplorerPreview.wxs -regroot $registryroot
#FileLocksmith
Generate-FileList -fileDepsJson "" -fileListName FileLocksmithAssetsFiles -wxsFilePath $PSScriptRoot\FileLocksmith.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\FileLocksmith"
Generate-FileComponents -fileListName "FileLocksmithAssetsFiles" -wxsFilePath $PSScriptRoot\FileLocksmith.wxs -regroot $registryroot
#Hosts
Generate-FileList -fileDepsJson "" -fileListName HostsAssetsFiles -wxsFilePath $PSScriptRoot\Hosts.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\Hosts"
Generate-FileComponents -fileListName "HostsAssetsFiles" -wxsFilePath $PSScriptRoot\Hosts.wxs -regroot $registryroot
#ImageResizer
Generate-FileList -fileDepsJson "" -fileListName ImageResizerAssetsFiles -wxsFilePath $PSScriptRoot\ImageResizer.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\ImageResizer"
Generate-FileComponents -fileListName "ImageResizerAssetsFiles" -wxsFilePath $PSScriptRoot\ImageResizer.wxs -regroot $registryroot
#New+
Generate-FileList -fileDepsJson "" -fileListName NewPlusAssetsFiles -wxsFilePath $PSScriptRoot\NewPlus.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\NewPlus"
Generate-FileComponents -fileListName "NewPlusAssetsFiles" -wxsFilePath $PSScriptRoot\NewPlus.wxs -regroot $registryroot
#Peek
Generate-FileList -fileDepsJson "" -fileListName PeekAssetsFiles -wxsFilePath $PSScriptRoot\Peek.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\Peek\"
Generate-FileComponents -fileListName "PeekAssetsFiles" -wxsFilePath $PSScriptRoot\Peek.wxs -regroot $registryroot
#PowerRename
Generate-FileList -fileDepsJson "" -fileListName PowerRenameAssetsFiles -wxsFilePath $PSScriptRoot\PowerRename.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\PowerRename\"
Generate-FileComponents -fileListName "PowerRenameAssetsFiles" -wxsFilePath $PSScriptRoot\PowerRename.wxs -regroot $registryroot
#RegistryPreview
Generate-FileList -fileDepsJson "" -fileListName RegistryPreviewAssetsFiles -wxsFilePath $PSScriptRoot\RegistryPreview.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\RegistryPreview\"
Generate-FileComponents -fileListName "RegistryPreviewAssetsFiles" -wxsFilePath $PSScriptRoot\RegistryPreview.wxs -regroot $registryroot
#KeystrokeOverlay
Generate-FileList -fileDepsJson "" -fileListName KeystrokeOverlayAssetsFiles -wxsFilePath $PSScriptRoot\KeystrokeOverlay.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\KeystrokeOverlay\"
Generate-FileComponents -fileListName "KeystrokeOverlayAssetsFiles" -wxsFilePath $PSScriptRoot\KeystrokeOverlay.wxs -regroot $registryroot
#Run
Generate-FileList -fileDepsJson "" -fileListName launcherImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\Assets\PowerLauncher"
Generate-FileComponents -fileListName "launcherImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
## Plugins
###Calculator
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Calculator\Microsoft.PowerToys.Run.Plugin.Calculator.deps.json" -fileListName calcComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
Generate-FileList -fileDepsJson "" -fileListName calcImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Calculator\Images"
Generate-FileComponents -fileListName "calcComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
Generate-FileComponents -fileListName "calcImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
###Folder
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Folder\Microsoft.Plugin.Folder.deps.json" -fileListName FolderComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
Generate-FileList -fileDepsJson "" -fileListName FolderImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Folder\Images"
Generate-FileComponents -fileListName "FolderComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
Generate-FileComponents -fileListName "FolderImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
###Program
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Program\Microsoft.Plugin.Program.deps.json" -fileListName ProgramComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
Generate-FileList -fileDepsJson "" -fileListName ProgramImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Program\Images"
Generate-FileComponents -fileListName "ProgramComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
Generate-FileComponents -fileListName "ProgramImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
###Shell
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Shell\Microsoft.Plugin.Shell.deps.json" -fileListName ShellComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
Generate-FileList -fileDepsJson "" -fileListName ShellImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Shell\Images"
Generate-FileComponents -fileListName "ShellComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
Generate-FileComponents -fileListName "ShellImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
###Indexer
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Indexer\Microsoft.Plugin.Indexer.deps.json" -fileListName IndexerComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
Generate-FileList -fileDepsJson "" -fileListName IndexerImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Indexer\Images"
Generate-FileComponents -fileListName "IndexerComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
Generate-FileComponents -fileListName "IndexerImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
###UnitConverter
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\UnitConverter\Community.PowerToys.Run.Plugin.UnitConverter.deps.json" -fileListName UnitConvCompFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
Generate-FileList -fileDepsJson "" -fileListName UnitConvImagesCompFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\UnitConverter\Images"
Generate-FileComponents -fileListName "UnitConvCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
Generate-FileComponents -fileListName "UnitConvImagesCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
###WebSearch
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\WebSearch\Community.PowerToys.Run.Plugin.WebSearch.deps.json" -fileListName WebSrchCompFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
Generate-FileList -fileDepsJson "" -fileListName WebSrchImagesCompFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\WebSearch\Images"
Generate-FileComponents -fileListName "WebSrchCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
Generate-FileComponents -fileListName "WebSrchImagesCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
###History
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\History\Microsoft.PowerToys.Run.Plugin.History.deps.json" -fileListName HistoryPluginComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
Generate-FileList -fileDepsJson "" -fileListName HistoryPluginImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\History\Images"
Generate-FileComponents -fileListName "HistoryPluginComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
Generate-FileComponents -fileListName "HistoryPluginImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
###Uri
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Uri\Microsoft.Plugin.Uri.deps.json" -fileListName UriComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
Generate-FileList -fileDepsJson "" -fileListName UriImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Uri\Images"
Generate-FileComponents -fileListName "UriComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
Generate-FileComponents -fileListName "UriImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
###VSCodeWorkspaces
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\VSCodeWorkspaces\Community.PowerToys.Run.Plugin.VSCodeWorkspaces.deps.json" -fileListName VSCWrkCompFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
Generate-FileList -fileDepsJson "" -fileListName VSCWrkImagesCompFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\VSCodeWorkspaces\Images"
Generate-FileComponents -fileListName "VSCWrkCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
Generate-FileComponents -fileListName "VSCWrkImagesCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
###WindowWalker
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\WindowWalker\Microsoft.Plugin.WindowWalker.deps.json" -fileListName WindowWlkrCompFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
Generate-FileList -fileDepsJson "" -fileListName WindowWlkrImagesCompFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\WindowWalker\Images"
Generate-FileComponents -fileListName "WindowWlkrCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
Generate-FileComponents -fileListName "WindowWlkrImagesCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
###OneNote
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\OneNote\Microsoft.PowerToys.Run.Plugin.OneNote.deps.json" -fileListName OneNoteComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
Generate-FileList -fileDepsJson "" -fileListName OneNoteImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\OneNote\Images"
Generate-FileComponents -fileListName "OneNoteComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
Generate-FileComponents -fileListName "OneNoteImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
###Registry
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Registry\Microsoft.PowerToys.Run.Plugin.Registry.deps.json" -fileListName RegistryComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
Generate-FileList -fileDepsJson "" -fileListName RegistryImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Registry\Images"
Generate-FileComponents -fileListName "RegistryComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
Generate-FileComponents -fileListName "RegistryImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
###Service
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Service\Microsoft.PowerToys.Run.Plugin.Service.deps.json" -fileListName ServiceComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
Generate-FileList -fileDepsJson "" -fileListName ServiceImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Service\Images"
Generate-FileComponents -fileListName "ServiceComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
Generate-FileComponents -fileListName "ServiceImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
###System
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\System\Microsoft.PowerToys.Run.Plugin.System.deps.json" -fileListName SystemComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
Generate-FileList -fileDepsJson "" -fileListName SystemImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\System\Images"
Generate-FileComponents -fileListName "SystemComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
Generate-FileComponents -fileListName "SystemImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
###TimeDate
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\TimeDate\Microsoft.PowerToys.Run.Plugin.TimeDate.deps.json" -fileListName TimeDateComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
Generate-FileList -fileDepsJson "" -fileListName TimeDateImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\TimeDate\Images"
Generate-FileComponents -fileListName "TimeDateComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
Generate-FileComponents -fileListName "TimeDateImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
###WindowsSettings
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\WindowsSettings\Microsoft.PowerToys.Run.Plugin.WindowsSettings.deps.json" -fileListName WinSetCmpFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
Generate-FileList -fileDepsJson "" -fileListName WinSetImagesCmpFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\WindowsSettings\Images"
Generate-FileComponents -fileListName "WinSetCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
Generate-FileComponents -fileListName "WinSetImagesCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
###WindowsTerminal
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\WindowsTerminal\Microsoft.PowerToys.Run.Plugin.WindowsTerminal.deps.json" -fileListName WinTermCmpFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
Generate-FileList -fileDepsJson "" -fileListName WinTermImagesCmpFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\WindowsTerminal\Images"
Generate-FileComponents -fileListName "WinTermCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
Generate-FileComponents -fileListName "WinTermImagesCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
###PowerToys
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\PowerToys\Microsoft.PowerToys.Run.Plugin.PowerToys.deps.json" -fileListName PowerToysCmpFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
Generate-FileList -fileDepsJson "" -fileListName PowerToysImagesCmpFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\PowerToys\Images"
Generate-FileComponents -fileListName "PowerToysCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
Generate-FileComponents -fileListName "PowerToysImagesCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
###ValueGenerator
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\ValueGenerator\Community.PowerToys.Run.Plugin.ValueGenerator.deps.json" -fileListName ValueGeneratorCmpFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
Generate-FileList -fileDepsJson "" -fileListName ValueGeneratorImagesCmpFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\ValueGenerator\Images"
Generate-FileComponents -fileListName "ValueGeneratorCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
Generate-FileComponents -fileListName "ValueGeneratorImagesCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
## Plugins
#ShortcutGuide
Generate-FileList -fileDepsJson "" -fileListName ShortcutGuideSvgFiles -wxsFilePath $PSScriptRoot\ShortcutGuide.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\Assets\ShortcutGuide\"
Generate-FileComponents -fileListName "ShortcutGuideSvgFiles" -wxsFilePath $PSScriptRoot\ShortcutGuide.wxs -regroot $registryroot
#Settings
Generate-FileList -fileDepsJson "" -fileListName SettingsV2AssetsFiles -wxsFilePath $PSScriptRoot\Settings.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\Settings\"
Generate-FileList -fileDepsJson "" -fileListName SettingsV2AssetsModulesFiles -wxsFilePath $PSScriptRoot\Settings.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\Settings\Modules\"
Generate-FileList -fileDepsJson "" -fileListName SettingsV2OOBEAssetsModulesFiles -wxsFilePath $PSScriptRoot\Settings.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\Settings\Modules\OOBE\"
Generate-FileList -fileDepsJson "" -fileListName SettingsV2OOBEAssetsFluentIconsFiles -wxsFilePath $PSScriptRoot\Settings.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\Settings\Icons\"
Generate-FileComponents -fileListName "SettingsV2AssetsFiles" -wxsFilePath $PSScriptRoot\Settings.wxs -regroot $registryroot
Generate-FileComponents -fileListName "SettingsV2AssetsModulesFiles" -wxsFilePath $PSScriptRoot\Settings.wxs -regroot $registryroot
Generate-FileComponents -fileListName "SettingsV2OOBEAssetsModulesFiles" -wxsFilePath $PSScriptRoot\Settings.wxs -regroot $registryroot
Generate-FileComponents -fileListName "SettingsV2OOBEAssetsFluentIconsFiles" -wxsFilePath $PSScriptRoot\Settings.wxs -regroot $registryroot
#Workspaces
Generate-FileList -fileDepsJson "" -fileListName WorkspacesImagesComponentFiles -wxsFilePath $PSScriptRoot\Workspaces.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\Assets\Workspaces\"
Generate-FileComponents -fileListName "WorkspacesImagesComponentFiles" -wxsFilePath $PSScriptRoot\Workspaces.wxs -regroot $registryroot

View File

@@ -136,6 +136,7 @@ call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuil
<Compile Include="Resources.wxs" />
<Compile Include="WinAppSDK.wxs" />
<Compile Include="Workspaces.wxs" />
<Compile Include="KeystrokeOverlay.wxs" />
</ItemGroup>
<ItemGroup>
<Folder Include="CustomDialogs" />

View File

@@ -62,6 +62,7 @@
<ComponentGroupRef Id="AdvancedPasteComponentGroup" />
<ComponentGroupRef Id="NewPlusComponentGroup" />
<ComponentGroupRef Id="NewPlusTemplatesComponentGroup" />
<ComponentGroupRef Id="KeystrokeOverlayComponentGroup" />
<ComponentGroupRef Id="ResourcesComponentGroup" />
<ComponentGroupRef Id="DscResourcesComponentGroup" />
<ComponentGroupRef Id="WindowsAppSDKComponentGroup" />

View File

@@ -176,6 +176,10 @@ Generate-FileComponents -fileListName "ImageResizerAssetsFiles" -wxsFilePath $PS
Generate-FileList -fileDepsJson "" -fileListName LightSwitchFiles -wxsFilePath $PSScriptRoot\LightSwitch.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\LightSwitchService"
Generate-FileComponents -fileListName "LightSwitchFiles" -wxsFilePath $PSScriptRoot\LightSwitch.wxs
# Light Switch Service
Generate-FileList -fileDepsJson "" -fileListName LightSwitchFiles -wxsFilePath $PSScriptRoot\LightSwitch.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\LightSwitchService"
Generate-FileComponents -fileListName "LightSwitchFiles" -wxsFilePath $PSScriptRoot\LightSwitch.wxs -regroot $registryroot
#New+
Generate-FileList -fileDepsJson "" -fileListName NewPlusAssetsFiles -wxsFilePath $PSScriptRoot\NewPlus.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\NewPlus"
Generate-FileComponents -fileListName "NewPlusAssetsFiles" -wxsFilePath $PSScriptRoot\NewPlus.wxs

View File

@@ -288,4 +288,8 @@ namespace winrt::PowerToys::GPOWrapper::implementation
{
return static_cast<GpoRuleConfigured>(powertoys_gpo::getConfiguredRunAtStartupValue());
}
GpoRuleConfigured GPOWrapper::GetConfiguredKeystrokeOverlayEnabledValue()
{
return static_cast<GpoRuleConfigured>(powertoys_gpo::getConfiguredKeystrokeOverlayEnabledValue());
}
}

View File

@@ -77,6 +77,7 @@ namespace winrt::PowerToys::GPOWrapper::implementation
static GpoRuleConfigured GetAllowDataDiagnosticsValue();
static GpoRuleConfigured GetConfiguredRunAtStartupValue();
static GpoRuleConfigured GetConfiguredNewPlusReplaceVariablesValue();
static GpoRuleConfigured GetConfiguredKeystrokeOverlayEnabledValue();
};
}

View File

@@ -81,6 +81,7 @@ namespace PowerToys
static GpoRuleConfigured GetAllowDataDiagnosticsValue();
static GpoRuleConfigured GetConfiguredRunAtStartupValue();
static GpoRuleConfigured GetConfiguredNewPlusReplaceVariablesValue();
static GpoRuleConfigured GetConfiguredKeystrokeOverlayEnabledValue();
}
}
}

View File

@@ -66,5 +66,10 @@ namespace PowerToys.GPOWrapperProjection
{
return (GpoRuleConfigured)PowerToys.GPOWrapper.GPOWrapper.GetConfiguredWorkspacesEnabledValue();
}
public static GpoRuleConfigured GetConfiguredKeystrokeOverlayEnabledValue()
{
return (GpoRuleConfigured)PowerToys.GPOWrapper.GPOWrapper.GetConfiguredKeystrokeOverlayEnabledValue();
}
}
}

View File

@@ -20,6 +20,7 @@ namespace ManagedCommon
Hosts,
ImageResizer,
KeyboardManager,
KeystrokeOverlay,
LightSwitch,
MouseHighlighter,
MouseJump,

View File

@@ -0,0 +1,399 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.PowerToys.UITest
{
/// <summary>
/// Provides methods for recording the screen during UI tests.
/// Requires FFmpeg to be installed and available in PATH.
/// </summary>
internal class ScreenRecording : IDisposable
{
private readonly string outputDirectory;
private readonly string framesDirectory;
private readonly string outputFilePath;
private readonly List<string> capturedFrames;
private readonly SemaphoreSlim recordingLock = new(1, 1);
private readonly Stopwatch recordingStopwatch = new();
private readonly string? ffmpegPath;
private CancellationTokenSource? recordingCancellation;
private Task? recordingTask;
private bool isRecording;
private int frameCount;
[DllImport("user32.dll")]
private static extern IntPtr GetDC(IntPtr hWnd);
[DllImport("gdi32.dll")]
private static extern int GetDeviceCaps(IntPtr hdc, int nIndex);
[DllImport("user32.dll")]
private static extern IntPtr ReleaseDC(IntPtr hWnd, IntPtr hDC);
[DllImport("user32.dll")]
private static extern bool GetCursorInfo(out ScreenCapture.CURSORINFO pci);
[DllImport("user32.dll")]
private static extern bool DrawIconEx(IntPtr hdc, int x, int y, IntPtr hIcon, int cx, int cy, int istepIfAniCur, IntPtr hbrFlickerFreeDraw, int diFlags);
private const int CURSORSHOWING = 0x00000001;
private const int DESKTOPHORZRES = 118;
private const int DESKTOPVERTRES = 117;
private const int DINORMAL = 0x0003;
private const int TargetFps = 15; // 15 FPS for good balance of quality and size
/// <summary>
/// Initializes a new instance of the <see cref="ScreenRecording"/> class.
/// </summary>
/// <param name="outputDirectory">Directory where the recording will be saved.</param>
public ScreenRecording(string outputDirectory)
{
this.outputDirectory = outputDirectory;
string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
framesDirectory = Path.Combine(outputDirectory, $"frames_{timestamp}");
outputFilePath = Path.Combine(outputDirectory, $"recording_{timestamp}.mp4");
capturedFrames = new List<string>();
frameCount = 0;
// Check if FFmpeg is available
ffmpegPath = FindFfmpeg();
if (ffmpegPath == null)
{
Console.WriteLine("FFmpeg not found. Screen recording will be disabled.");
Console.WriteLine("To enable video recording, install FFmpeg: https://ffmpeg.org/download.html");
}
}
/// <summary>
/// Gets a value indicating whether screen recording is available (FFmpeg found).
/// </summary>
public bool IsAvailable => ffmpegPath != null;
/// <summary>
/// Starts recording the screen.
/// </summary>
/// <returns>A task representing the asynchronous operation.</returns>
public async Task StartRecordingAsync()
{
await recordingLock.WaitAsync();
try
{
if (isRecording || !IsAvailable)
{
return;
}
// Create frames directory
Directory.CreateDirectory(framesDirectory);
recordingCancellation = new CancellationTokenSource();
isRecording = true;
recordingStopwatch.Start();
// Start the recording task
recordingTask = Task.Run(() => RecordFrames(recordingCancellation.Token));
Console.WriteLine($"Started screen recording at {TargetFps} FPS");
}
catch (Exception ex)
{
Console.WriteLine($"Failed to start recording: {ex.Message}");
isRecording = false;
}
finally
{
recordingLock.Release();
}
}
/// <summary>
/// Stops recording and encodes video.
/// </summary>
/// <returns>A task representing the asynchronous operation.</returns>
public async Task StopRecordingAsync()
{
await recordingLock.WaitAsync();
try
{
if (!isRecording || recordingCancellation == null)
{
return;
}
// Signal cancellation
recordingCancellation.Cancel();
// Wait for recording task to complete
if (recordingTask != null)
{
await recordingTask;
}
recordingStopwatch.Stop();
isRecording = false;
double duration = recordingStopwatch.Elapsed.TotalSeconds;
Console.WriteLine($"Recording stopped. Captured {capturedFrames.Count} frames in {duration:F2} seconds");
// Encode to video
await EncodeToVideoAsync();
}
catch (Exception ex)
{
Console.WriteLine($"Error stopping recording: {ex.Message}");
}
finally
{
Cleanup();
recordingLock.Release();
}
}
/// <summary>
/// Records frames from the screen.
/// </summary>
private void RecordFrames(CancellationToken cancellationToken)
{
try
{
int frameInterval = 1000 / TargetFps;
var frameTimer = Stopwatch.StartNew();
while (!cancellationToken.IsCancellationRequested)
{
var frameStart = frameTimer.ElapsedMilliseconds;
try
{
CaptureFrame();
}
catch (Exception ex)
{
Console.WriteLine($"Error capturing frame: {ex.Message}");
}
// Sleep for remaining time to maintain target FPS
var frameTime = frameTimer.ElapsedMilliseconds - frameStart;
var sleepTime = Math.Max(0, frameInterval - (int)frameTime);
if (sleepTime > 0)
{
Thread.Sleep(sleepTime);
}
}
}
catch (OperationCanceledException)
{
// Expected when stopping
}
catch (Exception ex)
{
Console.WriteLine($"Error during recording: {ex.Message}");
}
}
/// <summary>
/// Captures a single frame.
/// </summary>
private void CaptureFrame()
{
IntPtr hdc = GetDC(IntPtr.Zero);
int screenWidth = GetDeviceCaps(hdc, DESKTOPHORZRES);
int screenHeight = GetDeviceCaps(hdc, DESKTOPVERTRES);
ReleaseDC(IntPtr.Zero, hdc);
Rectangle bounds = new Rectangle(0, 0, screenWidth, screenHeight);
using (Bitmap bitmap = new Bitmap(bounds.Width, bounds.Height, PixelFormat.Format24bppRgb))
{
using (Graphics g = Graphics.FromImage(bitmap))
{
g.CopyFromScreen(bounds.Location, Point.Empty, bounds.Size);
ScreenCapture.CURSORINFO cursorInfo;
cursorInfo.CbSize = Marshal.SizeOf<ScreenCapture.CURSORINFO>();
if (GetCursorInfo(out cursorInfo) && cursorInfo.Flags == CURSORSHOWING)
{
IntPtr hdcDest = g.GetHdc();
DrawIconEx(hdcDest, cursorInfo.PTScreenPos.X, cursorInfo.PTScreenPos.Y, cursorInfo.HCursor, 0, 0, 0, IntPtr.Zero, DINORMAL);
g.ReleaseHdc(hdcDest);
}
}
string framePath = Path.Combine(framesDirectory, $"frame_{frameCount:D6}.jpg");
bitmap.Save(framePath, ImageFormat.Jpeg);
capturedFrames.Add(framePath);
frameCount++;
}
}
/// <summary>
/// Encodes captured frames to video using ffmpeg.
/// </summary>
private async Task EncodeToVideoAsync()
{
if (capturedFrames.Count == 0)
{
Console.WriteLine("No frames captured");
return;
}
try
{
// Build ffmpeg command with proper non-interactive flags
string inputPattern = Path.Combine(framesDirectory, "frame_%06d.jpg");
// -y: overwrite without asking
// -nostdin: disable interaction
// -loglevel error: only show errors
// -stats: show encoding progress
string args = $"-y -nostdin -loglevel error -stats -framerate {TargetFps} -i \"{inputPattern}\" -c:v libx264 -pix_fmt yuv420p -crf 23 \"{outputFilePath}\"";
Console.WriteLine($"Encoding {capturedFrames.Count} frames to video...");
var startInfo = new ProcessStartInfo
{
FileName = ffmpegPath!,
Arguments = args,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
RedirectStandardInput = true, // Important: redirect stdin to prevent hanging
CreateNoWindow = true,
};
using var process = Process.Start(startInfo);
if (process != null)
{
// Close stdin immediately to ensure FFmpeg doesn't wait for input
process.StandardInput.Close();
// Read output streams asynchronously to prevent deadlock
var outputTask = process.StandardOutput.ReadToEndAsync();
var errorTask = process.StandardError.ReadToEndAsync();
// Wait for process to exit
await process.WaitForExitAsync();
// Get the output
string stdout = await outputTask;
string stderr = await errorTask;
if (process.ExitCode == 0 && File.Exists(outputFilePath))
{
var fileInfo = new FileInfo(outputFilePath);
Console.WriteLine($"Video created: {outputFilePath} ({fileInfo.Length / 1024 / 1024:F1} MB)");
}
else
{
Console.WriteLine($"FFmpeg encoding failed with exit code {process.ExitCode}");
if (!string.IsNullOrWhiteSpace(stderr))
{
Console.WriteLine($"FFmpeg error: {stderr}");
}
}
}
}
catch (Exception ex)
{
Console.WriteLine($"Error encoding video: {ex.Message}");
}
}
/// <summary>
/// Finds ffmpeg executable.
/// </summary>
private static string? FindFfmpeg()
{
// Check if ffmpeg is in PATH
var pathDirs = Environment.GetEnvironmentVariable("PATH")?.Split(Path.PathSeparator) ?? Array.Empty<string>();
foreach (var dir in pathDirs)
{
var ffmpegPath = Path.Combine(dir, "ffmpeg.exe");
if (File.Exists(ffmpegPath))
{
return ffmpegPath;
}
}
// Check common installation locations
var commonPaths = new[]
{
@"C:\.tools\ffmpeg\bin\ffmpeg.exe",
@"C:\ffmpeg\bin\ffmpeg.exe",
@"C:\Program Files\ffmpeg\bin\ffmpeg.exe",
@"C:\Program Files (x86)\ffmpeg\bin\ffmpeg.exe",
@$"{Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)}\Microsoft\WinGet\Links\ffmpeg.exe",
};
foreach (var path in commonPaths)
{
if (File.Exists(path))
{
return path;
}
}
return null;
}
/// <summary>
/// Gets the path to the recorded video file.
/// </summary>
public string OutputFilePath => outputFilePath;
/// <summary>
/// Gets the directory containing recordings.
/// </summary>
public string OutputDirectory => outputDirectory;
/// <summary>
/// Cleans up resources.
/// </summary>
private void Cleanup()
{
recordingCancellation?.Dispose();
recordingCancellation = null;
recordingTask = null;
// Clean up frames directory if it exists
try
{
if (Directory.Exists(framesDirectory))
{
Directory.Delete(framesDirectory, true);
}
}
catch (Exception ex)
{
Console.WriteLine($"Failed to cleanup frames directory: {ex.Message}");
}
}
/// <summary>
/// Disposes resources.
/// </summary>
public void Dispose()
{
if (isRecording)
{
StopRecordingAsync().GetAwaiter().GetResult();
}
Cleanup();
recordingLock.Dispose();
GC.SuppressFinalize(this);
}
}
}

View File

@@ -130,9 +130,13 @@ namespace Microsoft.PowerToys.UITest
/// </summary>
/// <param name="appPath">The path to the application executable.</param>
/// <param name="args">Optional command line arguments to pass to the application.</param>
public void StartExe(string appPath, string[]? args = null)
public void StartExe(string appPath, string[]? args = null, string? enableModules = null)
{
var opts = new AppiumOptions();
if (!string.IsNullOrEmpty(enableModules))
{
opts.AddAdditionalCapability("enableModules", enableModules);
}
if (scope == PowerToysModule.PowerToysSettings)
{
@@ -169,27 +173,66 @@ namespace Microsoft.PowerToys.UITest
private void TryLaunchPowerToysSettings(AppiumOptions opts)
{
try
if (opts.ToCapabilities().HasCapability("enableModules"))
{
var runnerProcessInfo = new ProcessStartInfo
var modulesString = (string)opts.ToCapabilities().GetCapability("enableModules");
var modulesArray = modulesString.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
SettingsConfigHelper.ConfigureGlobalModuleSettings(modulesArray);
}
else
{
SettingsConfigHelper.ConfigureGlobalModuleSettings();
}
const int maxTries = 3;
const int delayMs = 5000;
const int maxRetries = 3;
for (int tryCount = 1; tryCount <= maxTries; tryCount++)
{
try
{
FileName = locationPath + runnerPath,
Verb = "runas",
Arguments = "--open-settings",
};
var runnerProcessInfo = new ProcessStartInfo
{
FileName = locationPath + runnerPath,
Verb = "runas",
Arguments = "--open-settings",
};
ExitExe(runnerProcessInfo.FileName);
runner = Process.Start(runnerProcessInfo);
ExitExe(runnerProcessInfo.FileName);
WaitForWindowAndSetCapability(opts, "PowerToys Settings", 5000, 5);
// Verify process was killed
string exeName = Path.GetFileNameWithoutExtension(runnerProcessInfo.FileName);
var remainingProcesses = Process.GetProcessesByName(exeName);
// Exit CmdPal UI before launching new process if use installer for test
ExitExeByName("Microsoft.CmdPal.UI");
}
catch (Exception ex)
{
throw new InvalidOperationException($"Failed to launch PowerToys Settings: {ex.Message}", ex);
runner = Process.Start(runnerProcessInfo);
if (WaitForWindowAndSetCapability(opts, "PowerToys Settings", delayMs, maxRetries))
{
// Exit CmdPal UI before launching new process if use installer for test
ExitExeByName("Microsoft.CmdPal.UI");
return;
}
// Window not found, kill all PowerToys processes and retry
if (tryCount < maxTries)
{
KillPowerToysProcesses();
}
}
catch (Exception ex)
{
if (tryCount == maxTries)
{
throw new InvalidOperationException($"Failed to launch PowerToys Settings after {maxTries} attempts: {ex.Message}", ex);
}
// Kill processes and retry
KillPowerToysProcesses();
}
}
throw new InvalidOperationException($"Failed to launch PowerToys Settings: Window not found after {maxTries} attempts.");
}
private void TryLaunchCommandPalette(AppiumOptions opts)
@@ -211,7 +254,10 @@ namespace Microsoft.PowerToys.UITest
var process = Process.Start(processStartInfo);
process?.WaitForExit();
WaitForWindowAndSetCapability(opts, "Command Palette", 5000, 10);
if (!WaitForWindowAndSetCapability(opts, "Command Palette", 5000, 10))
{
throw new TimeoutException("Failed to find Command Palette window after multiple attempts.");
}
}
catch (Exception ex)
{
@@ -219,7 +265,7 @@ namespace Microsoft.PowerToys.UITest
}
}
private void WaitForWindowAndSetCapability(AppiumOptions opts, string windowName, int delayMs, int maxRetries)
private bool WaitForWindowAndSetCapability(AppiumOptions opts, string windowName, int delayMs, int maxRetries)
{
for (int attempt = 1; attempt <= maxRetries; attempt++)
{
@@ -230,18 +276,16 @@ namespace Microsoft.PowerToys.UITest
{
var hexHwnd = window[0].HWnd.ToString("x");
opts.AddAdditionalCapability("appTopLevelWindow", hexHwnd);
return;
return true;
}
if (attempt < maxRetries)
{
Thread.Sleep(delayMs);
}
else
{
throw new TimeoutException($"Failed to find {windowName} window after multiple attempts.");
}
}
return false;
}
/// <summary>
@@ -292,17 +336,17 @@ namespace Microsoft.PowerToys.UITest
catch (Exception ex)
{
// Handle exceptions if needed
Debug.WriteLine($"Exception during Cleanup: {ex.Message}");
Console.WriteLine($"Exception during Cleanup: {ex.Message}");
}
}
/// <summary>
/// Restarts now exe and takes control of it.
/// </summary>
public void RestartScopeExe()
public void RestartScopeExe(string? enableModules = null)
{
ExitScopeExe();
StartExe(locationPath + sessionPath, this.commandLineArgs);
StartExe(locationPath + sessionPath, commandLineArgs, enableModules);
}
public WindowsDriver<WindowsElement> GetRoot()
@@ -327,5 +371,31 @@ namespace Microsoft.PowerToys.UITest
this.ExitExe(winAppDriverProcessInfo.FileName);
SessionHelper.appDriver = Process.Start(winAppDriverProcessInfo);
}
private void KillPowerToysProcesses()
{
var powerToysProcessNames = new[] { "PowerToys", "Microsoft.CmdPal.UI" };
foreach (var processName in powerToysProcessNames)
{
try
{
var processes = Process.GetProcessesByName(processName);
foreach (var process in processes)
{
process.Kill();
process.WaitForExit();
}
// Verify processes are actually gone
var remainingProcesses = Process.GetProcessesByName(processName);
}
catch (Exception ex)
{
Console.WriteLine($"[KillPowerToysProcesses] Failed to kill process {processName}: {ex.Message}");
}
}
}
}
}

View File

@@ -26,14 +26,13 @@ namespace Microsoft.PowerToys.UITest
/// <summary>
/// Configures global PowerToys settings to enable only specified modules and disable all others.
/// </summary>
/// <param name="modulesToEnable">Array of module names to enable (e.g., "Peek", "FancyZones"). All other modules will be disabled.</param>
/// <exception cref="ArgumentNullException">Thrown when modulesToEnable is null.</exception>
/// <param name="modulesToEnable">Array of module names to enable (e.g., "Peek", "FancyZones"). All other modules will be disabled. If null or empty, all modules will be disabled.</param>
/// <exception cref="InvalidOperationException">Thrown when settings file operations fail.</exception>
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "This is test code and will not be trimmed")]
[UnconditionalSuppressMessage("AOT", "IL3050", Justification = "This is test code and will not be AOT compiled")]
public static void ConfigureGlobalModuleSettings(params string[] modulesToEnable)
public static void ConfigureGlobalModuleSettings(params string[]? modulesToEnable)
{
ArgumentNullException.ThrowIfNull(modulesToEnable);
modulesToEnable ??= Array.Empty<string>();
try
{

View File

@@ -29,6 +29,8 @@ namespace Microsoft.PowerToys.UITest
public string? ScreenshotDirectory { get; set; }
public string? RecordingDirectory { get; set; }
public static MonitorInfoData.ParamsWrapper MonitorInfoData { get; set; } = new MonitorInfoData.ParamsWrapper() { Monitors = new List<MonitorInfoData.MonitorInfoDataWrapper>() };
private readonly PowerToysModule scope;
@@ -36,6 +38,7 @@ namespace Microsoft.PowerToys.UITest
private readonly string[]? commandLineArgs;
private SessionHelper? sessionHelper;
private System.Threading.Timer? screenshotTimer;
private ScreenRecording? screenRecording;
public UITestBase(PowerToysModule scope = PowerToysModule.PowerToysSettings, WindowSize size = WindowSize.UnSpecified, string[]? commandLineArgs = null)
{
@@ -65,12 +68,35 @@ namespace Microsoft.PowerToys.UITest
CloseOtherApplications();
if (IsInPipeline)
{
ScreenshotDirectory = Path.Combine(this.TestContext.TestResultsDirectory ?? string.Empty, "UITestScreenshots_" + Guid.NewGuid().ToString());
string baseDirectory = this.TestContext.TestResultsDirectory ?? string.Empty;
ScreenshotDirectory = Path.Combine(baseDirectory, "UITestScreenshots_" + Guid.NewGuid().ToString());
Directory.CreateDirectory(ScreenshotDirectory);
RecordingDirectory = Path.Combine(baseDirectory, "UITestRecordings_" + Guid.NewGuid().ToString());
Directory.CreateDirectory(RecordingDirectory);
// Take screenshot every 1 second
screenshotTimer = new System.Threading.Timer(ScreenCapture.TimerCallback, ScreenshotDirectory, TimeSpan.Zero, TimeSpan.FromMilliseconds(1000));
// Start screen recording (requires FFmpeg)
try
{
screenRecording = new ScreenRecording(RecordingDirectory);
if (screenRecording.IsAvailable)
{
_ = screenRecording.StartRecordingAsync();
}
else
{
screenRecording = null;
}
}
catch (Exception ex)
{
Console.WriteLine($"Failed to start screen recording: {ex.Message}");
screenRecording = null;
}
// Escape Popups before starting
System.Windows.Forms.SendKeys.SendWait("{ESC}");
}
@@ -88,15 +114,36 @@ namespace Microsoft.PowerToys.UITest
if (IsInPipeline)
{
screenshotTimer?.Change(Timeout.Infinite, Timeout.Infinite);
Dispose();
// Stop screen recording
if (screenRecording != null)
{
try
{
screenRecording.StopRecordingAsync().GetAwaiter().GetResult();
}
catch (Exception ex)
{
Console.WriteLine($"Failed to stop screen recording: {ex.Message}");
}
}
if (TestContext.CurrentTestOutcome is UnitTestOutcome.Failed
or UnitTestOutcome.Error
or UnitTestOutcome.Unknown)
{
Task.Delay(1000).Wait();
AddScreenShotsToTestResultsDirectory();
AddRecordingsToTestResultsDirectory();
AddLogFilesToTestResultsDirectory();
}
else
{
// Clean up recording if test passed
CleanupRecordingDirectory();
}
Dispose();
}
this.Session.Cleanup();
@@ -106,6 +153,7 @@ namespace Microsoft.PowerToys.UITest
public void Dispose()
{
screenshotTimer?.Dispose();
screenRecording?.Dispose();
GC.SuppressFinalize(this);
}
@@ -600,6 +648,47 @@ namespace Microsoft.PowerToys.UITest
}
}
/// <summary>
/// Adds screen recordings to test results directory when test fails.
/// </summary>
protected void AddRecordingsToTestResultsDirectory()
{
if (RecordingDirectory != null && Directory.Exists(RecordingDirectory))
{
// Add video files (MP4)
var videoFiles = Directory.GetFiles(RecordingDirectory, "*.mp4");
foreach (string file in videoFiles)
{
this.TestContext.AddResultFile(file);
var fileInfo = new FileInfo(file);
Console.WriteLine($"Added video recording: {Path.GetFileName(file)} ({fileInfo.Length / 1024 / 1024:F1} MB)");
}
if (videoFiles.Length == 0)
{
Console.WriteLine("No video recording available (FFmpeg not found). Screenshots are still captured.");
}
}
}
/// <summary>
/// Cleans up recording directory when test passes.
/// </summary>
private void CleanupRecordingDirectory()
{
if (RecordingDirectory != null && Directory.Exists(RecordingDirectory))
{
try
{
Directory.Delete(RecordingDirectory, true);
}
catch (Exception ex)
{
Console.WriteLine($"Failed to cleanup recording directory: {ex.Message}");
}
}
}
/// <summary>
/// Copies PowerToys log files to test results directory when test fails.
/// Renames files to include the directory structure after \PowerToys.
@@ -689,11 +778,11 @@ namespace Microsoft.PowerToys.UITest
/// <summary>
/// Restart scope exe.
/// </summary>
public void RestartScopeExe()
public Session RestartScopeExe(string? enableModules = null)
{
this.sessionHelper!.RestartScopeExe();
this.sessionHelper!.RestartScopeExe(enableModules);
this.Session = new Session(this.sessionHelper.GetRoot(), this.sessionHelper.GetDriver(), this.scope, this.size);
return;
return Session;
}
/// <summary>

View File

@@ -70,6 +70,7 @@ namespace powertoys_gpo
const std::wstring POLICY_CONFIGURE_ENABLED_QOI_THUMBNAILS = L"ConfigureEnabledUtilityFileExplorerQOIThumbnails";
const std::wstring POLICY_CONFIGURE_ENABLED_NEWPLUS = L"ConfigureEnabledUtilityNewPlus";
const std::wstring POLICY_CONFIGURE_ENABLED_WORKSPACES = L"ConfigureEnabledUtilityWorkspaces";
const std::wstring POLICY_CONFIGURE_ENABLED_KEYSTROKE_OVERLAY = L"ConfigureEnabledUtilityKeystrokeOverlay";
// The registry value names for PowerToys installer and update policies.
const std::wstring POLICY_DISABLE_PER_USER_INSTALLATION = L"PerUserInstallationDisabled";
@@ -475,6 +476,11 @@ namespace powertoys_gpo
return getUtilityEnabledValue(POLICY_CONFIGURE_ENABLED_PEEK);
}
inline gpo_rule_configured_t getConfiguredKeystrokeOverlayEnabledValue()
{
return getUtilityEnabledValue(POLICY_CONFIGURE_ENABLED_KEYSTROKE_OVERLAY);
}
inline gpo_rule_configured_t getConfiguredRegistryPreviewEnabledValue()
{
return getUtilityEnabledValue(POLICY_CONFIGURE_ENABLED_REGISTRY_PREVIEW);

View File

@@ -50,6 +50,7 @@ public sealed class SettingsResource : BaseResource
{ nameof(ModuleType.Hosts), CreateModuleFunctionData<HostsSettings> },
{ nameof(ModuleType.ImageResizer), CreateModuleFunctionData<ImageResizerSettings> },
{ nameof(ModuleType.KeyboardManager), CreateModuleFunctionData<KeyboardManagerSettings> },
{ nameof(ModuleType.KeystrokeOverlay), CreateModuleFunctionData<KeystrokeOverlaySettings> },
{ nameof(ModuleType.MouseHighlighter), CreateModuleFunctionData<MouseHighlighterSettings> },
{ nameof(ModuleType.MouseJump), CreateModuleFunctionData<MouseJumpSettings> },
{ nameof(ModuleType.MousePointerCrosshairs), CreateModuleFunctionData<MousePointerCrosshairsSettings> },

View File

@@ -198,20 +198,14 @@ namespace AdvancedPaste.Pages
}
}
private async void ClipboardHistory_ItemInvoked(ItemsView sender, ItemsViewItemInvokedEventArgs args)
private void ClipboardHistory_ItemInvoked(ItemsView sender, ItemsViewItemInvokedEventArgs args)
{
if (args.InvokedItem is ClipboardItem item)
if (args.InvokedItem is ClipboardItem item && item.Item is not null)
{
PowerToysTelemetry.Log.WriteEvent(new Telemetry.AdvancedPasteClipboardItemClicked());
if (!string.IsNullOrEmpty(item.Content))
{
ClipboardHelper.SetTextContent(item.Content);
}
else if (item.Image is not null)
{
RandomAccessStreamReference image = await item.Item.Content.GetBitmapAsync();
ClipboardHelper.SetImageContent(image);
}
// Use SetHistoryItemAsContent to set the clipboard content without creating a new history entry
Clipboard.SetHistoryItemAsContent(item.Item);
}
}
}

View File

@@ -0,0 +1,6 @@
{
"files.associations": {
"*.embeddedhtml": "html",
"xstring": "cpp"
}
}

View File

@@ -0,0 +1 @@
# Ignore all Windows executable files

View File

@@ -0,0 +1,86 @@
// Batcher.cpp
// Worker thread that batches and sends keystroke data through IPC.
#include "pch.h"
#include "Batcher.h"
#include <sstream>
// Escapes a string for JSON inclusion
static std::string Escape(const std::string &s)
{
std::string o;
o.reserve(s.size() + 4);
for (char c : s)
{
switch (c)
{
case '"':
o += "\\\"";
break;
case '\\':
o += "\\\\";
break;
case '\n':
o += "\\n";
break;
case '\r':
o += "\\r";
break;
case '\t':
o += "\\t";
break;
default:
o += c;
}
}
return o;
}
// Starts the batcher thread
void Batcher::Start()
{
if (_run.exchange(true))
return;
_t = std::thread([this]
{
std::vector<KeystrokeEvent> batch; batch.reserve(32);
while (_run.load()) {
// drain up to 32 events
KeystrokeEvent ev;
batch.clear();
for (int i=0; i<32 && _q.try_pop(ev); ++i) batch.push_back(ev);
if (!batch.empty()) {
std::ostringstream oss;
oss << R"({"schema":1,"events":[)";
for (size_t i=0; i<batch.size(); ++i) {
const auto& e = batch[i];
oss << R"({"t":")" << (e.type==KeystrokeEvent::Type::Down?"down":e.type==KeystrokeEvent::Type::Up?"up":"char") << R"(",)"
<< R"("vk":)" << e.vk << ','
<< R"("text":")" << (e.ch ? Escape(std::string{ static_cast<char>(e.ch) }) : "") << R"(",)"
<< R"("mods":[)" << (e.mods[0]?"\"Ctrl\"":"")
<< ((e.mods[0]&&e.mods[1])?",":"")
<< (e.mods[1]?"\"Alt\"":"")
<< (((e.mods[0]||e.mods[1])&&e.mods[2])?",":"")
<< (e.mods[2]?"\"Shift\"":"")
<< (((e.mods[0]||e.mods[1]||e.mods[2])&&e.mods[3])?",":"")
<< (e.mods[3]?"\"Win\"":"")
<< R"(],)"
<< R"("ts":)" << (e.ts_micros/1'000'000.0)
<< "}";
if (i+1<batch.size()) oss << ",";
}
oss << "]}";
_pipe.EnsureClient();
_pipe.SendFrame(oss.str());
}
Sleep(8); // ~120Hz batching
} });
}
void Batcher::Stop()
{
if (!_run.exchange(false))
return;
if (_t.joinable())
_t.join();
}

View File

@@ -0,0 +1,22 @@
// Batcher.h
// Worker thread that batches and sends keystroke data through Named Pipe server.
#pragma once
#include "EventQueue.h"
#include "KeystrokeEvent.h"
#include "PipeServer.h"
#include <thread>
#include <atomic>
class Batcher
{
public:
Batcher(SpscRing<KeystrokeEvent, 1024> &q) : _q(q) {}
void Start();
void Stop();
private:
SpscRing<KeystrokeEvent, 1024> &_q;
PipeServer _pipe;
std::atomic<bool> _run{false};
std::thread _t;
};

View File

@@ -0,0 +1,36 @@
// EventQueue.h
// Custom SPSC queue to store KeyStrokeEvent objects between producer and consumer threads.
#pragma once
#include <atomic> // atomic type for head and tail
#include <vector>
template <typename T, size_t N>
class SpscRing
{
public:
// Attempts to push an item into the queue. Returns false if the queue is full.
bool try_push(const T &v)
{
auto head = _head.load(std::memory_order_relaxed); // maybe fix pointer types later?
auto next = (head + 1) % N;
if (next == _tail.load(std::memory_order_acquire))
return false; // full case
_buf[head] = v;
_head.store(next, std::memory_order_release);
return true;
}
// Attempts to pop an item from the queue. Returns false if the queue is empty.
bool try_pop(T &out)
{
auto tail = _tail.load(std::memory_order_relaxed);
if (tail == _head.load(std::memory_order_acquire))
return false; // empty case
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}, _tail{0};
};

View File

@@ -0,0 +1,135 @@
#include "pch.h"
#include <iostream>
#include <windows.h>
#include <map>
#include <deque>
#include <cstdint>
#include <array>
#include "KeystrokeEvent.h"
#include "EventQueue.h"
#include "Batcher.h"
// Old compilation command saved for reference
// Compilation: x86_64-w64-mingw32-g++ KeyboardListener.cpp -o KeyboardHookTest.exe -static -luser32 -lgdi32 (need to compile with "static" because of lack of DLL's)
// Global handle for the hook
static HHOOK g_hook = nullptr;
static SpscRing<KeystrokeEvent, 1024> g_q;
// Batcher worker (drains g_q -> JSON -> named pipe)
static Batcher g_batcher(g_q);
// Timestamping
static inline uint64_t now_micros() {
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);
}
// Modifier snapshot (Ctrl, Alt, Shift, Win)
static inline std::array<bool,4> snapshot_mods() {
auto down = [](int vk){ return (GetKeyState(vk) & 0x8000) != 0; };
return std::array<bool,4>{
down(VK_CONTROL), // Ctrl
down(VK_MENU), // Alt
down(VK_SHIFT), // Shift
(down(VK_LWIN) || down(VK_RWIN)) // Win
};
}
// Translate vk to printable character
static inline char32_t vk_to_char(UINT vk, UINT sc) {
BYTE keystate[256];
if (!GetKeyboardState(keystate)) return 0;
WCHAR buf[4] = {0};
HKL layout = GetKeyboardLayout(0);
// Note: ToUnicodeEx can have side-effects for dead keys, this is "good enough" for an overlay.
int rc = ToUnicodeEx(vk, sc, keystate, buf, 4, 0, layout);
if (rc <= 0) return 0;
if (!iswprint(buf[0])) return 0;
return static_cast<char32_t>(buf[0]);
}
// Push helpers
static inline void emit_down(UINT vk, UINT sc) {
KeystrokeEvent e{};
e.type = KeystrokeEvent::Type::Down;
e.vk = static_cast<uint16_t>(vk);
e.ch = 0;
e.mods = snapshot_mods();
e.ts_micros = now_micros();
g_q.try_push(e);
// Also emit a Char if there is a printable character for this Down
if (char32_t ch = vk_to_char(vk, sc)) {
KeystrokeEvent c = e;
c.type = KeystrokeEvent::Type::Char;
c.ch = ch;
g_q.try_push(c);
}
}
static inline void emit_up(UINT vk) {
KeystrokeEvent e{};
e.type = KeystrokeEvent::Type::Up;
e.vk = static_cast<uint16_t>(vk);
e.ch = 0;
e.mods = snapshot_mods();
e.ts_micros = now_micros();
g_q.try_push(e);
}
// LL keyboard hook callback
static LRESULT CALLBACK LowLevelKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) {
if (nCode == HC_ACTION) {
const KBDLLHOOKSTRUCT* p = reinterpret_cast<KBDLLHOOKSTRUCT*>(lParam);
switch (wParam) {
case WM_KEYDOWN:
case WM_SYSKEYDOWN:
emit_down(p->vkCode, p->scanCode);
break;
case WM_KEYUP:
case WM_SYSKEYUP:
emit_up(p->vkCode);
break;
}
}
return CallNextHookEx(nullptr, nCode, wParam, lParam);
}
// Install/uninstall hook
static void SetGlobalHook() {
g_hook = SetWindowsHookExW(WH_KEYBOARD_LL, LowLevelKeyboardProc, GetModuleHandleW(nullptr), 0);
}
static void UnsetGlobalHook() {
if (g_hook) { UnhookWindowsHookEx(g_hook); g_hook = nullptr; }
}
// Main
int main() {
// Start the batcher FIRST so frames have somewhere to go
g_batcher.Start();
SetGlobalHook();
if (!g_hook) {
// If hook fails, stop batcher and exit
g_batcher.Stop();
return 1;
}
// Required message loop for WH_KEYBOARD_LL owner thread
MSG msg;
while (GetMessageW(&msg, nullptr, 0, 0)) {
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
UnsetGlobalHook();
g_batcher.Stop();
return 0;
}

View File

@@ -0,0 +1,23 @@
// KeystrokeEvent.h
// Contains the definition of the KeystrokeEvent struct used to represent keyboard events.
#pragma once // compiled once per build
#include <cstdint>
#include <array>
#include <windows.h>
struct KeystrokeEvent
{
enum class Type : uint8_t
{
Down,
Up,
Char
} type;
DWORD vk; // virtual key code (DWORD type)
char32_t ch; // Stores actual character being pressed. Skip for now. 0 if non-printable
std::array<bool, 4> mods; // Ctrl, Alt, Shift, Win. Read from getkeyboard state and one other method (in hook).
uint64_t ts_micros; // Timestamp in microseconds. Monotonic or UTC micros
};
// Note, VKCodes don't distinguish between key cases (A vs a), therefore mod keys
// need to be packaged in KeystrokeEvent in order to aid interpretation.

View File

@@ -0,0 +1,102 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!-- Import CppWinRT props if available -->
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props"
Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" />
<!-- Global project settings -->
<PropertyGroup Label="Globals">
<VCProjectVersion>15.0</VCProjectVersion>
<ProjectGuid>{A1B2C3D4-E5F6-4321-8765-123456789ABC}</ProjectGuid>
<Keyword>Win32Proj</Keyword>
<RootNamespace>KeystrokeOverlay</RootNamespace>
<ProjectName>KeystrokeOverlayKeyboardService</ProjectName>
</PropertyGroup>
<!-- Load VS default props -->
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<!-- THIS IS THE IMPORTANT CHANGE -->
<PropertyGroup Label="Configuration">
<ConfigurationType>Application</ConfigurationType> <!-- NOW BUILDS AN EXE -->
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<!-- User props -->
<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" />
<!-- Output directory — EXACTLY where your UI exe lives -->
<PropertyGroup>
<OutDir>$(SolutionDir)x64\$(Configuration)\WinUI3Apps\</OutDir>
<IntDir>$(ProjectDir)Intermediate\$(Platform)\$(Configuration)\</IntDir>
<TargetName>PowerToys.KeystrokeOverlayKeystrokeServer</TargetName>
<TargetExt>.exe</TargetExt>
</PropertyGroup>
<!-- Compiler configuration -->
<ItemDefinitionGroup>
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<Optimization>MaxSpeed</Optimization>
<PreprocessorDefinitions>
WIN32;_WINDOWS;%(PreprocessorDefinitions)
</PreprocessorDefinitions>
<AdditionalIncludeDirectories>
$(ProjectDir);
$(ProjectDir)..\..\..\common\inc;
$(ProjectDir)..\..\..\common\Telemetry;
$(ProjectDir)..\..\..;
%(AdditionalIncludeDirectories)
</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<AdditionalDependencies>
user32.lib;
gdi32.lib;
kernel32.lib;
advapi32.lib;
%(AdditionalDependencies)
</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<!-- Header files -->
<ItemGroup>
<ClInclude Include="pch.h" />
<ClInclude Include="Batcher.h" />
<ClInclude Include="EventQueue.h" />
<ClInclude Include="KeystrokeEvent.h" />
<ClInclude Include="Pipeserver.h" />
</ItemGroup>
<!-- Source files -->
<ItemGroup>
<ClCompile Include="Batcher.cpp" />
<ClCompile Include="Pipeserver.cpp" />
<ClCompile Include="KeyboardListener.cpp" /> <!-- ⭐ MAIN() HERE -->
<ClCompile Include="pch.cpp">
<PrecompiledHeader>Create</PrecompiledHeader>
</ClCompile>
</ItemGroup>
<!-- Standard VS C++ targets -->
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<!-- spdlog integration -->
<Import Project="..\..\..\..\deps\spdlog.props" />
<!-- No copy target needed — EXE is built directly into WinUI3Apps -->
</Project>

View File

@@ -0,0 +1,71 @@
// PipeServer.cpp
// Administrates named pipes server for IPC communications.
#include "pch.h"
#include "PipeServer.h" // relies on named pipes api through windows.h
PipeServer::PipeServer(const wchar_t *name) : _name(name) {} // Constructor
PipeServer::~PipeServer() { Close(); } // Destructor, calls Close()
// Check PipeServer.md for docs
bool PipeServer::CreateAndListen()
{
Close(); // close instance if already open
_hPipe = CreateNamedPipeW(
_name.c_str(),
PIPE_ACCESS_OUTBOUND | FILE_FLAG_FIRST_PIPE_INSTANCE,
PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT,
1, 1 << 16, 1 << 16, 0, nullptr);
if (_hPipe == INVALID_HANDLE_VALUE)
return false;
BOOL ok = ConnectNamedPipe(_hPipe, nullptr) ? TRUE : (GetLastError() == ERROR_PIPE_CONNECTED);
return ok == TRUE;
}
// If no client is connected, wait for one.
// Allows for worker thread to ensure a connection is present before sending stuff.
bool PipeServer::EnsureClient()
{
if (_hPipe != INVALID_HANDLE_VALUE)
return true;
return CreateAndListen();
}
// Sends a length-prefixed JSON frame through the pipe.
bool PipeServer::SendFrame(const std::string &json)
{
// if no client, try to create and listen
if (_hPipe == INVALID_HANDLE_VALUE && !CreateAndListen())
return false;
DWORD len = static_cast<DWORD>(json.size()); // gets size of payload (DWORD type for this case)
// prevent giant payloads (over 8 MiB this case)
if (len > 8 * 1024 * 1024)
return false;
DWORD wrote = 0;
if (!WriteFile(_hPipe, &len, sizeof(len), &wrote, nullptr) || wrote != sizeof(len)) // write length prefix
{
Close(); // on failed write, close (and reset) pipe
return false;
}
if (!WriteFile(_hPipe, json.data(), len, &wrote, nullptr) || wrote != len) // write payload
{
Close(); // on failed write, close (and reset) pipe
return false;
}
return true;
}
// Ensures any open handle is properly closed
void PipeServer::Close()
{
if (_hPipe != INVALID_HANDLE_VALUE)
{
FlushFileBuffers(_hPipe);
DisconnectNamedPipe(_hPipe);
CloseHandle(_hPipe);
_hPipe = INVALID_HANDLE_VALUE;
}
}

View File

@@ -0,0 +1,21 @@
// Pipeserver.h
// Administrates named pipes server for IPC communications.
#pragma once
#include <windows.h>
#include <string>
#include <vector>
class PipeServer
{
public:
explicit PipeServer(const wchar_t *name = LR"(\\.\pipe\KeystrokeOverlayPipe)");
~PipeServer(); // Destructor
bool EnsureClient(); // Accept client if none
bool SendFrame(const std::string &json); // [uint32 length][utf8]
private:
std::wstring _name;
HANDLE _hPipe = INVALID_HANDLE_VALUE;
bool CreateAndListen();
void Close();
};

View File

@@ -0,0 +1 @@
#include "pch.h"

View File

@@ -0,0 +1,13 @@
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
// C++ Standard Library Header Files used in your project
#include <vector>
#include <string>
#include <atomic>
#include <thread>
#include <mutex>
#include <sstream>
#include <array>
#include <algorithm>
#include <iostream>

View File

@@ -0,0 +1,32 @@
1 VERSIONINFO
FILEVERSION 0,1,0,0
PRODUCTVERSION 0,1,0,0
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
FILEFLAGS 0x1L
#else
FILEFLAGS 0x0L
#endif
FILEOS 0x40004L
FILETYPE 0x2L
FILESUBTYPE 0x0L
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904b0"
BEGIN
VALUE "CompanyName", "Company Name"
VALUE "FileDescription", "$projectname$ Module"
VALUE "FileVersion", "0.1.0.0"
VALUE "InternalName", "$projectname$"
VALUE "LegalCopyright", "Copyright (C) 2019 Company Name"
VALUE "OriginalFilename", "$projectname$.dll"
VALUE "ProductName", "$projectname$"
VALUE "ProductVersion", "0.1.0.0"
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1200
END
END

View File

@@ -0,0 +1,89 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" />
<PropertyGroup Label="Globals">
<VCProjectVersion>15.0</VCProjectVersion>
<ProjectGuid>{D66DAAF0-84FC-477D-A980-43370F4499C2}</ProjectGuid>
<Keyword>Win32Proj</Keyword>
<RootNamespace>KeystrokeOverlay</RootNamespace>
<ProjectName>KeystrokeOverlayModuleInterface</ProjectName>
<WindowsTargetPlatformVersion>10.0.26100.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
</PropertyGroup>
<PropertyGroup Label="Configuration" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<PlatformToolset>v143</PlatformToolset>
</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" />
<PropertyGroup>
<OutDir>..\..\..\..\$(Platform)\$(Configuration)\WinUI3Apps</OutDir>
</PropertyGroup>
<PropertyGroup>
<TargetName>PowerToys.KeystrokeOverlayModuleInterface</TargetName>
</PropertyGroup>
<ItemDefinitionGroup>
<ClCompile>
<PreprocessorDefinitions>KEYSTROKEOVERLAY_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<!-- Includes 'KeystrokeOverlayKeyboardService' so we can include "Batcher.h" in dllmain.cpp -->
<AdditionalIncludeDirectories>..\KeystrokeOverlayKeyboardService;..\..\..\common\inc;..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<OutputFile>$(OutDir)$(TargetName)$(TargetExt)</OutputFile>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="pch.h" />
<ClInclude Include="resource.h" />
<ClInclude Include="trace.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="dllmain.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
</ClCompile>
<ClCompile Include="trace.cpp" />
</ItemGroup>
<ItemGroup>
<Image Include="..\KeystrokeOverlayXAML\Assets\Icon.ico" />
</ItemGroup>
<ItemGroup>
<!-- Link dependencies -->
<ProjectReference Include="..\..\..\common\logger\logger.vcxproj">
<Project>{d9b8fc84-322a-4f9f-bbb9-20915c47ddfd}</Project>
</ProjectReference>
<ProjectReference Include="..\..\..\common\SettingsAPI\SettingsAPI.vcxproj">
<Project>{6955446d-23f7-4023-9bb3-8657f904af99}</Project>
</ProjectReference>
<!-- Reference the Service Static Library created above -->
<ProjectReference Include="..\KeystrokeOverlayKeyboardService\KeystrokeOverlayKeyboardService.vcxproj">
<Project>{A1B2C3D4-E5F6-4321-8765-123456789ABC}</Project>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="KeystrokeOverlay.rc" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<Import Project="..\..\..\..\deps\spdlog.props" />
<ImportGroup Label="ExtensionTargets">
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" />
</ImportGroup>
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets'))" />
</Target>
</Project>

View File

@@ -0,0 +1,438 @@
#include "pch.h"
#include <interface/powertoy_module_interface.h>
#include <common/SettingsAPI/settings_helpers.h>
#include <common/hooks/LowlevelKeyboardEvent.h>
#include <common/hooks/WinHookEvent.h>
#include <common/SettingsAPI/settings_objects.h>
#include <common/utils/gpo.h>
#include <common/logger/logger.h>
#include <common/utils/process_path.h>
#include <common/utils/logger_helper.h>
#include "trace.h"
#include <iostream>
extern "C" IMAGE_DOS_HEADER __ImageBase;
BOOL APIENTRY DllMain(HMODULE /*hModule*/, DWORD ul_reason_for_call, LPVOID /*lpReserved*/)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
Trace::RegisterProvider();
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
break;
case DLL_PROCESS_DETACH:
Trace::UnregisterProvider();
break;
}
return TRUE;
}
// The PowerToy name that will be shown in the settings.
const static wchar_t* MODULE_NAME = L"Keystroke Overlay";
const static wchar_t* MODULE_KEY = L"KeystrokeOverlay";
// Add a description that will we shown in the module settings page.
const static wchar_t* MODULE_DESC = L"<no description>";
const static wchar_t* win_hook_event = L"win_hook_event";
// Note: low-level keyboard hook and batching are handled by the external
// KeystrokeServer.exe. The module no longer declares an internal event queue
// or batcher to avoid duplicate IPC servers.
// These are the properties shown in the Settings page.
struct ModuleSettings
{
// Add the PowerToy module properties with default values.
bool is_draggable = true;
int overlay_timeout = 3000;
int display_mode = 0; // NEW: 0=Standard, 1=Compact, etc. (Define your modes)
int text_size = 24;
int text_opacity = 100;
int bg_opacity = 50;
std::wstring text_color = L"#FFFFFF";
std::wstring bg_color = L"#000000";
PowerToysSettings::HotkeyObject switch_monitor_hotkey =
PowerToysSettings::HotkeyObject::from_settings(true, false, false, true, 0xBF); // Default: Win+Ctrl+/
} g_settings;
class KeystrokeOverlay : public PowertoyModuleIface
{
std::wstring app_name;
std::wstring app_key;
private:
// The PowerToy state.
bool m_enabled = false;
HANDLE m_hProcess = nullptr;
// Handle for the hidden keystroke server process started by this module
HANDLE m_hServerProcess = nullptr;
// Load initial settings from the persisted values.
void init_settings();
// Launch and terminate the Keystroke Overlay process.
// Launch and terminate the Keystroke Overlay process.
void launch_process()
{
Logger::trace(L"Launching Keystroke Overlay process");
unsigned long powertoys_pid = GetCurrentProcessId();
//
// Determine the folder where this module's DLL lives
//
WCHAR modulePath[MAX_PATH] = { 0 };
if (GetModuleFileNameW(reinterpret_cast<HMODULE>(&__ImageBase), modulePath, ARRAYSIZE(modulePath)) == 0)
{
Logger::error(L"GetModuleFileNameW failed — cannot launch KeystrokeOverlay");
return;
}
std::wstring folder = modulePath;
size_t pos = folder.find_last_of(L"\\/");
if (pos != std::wstring::npos)
folder.resize(pos + 1);
//
// Build absolute path to KeystrokeOverlay.exe
//
std::wstring exePath = folder + L"PowerToys.KeystrokeOverlay.exe";
std::wstring cmdLine = exePath + L" " + std::to_wstring(powertoys_pid);
Logger::trace(L"Launching KeystrokeOverlay UI from: " + exePath);
//
// Launch the UI application
//
STARTUPINFOW si{};
si.cb = sizeof(si);
PROCESS_INFORMATION pi{};
if (!CreateProcessW(
exePath.c_str(), // lpApplicationName
&cmdLine[0], // lpCommandLine (modifiable buffer)
nullptr, nullptr,
FALSE, // inherit handles
0, // flags
nullptr, nullptr,
&si, &pi))
{
DWORD err = GetLastError();
Logger::error(L"KeystrokeOverlay.exe failed to start with error: " + std::to_wstring(err));
return;
}
m_hProcess = pi.hProcess;
CloseHandle(pi.hThread);
Logger::trace(L"KeystrokeOverlay.exe started successfully");
}
void terminate_process()
{
if (m_hProcess)
{
Logger::trace(L"Terminating Keystroke Overlay process");
TerminateProcess(m_hProcess, 0);
CloseHandle(m_hProcess);
m_hProcess = nullptr;
}
if (m_hServerProcess)
{
Logger::trace(L"Terminating KeystrokeServer process");
TerminateProcess(m_hServerProcess, 0);
CloseHandle(m_hServerProcess);
m_hServerProcess = nullptr;
}
}
public:
// Constructor
KeystrokeOverlay()
{
init_settings();
app_name = MODULE_NAME;
app_key = MODULE_KEY;
LoggerHelpers::init_logger(app_key, L"ModuleInterface", "KeystrokeOverlay");
Logger::info("Keystroke Overlay ModuleInterface object is constructing");
};
// Destroy the powertoy and free memory
virtual void destroy() override
{
delete this;
}
// Return the display name of the powertoy, this will be cached by the runner
virtual const wchar_t* get_name() override
{
return MODULE_NAME;
}
virtual const wchar_t* get_key() override
{
return MODULE_KEY;
}
// Return array of the names of all events that this powertoy listens for, with
// nullptr as the last element of the array. We no longer request ll_keyboard
// events from the runner because the external KeystrokeServer.exe owns the
// low-level keyboard hook and IPC delivery.
virtual const wchar_t** get_events()
{
static const wchar_t* events[] = { win_hook_event, nullptr };
return events;
}
// Return JSON with the configuration options.
virtual bool get_config(wchar_t* buffer, int* buffer_size) override
{
HINSTANCE hinstance = reinterpret_cast<HINSTANCE>(&__ImageBase);
// Create a Settings object.
PowerToysSettings::Settings settings(hinstance, get_name());
settings.set_description(MODULE_DESC);
// Show an overview link in the Settings page
// commented out for now
//settings.set_overview_link(L"https://");
// Show a video link in the Settings page.
//settings.set_video_link(L"https://");
// enable_draggable_overlay Toggle
settings.add_bool_toggle(
L"enable_draggable_overlay", // property name.
L"Enable Draggable Overlay", // description or resource id of the localized string.
g_settings.is_draggable // property value.
);
settings.add_int_spinner(
L"display_mode", // property name
L"Display Mode", // description
g_settings.display_mode, // current value
0, // Min value (e.g., LastFiveKeystrokes)
2, // Max value (e.g., ShortcutsOnly)
1 // Step
);
settings.add_hotkey(
L"switch_monitor_hotkey",
L"Move to Next Monitor",
g_settings.switch_monitor_hotkey
);
// Overlay Timeout Spinner
settings.add_int_spinner(
L"overlay_timeout", // property name
L"Overlay Timeout (ms)", // description or resource id of the localized string.
g_settings.overlay_timeout, // property value.
500, // Min
10000, // Max
100 // Step
);
// 3. Text Size Spinner
settings.add_int_spinner(
L"text_size", // property name
L"Text Font Size", // description or resource id of the localized string.
g_settings.text_size, // property value.
10, // Min
72, // Max
2 // Step
);
// 4. Text Opacity
settings.add_int_spinner(
L"text_opacity", // property name
L"Text Opacity (%)", // description or resource id of the localized string.
g_settings.text_opacity, // property value.
0, // Min
100, // Max
5 // Step
);
// 5. Background Opacity
settings.add_int_spinner(
L"background_opacity", // property name
L"Background Opacity (%)", // description or resource id of the localized string.
g_settings.bg_opacity, // property value.
0, // Min
100, // Max
5 // Step
);
// 6. Text Color
settings.add_color_picker(
L"text_color", // property name.
L"Text Color", // description or resource id of the localized string.
g_settings.text_color // property value.
);
// 7. Background Color
settings.add_color_picker(
L"background_color", // property name.
L"Background Color", // description or resource id of the localized string.
g_settings.bg_color // property value.
);
return settings.serialize_to_buffer(buffer, buffer_size);
}
// Return the configured status for the gpo policy for the module
virtual powertoys_gpo::gpo_rule_configured_t gpo_policy_enabled_configuration() override
{
return powertoys_gpo::getConfiguredKeystrokeOverlayEnabledValue();
}
// Called by the runner to pass the updated settings values as a serialized JSON.
virtual void set_config(const wchar_t* config) override
{
try
{
// Parse the input JSON string.
PowerToysSettings::PowerToyValues values =
PowerToysSettings::PowerToyValues::from_json_string(config, get_key());
if (auto v = values.get_bool_value(L"enable_draggable_overlay")) {
g_settings.is_draggable = *v;
}
if (auto v = values.get_int_value(L"overlay_timeout")) {
g_settings.overlay_timeout = *v;
}
if (auto v = values.get_int_value(L"text_size")) {
g_settings.text_size = *v;
}
if (auto v = values.get_int_value(L"text_opacity")) {
g_settings.text_opacity = *v;
}
if (auto v = values.get_int_value(L"background_opacity")) {
g_settings.bg_opacity = *v;
}
if (auto v = values.get_string_value(L"text_color")) {
g_settings.text_color = *v;
}
if (auto v = values.get_string_value(L"background_color")) {
g_settings.bg_color = *v;
}
if (auto v = values.get_int_value(L"display_mode"))
{
g_settings.display_mode = *v;
}
if (auto v = values.get_json(L"switch_monitor_hotkey"))
{
g_settings.switch_monitor_hotkey = PowerToysSettings::HotkeyObject::from_json(*v);
}
// Save to disk so the C# App can read the updated settings.json
// If you don't need to do any custom processing of the settings, proceed
// to persists the values calling:
values.save_to_settings_file();
// Otherwise call a custom function to process the settings before saving them to disk:
// save_settings();
}
catch (std::exception&)
{
// Improper JSON.
}
}
// Enable the powertoy
// where the keystrokeoverlay.exe is/should be launched
virtual void enable()
{
launch_process();
m_enabled = true;
Trace::EnableKeystrokeOverlay(true);
}
// Disable the powertoy
virtual void disable()
{
terminate_process();
m_enabled = false;
// External server handles batching; nothing else to stop here.
Trace::EnableKeystrokeOverlay(false);
}
// Returns if the powertoys is enabled
virtual bool is_enabled() override
{
return m_enabled;
}
// Handle incoming event, data is event-specific
// virtual intptr_t signal_event(const wchar_t* name, intptr_t data)
// {
// if (wcscmp(name, win_hook_event) == 0)
// {
// /* auto& event = *(reinterpret_cast<WinHookEvent*>(data)); */
// // Return value is ignored
// return 0;
// }
// return 0;
// }
};
// Load the settings file.
void KeystrokeOverlay::init_settings()
{
try
{
// Load and parse the settings file for this PowerToy.
PowerToysSettings::PowerToyValues settings =
PowerToysSettings::PowerToyValues::load_from_settings_file(KeystrokeOverlay::get_name());
if (auto v = settings.get_bool_value(L"enable_draggable_overlay")) {
g_settings.is_draggable = *v;
}
if (auto v = settings.get_int_value(L"overlay_timeout")) {
g_settings.overlay_timeout = *v;
}
if (auto v = settings.get_int_value(L"text_size")) {
g_settings.text_size = *v;
}
if (auto v = settings.get_int_value(L"text_opacity")) {
g_settings.text_opacity = *v;
}
if (auto v = settings.get_int_value(L"background_opacity")) {
g_settings.bg_opacity = *v;
}
if (auto v = settings.get_string_value(L"text_color")) {
g_settings.text_color = *v;
}
if (auto v = settings.get_string_value(L"background_color")) {
g_settings.bg_color = *v;
}
if (auto v = settings.get_int_value(L"display_mode"))
{
g_settings.display_mode = *v;
}
if (auto v = settings.get_json(L"switch_monitor_hotkey"))
{
g_settings.switch_monitor_hotkey = PowerToysSettings::HotkeyObject::from_json(*v);
}
}
catch (std::exception&)
{
// Error while loading from the settings file. Let default values stay as they are.
}
}
extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()
{
return new KeystrokeOverlay();
}

View File

@@ -0,0 +1,2 @@
#include "pch.h"
#pragma comment(lib, "windowsapp")

View File

@@ -0,0 +1,6 @@
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Foundation.Collections.h>
#include <shellapi.h>
#include <Shlwapi.h>

View File

@@ -0,0 +1,31 @@
#include "pch.h"
#include "trace.h"
#include <common/Telemetry/TraceBase.h>
TRACELOGGING_DEFINE_PROVIDER(
g_hProvider,
"Microsoft.PowerToys",
// {38e8889b-9731-53f5-e901-e8a7c1753074}
(0x38e8889b, 0x9731, 0x53f5, 0xe9, 0x01, 0xe8, 0xa7, 0xc1, 0x75, 0x30, 0x74),
TraceLoggingOptionProjectTelemetry());
void Trace::RegisterProvider()
{
TraceLoggingRegister(g_hProvider);
}
void Trace::UnregisterProvider()
{
TraceLoggingUnregister(g_hProvider);
}
void Trace::EnableKeystrokeOverlay(const bool enabled) noexcept
{
TraceLoggingWriteWrapper(
g_hProvider,
"KeystrokeOverlay_EnableKeystrokeOverlay",
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE),
TraceLoggingBoolean(enabled, "Enabled"));
}

View File

@@ -0,0 +1,14 @@
#pragma once
#include <common/Telemetry/TraceBase.h>
class Trace : public telemetry::TraceBase
{
public:
static void RegisterProvider();
static void UnregisterProvider();
// Log if the user has Keystroke Overlay enabled or disabled
static void EnableKeystrokeOverlay(const bool enabled) noexcept;
};

View File

@@ -0,0 +1,20 @@
<!-- Copyright (c) Microsoft Corporation and Contributors. -->
<!-- Licensed under the MIT License. -->
<Application
x:Class="KeystrokeOverlayUI.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:KeystrokeOverlayUI">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
<!-- Other merged dictionaries here -->
<ResourceDictionary Source="/Controls/KeyVisual/KeyCharPresenter.xaml"/>
<ResourceDictionary Source="/Controls/KeyVisual/KeyVisual.xaml"/>
</ResourceDictionary.MergedDictionaries>
<!-- Other app resources here -->
</ResourceDictionary>
</Application.Resources>
</Application>

View File

@@ -0,0 +1,100 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Diagnostics;
using System.IO;
using KeystrokeOverlayUI.Services;
using ManagedCommon;
using Microsoft.UI.Xaml;
namespace KeystrokeOverlayUI
{
// Application entry point for the Keystroke Overlay UI.
public sealed partial class App : Application, IDisposable
{
private readonly ProcessJob _job = new();
private Window _window;
private bool _disposed;
// Gets the running native keystroke server process.
public Process KeystrokeServerProcess { get; private set; }
public App()
{
InitializeComponent();
string appLanguage = LanguageHelper.LoadLanguage();
if (!string.IsNullOrEmpty(appLanguage))
{
Microsoft.Windows.Globalization.ApplicationLanguages.PrimaryLanguageOverride = appLanguage;
}
Logger.InitializeLogger("\\KeystrokeOverlay\\Logs");
// Explicitly use Microsoft.UI.Xaml.UnhandledExceptionEventArgs
UnhandledException += App_UnhandledException;
}
protected override void OnLaunched(LaunchActivatedEventArgs args)
{
StartKeystrokeServer();
_window = new MainWindow();
_window.Activate();
}
private void StartKeystrokeServer()
{
try
{
string exePath = Path.Combine(
AppDomain.CurrentDomain.BaseDirectory,
"PowerToys.KeystrokeOverlayKeystrokeServer.exe");
if (!File.Exists(exePath))
{
Logger.LogError($"Keystroke server missing: {exePath}");
return;
}
KeystrokeServerProcess = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = exePath,
UseShellExecute = false,
CreateNoWindow = true,
},
};
KeystrokeServerProcess.Start();
// Add process to job so it dies with UI
_job.AddProcess(KeystrokeServerProcess.Handle);
Logger.LogInfo("Keystroke server started and assigned to job object.");
}
catch (Exception ex)
{
Logger.LogError("Failed to launch keystroke server.", ex);
}
}
// Explicit namespace to resolve ambiguity
private void App_UnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
{
Logger.LogError("Unhandled UI exception.", e.Exception);
}
public void Dispose()
{
if (!_disposed)
{
_job.Dispose();
_disposed = true;
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

View File

@@ -0,0 +1,106 @@
<?xml version="1.0" encoding="utf-8" ?>
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Microsoft.PowerToys.Settings.UI.Controls">
<Style BasedOn="{StaticResource DefaultKeyCharPresenterStyle}" TargetType="local:KeyCharPresenter" />
<Style x:Key="DefaultKeyCharPresenterStyle" TargetType="local:KeyCharPresenter">
<Setter Property="FontWeight" Value="Normal" />
<Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="IsTabStop" Value="False" />
<Setter Property="AutomationProperties.AccessibilityView" Value="Raw" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:KeyCharPresenter">
<Grid Height="{TemplateBinding FontSize}">
<TextBlock
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
FontFamily="{TemplateBinding FontFamily}"
FontSize="{TemplateBinding FontSize}"
FontWeight="{TemplateBinding FontWeight}"
Text="{TemplateBinding Content}"
TextLineBounds="Tight" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style
x:Key="WindowsKeyCharPresenterStyle"
BasedOn="{StaticResource DefaultKeyCharPresenterStyle}"
TargetType="local:KeyCharPresenter">
<!-- Scale to visually align the height of the Windows logo and text -->
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:KeyCharPresenter">
<Grid Height="{TemplateBinding FontSize}">
<Viewbox>
<PathIcon Data="M9 20H0V11H9V20ZM20 20H11V11H20V20ZM9 9H0V0H9V9ZM20 9H11V0H20V9Z" />
</Viewbox>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style
x:Key="OfficeKeyCharPresenterStyle"
BasedOn="{StaticResource DefaultKeyCharPresenterStyle}"
TargetType="local:KeyCharPresenter">
<!-- Scale to visually align the height of the Office logo and text -->
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:KeyCharPresenter">
<Grid Height="{TemplateBinding FontSize}">
<Viewbox>
<PathIcon Data="M1792 405v1238q0 33-10 62t-28 54-44 41-57 27l-555 159q-23 6-47 6-31 0-58-8t-53-24l-363-205q-20-11-31-29t-12-42q0-35 24-59t60-25h470V458L735 584q-43 15-69 53t-26 83v651q0 41-20 73t-55 53l-167 91q-23 12-46 12-40 0-68-28t-28-68V587q0-51 26-96t71-71L949 81q41-23 89-23 17 0 30 2t30 8l555 153q31 9 56 27t44 42 29 54 10 61zm-128 1238V405q0-22-13-38t-34-23l-273-75-64-18-64-18v1586l401-115q21-6 34-22t13-39z" />
</Viewbox>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style
x:Key="CopilotKeyCharPresenterStyle"
BasedOn="{StaticResource DefaultKeyCharPresenterStyle}"
TargetType="local:KeyCharPresenter">
<!-- Scale to visually align the height of the Copilot logo and text -->
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:KeyCharPresenter">
<Grid Height="{TemplateBinding FontSize}">
<Viewbox>
<PathIcon Data="M0 1213q0-60 10-124t27-130 35-129 38-121q18-55 41-119t54-129 70-125 87-106 106-74 129-28h661q59 0 114 17t96 64q30 34 46 72t33 81l22 58q11 29 34 52 23 25 56 31t65 9h4q157 0 238 83t82 240q0 60-10 125t-27 130-35 128-38 121q-18 55-41 119t-54 129-70 125-87 106-106 74-129 28H790q-61 0-107-15t-82-44-61-72-46-98q-11-29-24-60t-35-55q-23-25-51-31t-60-9h-4q-157 0-238-83T0 1213zm598-957q-50 0-93 25t-79 68-67 94-54 108-42 106-31 91q-17 51-35 110t-33 119-26 121-10 114q0 102 43 149t147 47h163q39 0 74-12t64-35 50-53 34-67q19-58 35-115t35-117q35-117 70-232t72-233q23-73 47-147t63-141H598zm452 285q69-29 143-29h281q-18-29-29-59t-21-58-21-54-30-44-46-30-69-11q-32 0-60 9t-48 35q-17 23-31 53t-27 63-23 65-19 60zm-296 867h101q39 0 74-12t66-34 52-52 33-68l58-191 42-140q21-70 43-140 11-36 28-69t43-62h-101q-39 0-74 12t-66 34-52 52-33 68q-15 48-29 96t-29 96q-21 70-41 140t-44 140q-11 36-28 68t-43 62zm814-768q-39 0-74 12t-64 35-50 53-34 68q-56 174-107 347t-106 349q-23 74-47 147t-63 141h427q50 0 93-25t79-68 67-94 54-108 42-106 31-91q16-51 34-110t34-119 26-121 10-114q0-102-43-149t-147-47h-162zm-570 867q-69 29-143 29H564q17 28 29 58t22 58 24 54 32 45 48 30 71 11q31 0 60-8t49-35q15-19 29-50t28-65 24-69 18-58z" />
</Viewbox>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style
x:Key="GlyphKeyCharPresenterStyle"
BasedOn="{StaticResource DefaultKeyCharPresenterStyle}"
TargetType="local:KeyCharPresenter">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:KeyCharPresenter">
<Grid>
<Viewbox>
<FontIcon
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
FontSize="{TemplateBinding FontSize}"
FontWeight="{TemplateBinding FontWeight}"
Glyph="{TemplateBinding Content}" />
</Viewbox>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@@ -0,0 +1,32 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Documents;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
namespace Microsoft.PowerToys.Settings.UI.Controls;
public sealed partial class KeyCharPresenter : Control
{
public KeyCharPresenter()
{
DefaultStyleKey = typeof(KeyCharPresenter);
}
public object Content
{
get => (object)GetValue(ContentProperty);
set => SetValue(ContentProperty, value);
}
public static readonly DependencyProperty ContentProperty = DependencyProperty.Register(nameof(Content), typeof(object), typeof(KeyCharPresenter), new PropertyMetadata(default(string)));
}

View File

@@ -0,0 +1,213 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Microsoft.PowerToys.Settings.UI.Controls">
<Style x:Key="DefaultKeyVisualStyle" TargetType="local:KeyVisual">
<Setter Property="MinWidth" Value="16" />
<Setter Property="AutomationProperties.AccessibilityView" Value="Raw" />
<Setter Property="IsTabStop" Value="False" />
<Setter Property="MinHeight" Value="16" />
<Setter Property="Background" Value="{ThemeResource CardBackgroundFillColorSecondaryBrush}" />
<Setter Property="Foreground" Value="{ThemeResource TextFillColorPrimaryBrush}" />
<Setter Property="BorderBrush" Value="{ThemeResource CardStrokeColorDefaultBrush}" />
<Setter Property="BorderThickness" Value="{ThemeResource ButtonBorderThemeThickness}" />
<Setter Property="Padding" Value="4,4,4,4" />
<Setter Property="FontWeight" Value="Normal" />
<Setter Property="FontSize" Value="14" />
<Setter Property="CornerRadius" Value="2" />
<Setter Property="BackgroundSizing" Value="InnerBorderEdge" />
<Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:KeyVisual">
<Grid
x:Name="KeyHolder"
MinWidth="{TemplateBinding MinWidth}"
MinHeight="{TemplateBinding MinHeight}"
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
VerticalAlignment="{TemplateBinding VerticalAlignment}"
Background="{TemplateBinding Background}"
BackgroundSizing="{TemplateBinding BackgroundSizing}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}">
<Grid.BackgroundTransition>
<BrushTransition Duration="0:0:0.083" />
</Grid.BackgroundTransition>
<local:KeyCharPresenter
x:Name="KeyPresenter"
Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
AutomationProperties.AccessibilityView="Raw"
Content="{TemplateBinding Content}"
FontSize="{TemplateBinding FontSize}"
FontWeight="{TemplateBinding FontWeight}"
Foreground="{TemplateBinding Foreground}" />
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Target="KeyHolder.Background" Value="{ThemeResource SubtleFillColorTransparentBrush}" />
<Setter Target="KeyHolder.BorderBrush" Value="{ThemeResource CardStrokeColorDefaultSolidBrush}" />
<Setter Target="KeyPresenter.Foreground" Value="{ThemeResource ControlStrokeColorDefaultBrush}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Invalid">
<VisualState.Setters>
<Setter Target="KeyHolder.Background" Value="{ThemeResource SystemFillColorCriticalBackgroundBrush}" />
<Setter Target="KeyHolder.BorderBrush" Value="{ThemeResource SystemFillColorCriticalBrush}" />
<Setter Target="KeyHolder.BorderThickness" Value="1" />
<Setter Target="KeyPresenter.Foreground" Value="{ThemeResource SystemFillColorCriticalBrush}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Warning">
<VisualState.Setters>
<Setter Target="KeyHolder.Background" Value="{ThemeResource SystemFillColorCautionBackgroundBrush}" />
<Setter Target="KeyHolder.BorderBrush" Value="{ThemeResource SystemFillColorCautionBrush}" />
<Setter Target="KeyHolder.BorderThickness" Value="1" />
<Setter Target="KeyPresenter.Foreground" Value="{ThemeResource SystemFillColorCautionBrush}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style
x:Key="SubtleKeyVisualStyle"
BasedOn="{StaticResource DefaultKeyVisualStyle}"
TargetType="local:KeyVisual">
<Setter Property="Background" Value="{ThemeResource SubtleFillColorTransparentBrush}" />
<Setter Property="BorderBrush" Value="{ThemeResource SubtleFillColorTransparentBrush}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:KeyVisual">
<Grid
x:Name="KeyHolder"
MinWidth="{TemplateBinding MinWidth}"
MinHeight="{TemplateBinding MinHeight}"
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
VerticalAlignment="{TemplateBinding VerticalAlignment}"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}">
<Grid.BackgroundTransition>
<BrushTransition Duration="0:0:0.083" />
</Grid.BackgroundTransition>
<local:KeyCharPresenter
x:Name="KeyPresenter"
Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
AutomationProperties.AccessibilityView="Raw"
Content="{TemplateBinding Content}"
FontSize="{TemplateBinding FontSize}"
FontWeight="{TemplateBinding FontWeight}"
Foreground="{TemplateBinding Foreground}" />
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Target="KeyPresenter.Foreground" Value="{ThemeResource TextFillColorDisabledBrush}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Invalid">
<VisualState.Setters>
<Setter Target="KeyPresenter.Foreground" Value="{ThemeResource SystemFillColorCriticalBrush}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Warning">
<VisualState.Setters>
<Setter Target="KeyPresenter.Foreground" Value="{ThemeResource SystemFillColorCautionBrush}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style
x:Key="AccentKeyVisualStyle"
BasedOn="{StaticResource DefaultKeyVisualStyle}"
TargetType="local:KeyVisual">
<Setter Property="Background" Value="{ThemeResource AccentFillColorDefaultBrush}" />
<Setter Property="Foreground" Value="{ThemeResource TextOnAccentFillColorPrimaryBrush}" />
<Setter Property="BorderBrush" Value="{ThemeResource AccentControlElevationBorderBrush}" />
<Setter Property="BackgroundSizing" Value="OuterBorderEdge" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:KeyVisual">
<Grid
x:Name="KeyHolder"
MinWidth="{TemplateBinding MinWidth}"
MinHeight="{TemplateBinding MinHeight}"
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
VerticalAlignment="{TemplateBinding VerticalAlignment}"
AutomationProperties.AccessibilityView="Raw"
Background="{TemplateBinding Background}"
BackgroundSizing="{TemplateBinding BackgroundSizing}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}">
<Grid.BackgroundTransition>
<BrushTransition Duration="0:0:0.083" />
</Grid.BackgroundTransition>
<local:KeyCharPresenter
x:Name="KeyPresenter"
Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Content="{TemplateBinding Content}"
FontSize="{TemplateBinding FontSize}"
FontWeight="{TemplateBinding FontWeight}"
Foreground="{TemplateBinding Foreground}" />
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Target="KeyHolder.Background" Value="{ThemeResource AccentButtonBackgroundDisabled}" />
<Setter Target="KeyHolder.BorderBrush" Value="{ThemeResource AccentButtonBorderBrushDisabled}" />
<Setter Target="KeyPresenter.Foreground" Value="{ThemeResource AccentButtonForegroundDisabled}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Invalid">
<VisualState.Setters>
<Setter Target="KeyHolder.Background" Value="{ThemeResource SystemFillColorCriticalBackgroundBrush}" />
<Setter Target="KeyHolder.BorderBrush" Value="{ThemeResource SystemFillColorCriticalBrush}" />
<Setter Target="KeyHolder.BorderThickness" Value="1" />
<Setter Target="KeyPresenter.Foreground" Value="{ThemeResource SystemFillColorCriticalBrush}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Warning">
<VisualState.Setters>
<Setter Target="KeyHolder.Background" Value="{ThemeResource SystemFillColorCautionBackgroundBrush}" />
<Setter Target="KeyHolder.BorderBrush" Value="{ThemeResource SystemFillColorCautionBrush}" />
<Setter Target="KeyHolder.BorderThickness" Value="1" />
<Setter Target="KeyPresenter.Foreground" Value="{ThemeResource SystemFillColorCautionBrush}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style BasedOn="{StaticResource DefaultKeyVisualStyle}" TargetType="local:KeyVisual" />
</ResourceDictionary>

View File

@@ -0,0 +1,193 @@
// 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.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Windows.System;
namespace Microsoft.PowerToys.Settings.UI.Controls
{
[TemplatePart(Name = KeyPresenter, Type = typeof(KeyCharPresenter))]
[TemplateVisualState(Name = NormalState, GroupName = "CommonStates")]
[TemplateVisualState(Name = DisabledState, GroupName = "CommonStates")]
[TemplateVisualState(Name = InvalidState, GroupName = "CommonStates")]
[TemplateVisualState(Name = WarningState, GroupName = "CommonStates")]
public sealed partial class KeyVisual : Control
{
private const string KeyPresenter = "KeyPresenter";
private const string NormalState = "Normal";
private const string DisabledState = "Disabled";
private const string InvalidState = "Invalid";
private const string WarningState = "Warning";
private KeyCharPresenter _keyPresenter;
public object Content
{
get => (object)GetValue(ContentProperty);
set => SetValue(ContentProperty, value);
}
public static readonly DependencyProperty ContentProperty = DependencyProperty.Register(nameof(Content), typeof(object), typeof(KeyVisual), new PropertyMetadata(default(string), OnContentChanged));
public State State
{
get => (State)GetValue(StateProperty);
set => SetValue(StateProperty, value);
}
public static readonly DependencyProperty StateProperty = DependencyProperty.Register(nameof(State), typeof(State), typeof(KeyVisual), new PropertyMetadata(State.Normal, OnStateChanged));
public bool RenderKeyAsGlyph
{
get => (bool)GetValue(RenderKeyAsGlyphProperty);
set => SetValue(RenderKeyAsGlyphProperty, value);
}
public static readonly DependencyProperty RenderKeyAsGlyphProperty = DependencyProperty.Register(nameof(RenderKeyAsGlyph), typeof(bool), typeof(KeyVisual), new PropertyMetadata(false, OnContentChanged));
public KeyVisual()
{
this.DefaultStyleKey = typeof(KeyVisual);
}
protected override void OnApplyTemplate()
{
IsEnabledChanged -= KeyVisual_IsEnabledChanged;
_keyPresenter = (KeyCharPresenter)this.GetTemplateChild(KeyPresenter);
Update();
SetVisualStates();
IsEnabledChanged += KeyVisual_IsEnabledChanged;
base.OnApplyTemplate();
}
private static void OnContentChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((KeyVisual)d).SetVisualStates();
}
private static void OnStateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((KeyVisual)d).SetVisualStates();
}
private void SetVisualStates()
{
if (this != null)
{
if (State == State.Error)
{
VisualStateManager.GoToState(this, InvalidState, true);
}
else if (State == State.Warning)
{
VisualStateManager.GoToState(this, WarningState, true);
}
else if (!IsEnabled)
{
VisualStateManager.GoToState(this, DisabledState, true);
}
else
{
VisualStateManager.GoToState(this, NormalState, true);
}
}
}
private void Update()
{
if (Content == null)
{
return;
}
if (Content is string key)
{
switch (key)
{
case "Copilot":
_keyPresenter.Style = (Style)Application.Current.Resources["CopilotKeyCharPresenterStyle"];
break;
case "Office":
_keyPresenter.Style = (Style)Application.Current.Resources["OfficeKeyCharPresenterStyle"];
break;
default:
_keyPresenter.Style = (Style)Application.Current.Resources["DefaultKeyCharPresenterStyle"];
break;
}
return;
}
if (Content is int keyCode)
{
VirtualKey virtualKey = (VirtualKey)keyCode;
switch (virtualKey)
{
case VirtualKey.Enter:
SetGlyphOrText("\uE751", virtualKey);
break;
case VirtualKey.Back:
SetGlyphOrText("\uE750", virtualKey);
break;
case VirtualKey.Shift:
case (VirtualKey)160: // Left Shift
case (VirtualKey)161: // Right Shift
SetGlyphOrText("\uE752", virtualKey);
break;
case VirtualKey.Up:
_keyPresenter.Content = "\uE0E4";
break;
case VirtualKey.Down:
_keyPresenter.Content = "\uE0E5";
break;
case VirtualKey.Left:
_keyPresenter.Content = "\uE0E2";
break;
case VirtualKey.Right:
_keyPresenter.Content = "\uE0E3";
break;
case VirtualKey.LeftWindows:
case VirtualKey.RightWindows:
_keyPresenter.Style = (Style)Application.Current.Resources["WindowsKeyCharPresenterStyle"];
break;
}
}
}
private void SetGlyphOrText(string glyph, VirtualKey key)
{
if (RenderKeyAsGlyph)
{
_keyPresenter.Content = glyph;
_keyPresenter.Style = (Style)Application.Current.Resources["GlyphKeyCharPresenterStyle"];
}
else
{
_keyPresenter.Content = key.ToString();
_keyPresenter.Style = (Style)Application.Current.Resources["DefaultKeyCharPresenterStyle"];
}
}
private void KeyVisual_IsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e)
{
SetVisualStates();
}
}
public enum State
{
Normal,
Error,
Warning,
}
}

View File

@@ -0,0 +1,31 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using KeystrokeOverlayUI.Models;
using Microsoft.UI;
using Microsoft.UI.Xaml.Media;
using Windows.UI;
namespace KeystrokeOverlayUI.Controls
{
public partial class KeyVisualItem : ObservableObject
{
[ObservableProperty]
private string _text;
[ObservableProperty]
private int _textSize;
[ObservableProperty]
private double _opacity = 1.0;
public bool IsExiting { get; set; }
}
}

View File

@@ -0,0 +1,243 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
namespace KeystrokeOverlayUI.Controls
{
[StructLayout(LayoutKind.Sequential)]
public struct KeystrokeEvent
{
public uint VirtualKey;
public bool IsPressed;
public List<string> Modifiers;
public string Text;
public string EventType;
// Convert the KeystrokeEvent to a human-readable string for display
public override string ToString()
{
if (!IsPressed)
{
return string.Empty;
}
bool isCharEvent = string.Equals(EventType, "char", StringComparison.OrdinalIgnoreCase);
string keyName = null;
// check for modifiers
bool hasCtrl = Modifiers != null && Modifiers.Contains("Ctrl");
bool hasAlt = Modifiers != null && Modifiers.Contains("Alt");
bool hasWin = Modifiers != null && Modifiers.Contains("Win");
if (isCharEvent && !hasWin)
{
if (string.IsNullOrWhiteSpace(Text))
{
return string.Empty;
}
if (!string.IsNullOrEmpty(Text))
{
keyName = Text;
}
}
else
{
if (IsCommandKey(VirtualKey) || hasCtrl || hasAlt || hasWin)
{
keyName = GetKeyName(VirtualKey);
}
}
// only register valid key combinations
if (keyName != null)
{
var displayParts = new List<string>();
if (Modifiers != null)
{
foreach (var mod in Modifiers)
{
// Don't show "Shift" if we are displaying a Char
// (because the Char "!" already implies Shift was pressed)
if (isCharEvent && !hasWin && mod == "Shift" && !hasCtrl && !hasAlt)
{
continue;
}
string symbol = GetModifierSymbol(mod);
if (!displayParts.Contains(symbol))
{
displayParts.Add(symbol);
}
}
}
// Avoid duplicates (e.g. Ctrl + Ctrl)
string modSym = GetModifierSymbol(keyName);
if (!displayParts.Contains(keyName) && !displayParts.Contains(modSym))
{
displayParts.Add(keyName);
}
return string.Join(" + ", displayParts);
}
return string.Empty;
}
public bool IsShortcut
{
get
{
// 1. If it has modifiers (Ctrl, Alt, Win), it is definitely a shortcut
if (Modifiers != null && Modifiers.Count > 0)
{
return true;
}
// 2. If it is a "Command Key" (Enter, Esc, F1), treat it as a shortcut
if (IsCommandKey(VirtualKey))
{
return true;
}
// 3. Otherwise, it is just a standard character (A, 1, !)
return false;
}
}
private static bool IsCommandKey(uint virtualKey)
{
var key = (Windows.System.VirtualKey)virtualKey;
switch (key)
{
case Windows.System.VirtualKey.Space:
case Windows.System.VirtualKey.Enter:
case Windows.System.VirtualKey.Tab:
case Windows.System.VirtualKey.Back:
case Windows.System.VirtualKey.Escape:
case Windows.System.VirtualKey.Delete:
case Windows.System.VirtualKey.Insert:
case Windows.System.VirtualKey.Home:
case Windows.System.VirtualKey.End:
case Windows.System.VirtualKey.PageUp:
case Windows.System.VirtualKey.PageDown:
case Windows.System.VirtualKey.Left:
case Windows.System.VirtualKey.Right:
case Windows.System.VirtualKey.Up:
case Windows.System.VirtualKey.Down:
case Windows.System.VirtualKey.Snapshot: // Print Screen
case Windows.System.VirtualKey.Pause:
case Windows.System.VirtualKey.CapitalLock:
case Windows.System.VirtualKey.LeftWindows:
case Windows.System.VirtualKey.RightWindows:
return true;
}
if (virtualKey >= 112 && virtualKey <= 135)
{
return true;
}
// BLOCK everything else (A-Z, 0-9, Punctuation)
// These will be handled by the "Char" event instead
return false;
}
private static string GetModifierSymbol(string modifier)
{
return modifier switch
{
"Ctrl" => "Ctrl",
"Alt" => "Alt",
"Shift" => "⇧",
"Win" => "⊞",
_ => modifier,
};
}
private static string GetKeyName(uint virtualKey)
{
var key = (Windows.System.VirtualKey)virtualKey;
int intKey = (int)virtualKey;
switch (key)
{
case Windows.System.VirtualKey.LeftShift:
case Windows.System.VirtualKey.RightShift: return "⇧";
case Windows.System.VirtualKey.Control:
case Windows.System.VirtualKey.LeftControl:
case Windows.System.VirtualKey.RightControl: return "Ctrl";
case Windows.System.VirtualKey.Menu:
case Windows.System.VirtualKey.LeftMenu:
case Windows.System.VirtualKey.RightMenu: return "Alt";
case Windows.System.VirtualKey.LeftWindows:
case Windows.System.VirtualKey.RightWindows: return "⊞";
case Windows.System.VirtualKey.Space: return "Space";
case Windows.System.VirtualKey.Enter: return "Enter";
case Windows.System.VirtualKey.Back: return "Backspace";
case Windows.System.VirtualKey.Tab: return "Tab";
case Windows.System.VirtualKey.Escape: return "Esc";
case Windows.System.VirtualKey.Delete: return "Del";
case Windows.System.VirtualKey.Insert: return "Ins";
case Windows.System.VirtualKey.Left: return "←";
case Windows.System.VirtualKey.Right: return "→";
case Windows.System.VirtualKey.Up: return "↑";
case Windows.System.VirtualKey.Down: return "↓";
}
// Handle Letters (A-Z)
if (key >= Windows.System.VirtualKey.A && key <= Windows.System.VirtualKey.Z)
{
return key.ToString();
}
// Handle Numbers (0-9) - strip the word "Number"
if (key >= Windows.System.VirtualKey.Number0 && key <= Windows.System.VirtualKey.Number9)
{
return key.ToString().Replace("Number", string.Empty);
}
// Handle Numpad
if (key >= Windows.System.VirtualKey.NumberPad0 && key <= Windows.System.VirtualKey.NumberPad9)
{
return "Num " + key.ToString().Replace("NumberPad", string.Empty);
}
// Handle Punctuation (The "Numbers" you are seeing)
switch (intKey)
{
case 186: return ";";
case 187: return "=";
case 188: return ",";
case 189: return "-";
case 190: return ".";
case 191: return "/"; // Forward Slash
case 192: return "`"; // Backtick
case 219: return "[";
case 220: return "\\"; // Backslash
case 221: return "]";
case 222: return "'"; // Single Quote
case 173: return "Mute";
case 174: return "Vol -";
case 175: return "Vol +";
case 176: return "Next";
case 177: return "Prev";
case 179: return "Play/Pause";
}
return key.ToString();
}
}
}

View File

@@ -0,0 +1,59 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.UI;
using Microsoft.UI.Xaml.Media;
using Windows.UI;
namespace KeystrokeOverlayUI.Helpers
{
public static class CustomColorHelper
{
public static SolidColorBrush GetBrushFromHex(string hex)
{
try
{
if (string.IsNullOrEmpty(hex))
{
return new SolidColorBrush(Colors.Transparent);
}
// Handles #RRGGBB or #AARRGGBB
hex = hex.Replace("#", string.Empty);
byte a = 255;
byte r = 0, g = 0, b = 0;
var provider = CultureInfo.InvariantCulture;
if (hex.Length == 6)
{
r = byte.Parse(hex.AsSpan(0, 2), NumberStyles.HexNumber, provider);
g = byte.Parse(hex.AsSpan(2, 2), NumberStyles.HexNumber, provider);
b = byte.Parse(hex.AsSpan(4, 2), NumberStyles.HexNumber, provider);
}
else if (hex.Length == 8)
{
a = byte.Parse(hex.AsSpan(0, 2), NumberStyles.HexNumber, provider);
r = byte.Parse(hex.AsSpan(2, 2), NumberStyles.HexNumber, provider);
g = byte.Parse(hex.AsSpan(4, 2), NumberStyles.HexNumber, provider);
b = byte.Parse(hex.AsSpan(6, 2), NumberStyles.HexNumber, provider);
}
return new SolidColorBrush(Color.FromArgb(a, r, g, b));
}
catch
{
// Error fallback
return new SolidColorBrush(Colors.Magenta);
}
}
}
}

View File

@@ -0,0 +1,100 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Runtime.InteropServices;
namespace KeystrokeOverlayUI.Helpers
{
public static class NativeWindowHelper
{
// ---------------------------------------------------------
// Public Structs
// ---------------------------------------------------------
[StructLayout(LayoutKind.Sequential)]
public struct POINT
{
public int X;
public int Y;
}
// ---------------------------------------------------------
// Public Methods
// ---------------------------------------------------------
/// <summary>
/// Retrieves the current cursor position in screen coordinates.
/// </summary>
public static POINT GetCursorPosition()
{
GetCursorPos(out POINT lpPoint);
return lpPoint;
}
/// <summary>
/// Sets the window style to be "ToolWindow" (hidden from Alt-Tab) and
/// "NoActivate" (doesn't steal focus).
/// </summary>
public static void SetOverlayWindowStyles(IntPtr hWnd)
{
int exStyle = GetWindowLong(hWnd, GwlExStyle);
_ = SetWindowLong(hWnd, GwlExStyle, exStyle | WsExNoActivate | WsExToolWindow);
}
/// <summary>
/// Forces the window to stay on top of all other windows without stealing focus.
/// </summary>
public static void EnforceTopMost(IntPtr hWnd)
{
// SwpNoActivate is critical to ensure we don't steal focus while the user types
SetWindowPos(hWnd, HwndTopmost, 0, 0, 0, 0, SwpNoMove | SwpNoSize | SwpShowWindow | SwpNoActivate);
}
/// <summary>
/// Applies Windows 11 rounded corners preference to the window.
/// </summary>
public static void SetRoundedCorners(IntPtr hWnd)
{
int cornerPreference = DwmwcpRound;
_ = DwmSetWindowAttribute(hWnd, DwmwaWindowCornerPreference, ref cornerPreference, sizeof(int));
}
// ---------------------------------------------------------
// Native Constants & Private Imports
// ---------------------------------------------------------
// Window Pos Flags
private static readonly IntPtr HwndTopmost = new IntPtr(-1);
private const uint SwpNoSize = 0x0001;
private const uint SwpNoMove = 0x0002;
private const uint SwpNoActivate = 0x0010;
private const uint SwpShowWindow = 0x0040;
// Window Styles
private const int GwlExStyle = -20;
private const int WsExNoActivate = 0x08000000;
private const int WsExToolWindow = 0x00000080;
// DWM Constants
private const int DwmwaWindowCornerPreference = 33;
private const int DwmwcpRound = 2;
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool GetCursorPos(out POINT lpPoint);
[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int x, int y, int cx, int cy, uint uFlags);
[DllImport("user32.dll")]
private static extern int GetWindowLong(IntPtr hWnd, int nIndex);
[DllImport("user32.dll")]
private static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
[DllImport("dwmapi.dll")]
private static extern int DwmSetWindowAttribute(IntPtr hwnd, int attribute, ref int value, int size);
}
}

View File

@@ -0,0 +1,75 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- Standard PowerToys Imports -->
<Import Project="..\..\..\Common.Dotnet.CsWinRT.props" />
<Import Project="..\..\..\Common.SelfContained.props" />
<PropertyGroup>
<OutputType>WinExe</OutputType>
<!-- IMPORTANT: Rename UI EXE so that Runner can launch it -->
<AssemblyName>PowerToys.KeystrokeOverlay</AssemblyName>
<RootNamespace>PowerToys.KeystrokeOverlayUI</RootNamespace>
<!-- Output folder must match where Runner searches -->
<OutputPath>..\..\..\..\$(Platform)\$(Configuration)\WinUI3Apps\</OutputPath>
<UseWinUI>true</UseWinUI>
<ApplicationManifest>app.manifest</ApplicationManifest>
<!-- PowerToys WinAppSDK settings (must stay enabled) -->
<WindowsPackageType>None</WindowsPackageType>
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
<SelfContained>true</SelfContained>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<!-- Platform settings -->
<Platforms>x64;ARM64</Platforms>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
</PropertyGroup>
<ItemGroup>
<RuntimeHostConfigurationOption Include="MVVMTOOLKIT_ENABLE_INOTIFYPROPERTYCHANGING_SUPPORT" Value="true" />
</ItemGroup>
<ItemGroup>
<!-- DO NOT specify version numbers — PowerToys manages versions repo-wide -->
<PackageReference Include="CommunityToolkit.Mvvm" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" />
<PackageReference Include="Microsoft.WindowsAppSDK" />
<PackageReference Include="Microsoft.Extensions.Hosting" />
<PackageReference Include="System.Text.Json" />
<Manifest Include="$(ApplicationManifest)" />
</ItemGroup>
<!-- Required PowerToys common library references -->
<ItemGroup>
<ProjectReference Include="..\..\..\common\Common.UI\Common.UI.csproj" />
<ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
<ProjectReference Include="..\..\..\common\LanguageModelProvider\LanguageModelProvider.csproj" />
<ProjectReference Include="..\..\..\common\GPOWrapper\GPOWrapper.vcxproj" />
<ProjectReference Include="..\..\..\common\interop\PowerToys.Interop.vcxproj" />
<ProjectReference Include="..\..\..\settings-ui/Settings.UI.Library/Settings.UI.Library.csproj" />
</ItemGroup>
<!-- Ensure WinUI pages compile -->
<ItemGroup>
<Page Update="Controls\KeyVisual\KeyVisual.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<!-- Publish Properties (required for flattening like other PT modules) -->
<PropertyGroup>
<PublishReadyToRun Condition="'$(Configuration)' == 'Debug'">False</PublishReadyToRun>
<PublishReadyToRun Condition="'$(Configuration)' != 'Debug'">True</PublishReadyToRun>
<PublishTrimmed Condition="'$(Configuration)' == 'Debug'">False</PublishTrimmed>
<PublishTrimmed Condition="'$(Configuration)' != 'Debug'">True</PublishTrimmed>
<ApplicationIcon>Assets\KeystrokeOverlay\Icon.ico</ApplicationIcon>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,335 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using KeystrokeOverlayUI.Controls;
using KeystrokeOverlayUI.Helpers;
using KeystrokeOverlayUI.Models;
using KeystrokeOverlayUI.Services;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.UI;
using Microsoft.UI.Xaml.Media;
using Windows.UI;
namespace KeystrokeOverlayUI
{
public partial class MainViewModel : ObservableObject
{
// Changed from string to KeyVisualItem to support individual properties
public ObservableCollection<KeyVisualItem> PressedKeys { get; } = new();
[ObservableProperty]
private int _timeoutMs = 3000;
[ObservableProperty]
private int _textSize = 24;
[ObservableProperty]
private SolidColorBrush _textColor = new(Colors.White);
[ObservableProperty]
private SolidColorBrush _backgroundColor = new(Colors.Transparent);
[ObservableProperty]
private bool _isDraggable = true;
[ObservableProperty]
private DisplayMode _displayMode = DisplayMode.Last5;
[ObservableProperty]
private bool _isActive = true;
[ObservableProperty]
private string _monitorLabelText = string.Empty;
[ObservableProperty]
private bool _isMonitorLabelVisible = false;
[ObservableProperty]
private string _activationLabelText = string.Empty;
[ObservableProperty]
private bool _isActivationLabelVisible = false;
[ObservableProperty]
private string _displayModeText = string.Empty;
[ObservableProperty]
private bool _isDisplayModeVisible = false;
[ObservableProperty]
private bool _isVisibleHotkey = false;
private int _maxKeystrokesShown = 5;
public HotkeySettings ActivationShortcut { get; set; }
= new HotkeySettings(true, false, false, true, 0x4B);
public HotkeySettings SwitchMonitorHotkey { get; set; }
= new HotkeySettings(true, true, false, false, 0x4B);
public HotkeySettings SwitchDisplayModeHotkey { get; set; }
= new HotkeySettings(true, false, false, true, 0x44);
public event EventHandler<HotkeyAction> HotkeyActionTriggered;
public async void ShowLabel(HotkeyAction action, string text, int durationMs = 2000)
{
switch (action)
{
case HotkeyAction.Monitor:
MonitorLabelText = text;
IsMonitorLabelVisible = true;
break;
case HotkeyAction.Activation:
ActivationLabelText = text;
IsActivationLabelVisible = true;
break;
case HotkeyAction.DisplayMode:
DisplayModeText = text;
IsDisplayModeVisible = true;
break;
}
IsVisibleHotkey = IsMonitorLabelVisible || IsActivationLabelVisible || IsDisplayModeVisible;
try
{
await Task.Delay(durationMs);
}
catch
{
// write to logs
Logger.LogError("KeystrokeOverlay: Error showing label delay.");
}
switch (action)
{
case HotkeyAction.Monitor:
IsMonitorLabelVisible = false;
break;
case HotkeyAction.Activation:
IsActivationLabelVisible = false;
break;
case HotkeyAction.DisplayMode:
IsDisplayModeVisible = false;
break;
}
IsVisibleHotkey = IsMonitorLabelVisible || IsActivationLabelVisible || IsDisplayModeVisible;
}
public void ApplySettings(ModuleProperties props)
{
TimeoutMs = props.OverlayTimeout.Value;
TextSize = props.TextSize.Value;
TextColor = CustomColorHelper.GetBrushFromHex(props.TextColor.Value);
BackgroundColor = CustomColorHelper.GetBrushFromHex(props.BackgroundColor.Value);
IsDraggable = props.IsDraggable.Value;
DisplayMode = (DisplayMode)props.DisplayMode.Value;
_maxKeystrokesShown = DisplayMode == DisplayMode.SingleCharactersOnly ? 1 : 5;
ActivationShortcut = props.ActivationShortcut;
SwitchMonitorHotkey = props.SwitchMonitorHotkey;
SwitchDisplayModeHotkey = props.SwitchDisplayModeHotkey;
}
private readonly KeystrokeProcessor _keystrokeProcessor = new();
public void HandleKeystrokeEvent(KeystrokeEvent keystroke)
{
bool isDown = string.Equals(keystroke.EventType, "down", StringComparison.OrdinalIgnoreCase);
if (isDown && keystroke.IsPressed)
{
if (CheckGlobalHotkeys(keystroke))
{
return;
}
}
if (!IsActive)
{
return;
}
// update UI
var result = _keystrokeProcessor.Process(keystroke, DisplayMode);
switch (result.Action)
{
case KeystrokeAction.Add:
RegisterKey(result.Text, TimeoutMs);
break;
case KeystrokeAction.ReplaceLast:
if (PressedKeys.Count > 0)
{
PressedKeys.RemoveAt(PressedKeys.Count - 1);
}
RegisterKey(result.Text, TimeoutMs);
break;
case KeystrokeAction.RemoveLast:
if (PressedKeys.Count > 0)
{
PressedKeys.RemoveAt(PressedKeys.Count - 1);
UpdateOpacities();
}
break;
case KeystrokeAction.None:
default:
break;
}
}
private bool CheckGlobalHotkeys(KeystrokeEvent keystroke)
{
if (IsHotkeyMatch(keystroke, ActivationShortcut))
{
IsActive = !IsActive;
ShowLabel(HotkeyAction.Activation, IsActive ? "Overlay On" : "Overlay Off");
if (!IsActive)
{
ClearKeys();
_keystrokeProcessor.ResetBuffer();
}
HotkeyActionTriggered?.Invoke(this, HotkeyAction.Activation);
return true;
}
else if (IsHotkeyMatch(keystroke, SwitchMonitorHotkey))
{
HotkeyActionTriggered?.Invoke(this, HotkeyAction.Monitor);
return true;
}
else if (IsHotkeyMatch(keystroke, SwitchDisplayModeHotkey))
{
int current = (int)DisplayMode;
DisplayMode = (DisplayMode)((current + 1) % 4);
_maxKeystrokesShown = DisplayMode == DisplayMode.SingleCharactersOnly ? 1 : 5;
string modeText = DisplayMode switch
{
DisplayMode.Last5 => "Last Five Keystrokes",
DisplayMode.SingleCharactersOnly => "Single Characters Only",
DisplayMode.ShortcutsOnly => "Shortcuts Only",
DisplayMode.Stream => "Stream",
_ => "Unknown",
};
_keystrokeProcessor.ResetBuffer();
ShowLabel(HotkeyAction.DisplayMode, modeText);
HotkeyActionTriggered?.Invoke(this, HotkeyAction.DisplayMode);
return true;
}
return false;
}
private bool IsHotkeyMatch(KeystrokeEvent kEvent, HotkeySettings settings)
{
if (settings == null || !settings.IsValid())
{
return false;
}
// Compare the Main Key Code
if (kEvent.VirtualKey != settings.Code)
{
return false;
}
// Compare Modifiers
bool hasWin = kEvent.Modifiers?.Contains("Win") ?? false;
bool hasCtrl = kEvent.Modifiers?.Contains("Ctrl") ?? false;
bool hasAlt = kEvent.Modifiers?.Contains("Alt") ?? false;
bool hasShift = kEvent.Modifiers?.Contains("Shift") ?? false;
return hasWin == settings.Win &&
hasCtrl == settings.Ctrl &&
hasAlt == settings.Alt &&
hasShift == settings.Shift;
}
public void RegisterKey(string key, int durationMs, int textSize = -1)
{
if (textSize == -1)
{
textSize = TextSize;
}
var newItem = new KeyVisualItem { Text = key, Opacity = 1.0, TextSize = textSize };
PressedKeys.Add(newItem);
UpdateOpacities();
if (PressedKeys.Count > _maxKeystrokesShown)
{
PressedKeys.RemoveAt(0);
UpdateOpacities();
}
// Pass the duration to the removal logic
_ = RemoveKeyAfterDelayAsync(newItem, durationMs);
}
// 2. Add a helper to clear keys immediately (for switching phases)
public void ClearKeys()
{
PressedKeys.Clear();
}
private void UpdateOpacities()
{
// Iterate through all keys
for (int i = 0; i < PressedKeys.Count; i++)
{
var item = PressedKeys[i];
// If the item is currently running its "death animation", skip it
if (item.IsExiting)
{
continue;
}
// Calculate index from the end (Newest = 0)
int indexFromEnd = PressedKeys.Count - 1 - i;
// Decrease 15% for every step back
double targetOpacity = 1.0 - (0.15 * indexFromEnd);
// Clamp to valid range (e.g. don't go below 0.1 visible)
item.Opacity = Math.Max(0.1, targetOpacity);
}
}
private async Task RemoveKeyAfterDelayAsync(KeyVisualItem item, int durationMs)
{
// Wait the defined lifetime
await Task.Delay(durationMs);
// Mark as exiting so UpdateOpacities doesn't fight us
item.IsExiting = true;
PressedKeys.Remove(item);
// Re-adjust remaining keys
UpdateOpacities();
}
}
}

View File

@@ -0,0 +1,96 @@
<Window
x:Class="KeystrokeOverlayUI.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:KeystrokeOverlayUI"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls"
mc:Ignorable="d"
Title="KeystrokeOverlayUI">
<Window.SystemBackdrop>
<MicaBackdrop Kind="BaseAlt"/>
</Window.SystemBackdrop>
<Border x:Name="RootGrid"
Loaded="RootGrid_Loaded"
PointerPressed="RootGrid_PointerPressed"
PointerMoved="RootGrid_PointerMoved"
PointerReleased="RootGrid_PointerReleased"
PointerEntered="RootGrid_PointerEntered"
PointerExited="RootGrid_PointerExited"
Padding="10,7"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Background="Transparent">
<StackPanel Orientation="Vertical" Spacing="5">
<StackPanel Orientation="Horizontal" Spacing="5">
<Border CornerRadius="4"
Padding="8,4"
HorizontalAlignment="Left"
Visibility="{x:Bind ViewModel.IsMonitorLabelVisible, Mode=OneWay}">
<TextBlock Text="{x:Bind ViewModel.MonitorLabelText, Mode=OneWay}"
Foreground="Black"
FontSize="18"
FontWeight="SemiBold"/>
</Border>
<Border CornerRadius="4"
Padding="8,4"
HorizontalAlignment="Left"
Visibility="{x:Bind ViewModel.IsActivationLabelVisible, Mode=OneWay}">
<TextBlock Text="{x:Bind ViewModel.ActivationLabelText, Mode=OneWay}"
Foreground="Black"
FontSize="18"
FontWeight="SemiBold"/>
</Border>
<Border CornerRadius="4"
Padding="8,4"
HorizontalAlignment="Left"
Visibility="{x:Bind ViewModel.IsDisplayModeVisible, Mode=OneWay}">
<TextBlock Text="{x:Bind ViewModel.DisplayModeText, Mode=OneWay}"
Foreground="Black"
FontSize="18"
FontWeight="SemiBold"/>
</Border>
</StackPanel>
<ItemsControl ItemsSource="{x:Bind ViewModel.PressedKeys, Mode=OneWay}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" Spacing="10" SizeChanged="StackPanel_SizeChanged"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<controls:KeyVisual
Content="{Binding Text}"
VerticalAlignment="Center"
IsTabStop="False"
AutomationProperties.AccessibilityView="Raw"
Padding="12,6"
CornerRadius="6"
Opacity="{Binding Opacity}"
FontSize="{Binding TextSize}"
Foreground="{Binding DataContext.TextColor, ElementName=RootGrid}"
Background="{Binding DataContext.BackgroundColor, ElementName=RootGrid}">
<controls:KeyVisual.OpacityTransition>
<ScalarTransition />
</controls:KeyVisual.OpacityTransition>
</controls:KeyVisual>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Border>
</Window>

View File

@@ -0,0 +1,465 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Threading;
using System.Threading.Tasks;
using KeystrokeOverlayUI.Controls;
using KeystrokeOverlayUI.Helpers;
using KeystrokeOverlayUI.Models;
using KeystrokeOverlayUI.Services;
using Microsoft.UI;
using Microsoft.UI.Composition.SystemBackdrops;
using Microsoft.UI.Input;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
using Windows.Foundation;
using Windows.Foundation.Collections;
using WinRT.Interop;
namespace KeystrokeOverlayUI
{
// Main overlay window.
public sealed partial class MainWindow : Window, IDisposable
{
public MainViewModel ViewModel { get; set; } = new();
// readonly constants
private readonly InputCursor _dragCursor = InputSystemCursor.Create(InputSystemCursorShape.SizeAll);
private readonly DispatcherTimer _zOrderEnforcer = new();
// core components
private readonly KeystrokeListener _keystrokeListener = new();
private readonly OverlaySettings _overlaySettings = new();
private CancellationTokenSource _startupCancellationSource;
private bool _disposed;
// draggable overlay
private bool _isDragging;
private NativeWindowHelper.POINT _lastCursorPos;
public MainWindow()
{
InitializeComponent();
SystemBackdrop = new MicaBackdrop() { Kind = MicaKind.BaseAlt };
ForceWindowOnTop();
Activated += (s, e) => ApplyOverlayStyles();
_zOrderEnforcer.Interval = TimeSpan.FromMilliseconds(500);
_zOrderEnforcer.Tick += (s, e) => ForceWindowOnTop();
RootGrid.DataContext = ViewModel;
_overlaySettings.SettingsUpdated += (props) =>
{
DispatcherQueue.TryEnqueue(() => ViewModel.ApplySettings(props));
};
_overlaySettings.Initialize();
_keystrokeListener.OnBatchReceived += OnKeyReceived;
_keystrokeListener.Start();
ConfigureOverlayWindow();
RunStartupSequence(isDraggable: ViewModel.IsDraggable);
ViewModel.HotkeyActionTriggered += OnHotkeyActionTriggered;
ViewModel.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(MainViewModel.IsDraggable))
{
RunStartupSequence(isDraggable: ViewModel.IsDraggable);
}
};
}
private void ApplyOverlayStyles()
{
IntPtr hWnd = WindowNative.GetWindowHandle(this);
NativeWindowHelper.SetOverlayWindowStyles(hWnd);
NativeWindowHelper.SetRoundedCorners(hWnd);
ForceWindowOnTop();
}
private void ConfigureOverlayWindow()
{
var appWindow = GetAppWindow();
if (appWindow != null)
{
var presenter = appWindow.Presenter as OverlappedPresenter
?? OverlappedPresenter.Create();
appWindow.SetPresenter(presenter);
presenter.IsAlwaysOnTop = true;
presenter.IsResizable = false;
presenter.IsMinimizable = false;
presenter.IsMaximizable = false;
presenter.SetBorderAndTitleBar(false, false);
}
}
private async void RunStartupSequence(bool isDraggable)
{
_startupCancellationSource?.Cancel();
_startupCancellationSource?.Dispose();
_startupCancellationSource = new CancellationTokenSource();
var token = _startupCancellationSource.Token;
ForceWindowOnTop();
if (!_zOrderEnforcer.IsEnabled)
{
_zOrderEnforcer.Start();
}
try
{
if (isDraggable)
{
// Loop with cancellation check
for (int index = 10; index > 0; index--)
{
ViewModel.RegisterKey($"Drag to Position ({index})", durationMs: 1000, textSize: 30);
await Task.Delay(1000, token);
}
}
// cleanup
ViewModel.ClearKeys();
await Task.Delay(500, token);
}
catch (OperationCanceledException)
{
ViewModel.ClearKeys();
}
finally
{
// cleanup
if (_startupCancellationSource != null)
{
_startupCancellationSource.Dispose();
_startupCancellationSource = null;
}
}
}
private void OnKeyReceived(KeystrokeEvent kEvent)
{
if (_startupCancellationSource != null && !_startupCancellationSource.IsCancellationRequested)
{
_startupCancellationSource.Cancel();
}
DispatcherQueue.TryEnqueue(() =>
{
ViewModel.HandleKeystrokeEvent(kEvent);
if (!_zOrderEnforcer.IsEnabled)
{
_zOrderEnforcer.Start();
}
});
}
// ----------------------
// Hotkey Methods
// ----------------------
private void OnHotkeyActionTriggered(object sender, HotkeyAction action)
{
switch (action)
{
case HotkeyAction.Monitor:
MoveToNextMonitor();
break;
case HotkeyAction.DisplayMode:
case HotkeyAction.Activation:
HandleActivation();
break;
}
ForceWindowOnTop();
// resize to show labels
RootGrid.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
var desiredSize = RootGrid.DesiredSize;
ResizeAppWindow(desiredSize.Width + 5, desiredSize.Height + 15);
}
private void HandleActivation()
{
ShowAppWindow();
}
private void MoveToNextMonitor()
{
IntPtr hWnd = WindowNative.GetWindowHandle(this);
WindowId wndId = Win32Interop.GetWindowIdFromWindow(hWnd);
AppWindow appWindow = AppWindow.GetFromWindowId(wndId);
if (appWindow == null)
{
return;
}
var displayAreas = DisplayArea.FindAll();
if (displayAreas.Count <= 1)
{
return;
}
// Find current display index
DisplayArea currentDisplay = DisplayArea.GetFromWindowId(wndId, DisplayAreaFallback.Primary);
int currentIndex = -1;
for (int i = 0; i < displayAreas.Count; i++)
{
if (displayAreas[i].DisplayId.Value == currentDisplay.DisplayId.Value)
{
currentIndex = i;
break;
}
}
// Calculate Next Index
int nextIndex = (currentIndex + 1) % displayAreas.Count;
DisplayArea nextDisplay = displayAreas[nextIndex];
// move to Top-Left of new monitor
int newX = nextDisplay.WorkArea.X + 15;
int newY = nextDisplay.WorkArea.Y + 12;
appWindow.Move(new Windows.Graphics.PointInt32(newX, newY));
ViewModel.ShowLabel(HotkeyAction.Monitor, $"Monitor {nextIndex + 1}");
}
// ----------------------
// Draggable Overlay
// ----------------------
private void SetRootGridCursor(InputCursor cursor)
{
// Use Reflection to access the protected "ProtectedCursor" property on the Border (RootGrid)
typeof(UIElement)
.GetProperty("ProtectedCursor", BindingFlags.NonPublic | BindingFlags.Instance)
?.SetValue(RootGrid, cursor);
}
private void RootGrid_PointerPressed(object sender, PointerRoutedEventArgs e)
{
var properties = e.GetCurrentPoint(RootGrid).Properties;
if (ViewModel.IsDraggable && properties.IsLeftButtonPressed)
{
_isDragging = true;
RootGrid.CapturePointer(e.Pointer);
_lastCursorPos = NativeWindowHelper.GetCursorPosition();
SetRootGridCursor(_dragCursor);
e.Handled = true;
}
}
private void RootGrid_PointerMoved(object sender, PointerRoutedEventArgs e)
{
if (_isDragging)
{
var currentPos = NativeWindowHelper.GetCursorPosition();
int deltaX = currentPos.X - _lastCursorPos.X;
int deltaY = currentPos.Y - _lastCursorPos.Y;
var appWindow = GetAppWindow();
if (appWindow != null)
{
var newPos = new Windows.Graphics.PointInt32(
appWindow.Position.X + deltaX,
appWindow.Position.Y + deltaY );
appWindow.Move(newPos);
}
_lastCursorPos = currentPos;
}
}
private void RootGrid_PointerReleased(object sender, PointerRoutedEventArgs e)
{
if (_isDragging)
{
_isDragging = false;
RootGrid.ReleasePointerCapture(e.Pointer);
UpdateCursorState();
}
}
private void RootGrid_PointerEntered(object sender, PointerRoutedEventArgs e)
{
UpdateCursorState();
}
private void RootGrid_PointerExited(object sender, PointerRoutedEventArgs e)
{
SetRootGridCursor(null);
}
private void UpdateCursorState()
{
if (ViewModel.IsDraggable)
{
SetRootGridCursor(_dragCursor);
}
else
{
SetRootGridCursor(null);
}
}
// ----------------------
// WinUI Event Handlers
// ----------------------
private void RootGrid_Loaded(object sender, RoutedEventArgs e)
{
RootGrid.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
var desiredSize = RootGrid.DesiredSize;
ResizeAppWindow(desiredSize.Width + 5, desiredSize.Height + 15);
}
private void StackPanel_SizeChanged(object sender, SizeChangedEventArgs e)
{
if (sender is not StackPanel stackPanel)
{
return;
}
stackPanel.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
var desiredSize = stackPanel.DesiredSize;
if (desiredSize.Width == 0 ||
desiredSize.Height == 0 ||
ViewModel.PressedKeys.Count == 0)
{
if (!ViewModel.IsVisibleHotkey)
{
HideAppWindow();
_zOrderEnforcer.Stop();
}
return;
}
ShowAppWindow();
double totalWidth =
desiredSize.Width + RootGrid.Padding.Left + RootGrid.Padding.Right + 5;
double totalHeight =
desiredSize.Height + RootGrid.Padding.Top + RootGrid.Padding.Bottom + 15;
if (ViewModel.IsVisibleHotkey)
{
totalHeight = totalHeight + 30;
}
ResizeAppWindow(totalWidth, totalHeight);
ForceWindowOnTop();
}
// ----------------------
// Window Helper Methods
// ----------------------
private AppWindow GetAppWindow()
{
IntPtr hWnd = WindowNative.GetWindowHandle(this);
WindowId wndId = Win32Interop.GetWindowIdFromWindow(hWnd);
return AppWindow.GetFromWindowId(wndId);
}
private void HideAppWindow()
{
var appWindow = GetAppWindow();
appWindow?.Hide();
}
private void ShowAppWindow()
{
var appWindow = GetAppWindow();
appWindow?.Show();
}
private void ResizeAppWindow(double widthDIPs, double heightDIPs)
{
var appWindow = GetAppWindow();
if (appWindow != null)
{
double scale = RootGrid.XamlRoot.RasterizationScale;
int windowWidth = (int)Math.Ceiling(widthDIPs * scale);
int windowHeight = (int)Math.Ceiling(heightDIPs * scale);
if (appWindow.Size.Width != windowWidth ||
appWindow.Size.Height != windowHeight)
{
appWindow.Resize(new Windows.Graphics.SizeInt32(windowWidth, windowHeight));
}
}
}
private void ForceWindowOnTop()
{
if (ViewModel.PressedKeys.Count == 0)
{
_zOrderEnforcer.Stop();
return;
}
IntPtr hWnd = WindowNative.GetWindowHandle(this);
NativeWindowHelper.EnforceTopMost(hWnd);
}
// -------------------
// Other Methods
// -------------------
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
_keystrokeListener?.Dispose();
_overlaySettings?.Dispose();
}
_disposed = true;
}
}
~MainWindow()
{
Dispose(false);
}
}
}

View File

@@ -0,0 +1,14 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Text.Json.Serialization;
namespace KeystrokeOverlayUI.Models
{
public class BoolProperty
{
[JsonPropertyName("value")]
public bool Value { get; set; }
}
}

View File

@@ -0,0 +1,14 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Text.Json.Serialization;
namespace KeystrokeOverlayUI.Models
{
public class IntProperty
{
[JsonPropertyName("value")]
public int Value { get; set; }
}
}

View File

@@ -0,0 +1,14 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Text.Json.Serialization;
namespace KeystrokeOverlayUI.Models
{
public class StringProperty
{
[JsonPropertyName("value")]
public string Value { get; set; }
}
}

View File

@@ -0,0 +1,20 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace KeystrokeOverlayUI.Models
{
public enum DisplayMode
{
Last5 = 0,
SingleCharactersOnly = 1,
ShortcutsOnly = 2,
Stream = 3,
}
}

View File

@@ -0,0 +1,19 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace KeystrokeOverlayUI.Models
{
public enum HotkeyAction
{
Monitor,
Activation,
DisplayMode,
}
}

View File

@@ -0,0 +1,30 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Text.Json.Serialization;
namespace KeystrokeOverlayUI.Models
{
/// <summary>
/// Single keystroke event emitted by the native Batcher.
/// Matches native JSON fields: t, vk, text, mods, ts.
/// </summary>
public sealed class KeystrokeBatchEvent
{
[JsonPropertyName("t")]
public string Type { get; set; }
[JsonPropertyName("vk")]
public int VirtualKey { get; set; }
[JsonPropertyName("text")]
public string Text { get; set; }
[JsonPropertyName("mods")]
public string[] Modifiers { get; set; }
[JsonPropertyName("ts")]
public double Timestamp { get; set; }
}
}

View File

@@ -0,0 +1,21 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Text.Json.Serialization;
namespace KeystrokeOverlayUI.Models
{
/// <summary>
/// Root object wrapping an array of keystroke events.
/// Matches native JSON: { "schema": 1, "events": [ ... ] }.
/// </summary>
public sealed class KeystrokeBatchRoot
{
[JsonPropertyName("schema")]
public int Schema { get; set; }
[JsonPropertyName("events")]
public KeystrokeBatchEvent[] Events { get; set; }
}
}

View File

@@ -0,0 +1,27 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace KeystrokeOverlayUI.Models
{
public enum KeystrokeAction
{
None, // Do nothing
Add, // Create a new visual bubble (e.g., new word or shortcut)
ReplaceLast, // Update the current bubble (e.g., typing "Hell" -> "Hello")
RemoveLast, // Backspace a full bubble
}
public struct KeystrokeResult
{
public KeystrokeAction Action { get; set; }
public string Text { get; set; }
}
}

View File

@@ -0,0 +1,18 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Text.Json.Serialization;
using KeystrokeOverlayUI.Controls;
using KeystrokeOverlayUI.Models;
namespace KeystrokeOverlayUI
{
[JsonSourceGenerationOptions(PropertyNameCaseInsensitive = true)]
[JsonSerializable(typeof(KeystrokeEvent))]
[JsonSerializable(typeof(KeystrokeBatchRoot))]
[JsonSerializable(typeof(KeystrokeBatchEvent))]
internal sealed partial class KeystrokeEventJsonContext : JsonSerializerContext
{
}
}

View File

@@ -0,0 +1,15 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Text.Json.Serialization;
using KeystrokeOverlayUI.Models;
namespace KeystrokeOverlayUI;
/// Trimming-safe JSON metadata for loading module settings.
[JsonSourceGenerationOptions(PropertyNameCaseInsensitive = true)]
[JsonSerializable(typeof(ModuleSettingsRoot))]
internal sealed partial class ModuleSettingsJsonContext : JsonSerializerContext
{
}

View File

@@ -0,0 +1,57 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Text.Json.Serialization;
using Microsoft.PowerToys.Settings.UI.Library;
using Settings.UI.Library.Attributes;
namespace KeystrokeOverlayUI.Models
{
public class ModuleProperties
{
[CmdConfigureIgnore]
public HotkeySettings DefaultSwitchMonitorHotkey => new HotkeySettings(true, true, false, false, 0x4B);
[CmdConfigureIgnore]
public HotkeySettings DefaultActivationShortcut => new HotkeySettings(true, false, false, true, 0x4B);
[CmdConfigureIgnore]
public HotkeySettings DefaultSwitchDisplayModeHotkey => new HotkeySettings(true, false, false, true, 0x44);
[JsonPropertyName("enable_draggable_overlay")]
public BoolProperty IsDraggable { get; set; } = new() { Value = true };
[JsonPropertyName("activation_shortcut")]
public HotkeySettings ActivationShortcut { get; set; }
[JsonPropertyName("switch_monitor_hotkey")]
public HotkeySettings SwitchMonitorHotkey { get; set; }
[JsonPropertyName("switch_display_mode_hotkey")]
public HotkeySettings SwitchDisplayModeHotkey { get; set; }
[JsonPropertyName("display_mode")]
public IntProperty DisplayMode { get; set; } = new() { Value = 0 };
[JsonPropertyName("overlay_timeout")]
public IntProperty OverlayTimeout { get; set; } = new() { Value = 3000 };
[JsonPropertyName("text_size")]
public IntProperty TextSize { get; set; } = new() { Value = 24 };
[JsonPropertyName("text_color")]
public StringProperty TextColor { get; set; } = new() { Value = "#FF000000" };
[JsonPropertyName("background_color")]
public StringProperty BackgroundColor { get; set; } = new() { Value = "#00000000" };
public ModuleProperties()
{
ActivationShortcut = DefaultActivationShortcut;
SwitchMonitorHotkey = DefaultSwitchMonitorHotkey;
SwitchDisplayModeHotkey = DefaultSwitchDisplayModeHotkey;
}
}
}

View File

@@ -0,0 +1,14 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Text.Json.Serialization;
namespace KeystrokeOverlayUI.Models
{
public class ModuleSettingsRoot
{
[JsonPropertyName("properties")]
public ModuleProperties Properties { get; set; } = new();
}
}

View File

@@ -0,0 +1,142 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Pipes;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using KeystrokeOverlayUI;
using KeystrokeOverlayUI.Controls;
using KeystrokeOverlayUI.Models;
namespace KeystrokeOverlayUI.Services
{
// Connects to the native KeystrokeOverlayPipe and converts native JSON
// batches into KeystrokeEvent objects for the overlay UI.
public class KeystrokeListener : IDisposable
{
private const string PipeName = "KeystrokeOverlayPipe";
private CancellationTokenSource _cts;
public event Action<KeystrokeEvent> OnBatchReceived;
public void Start()
{
_cts = new CancellationTokenSource();
Task.Run(() => ListenLoop(_cts.Token));
}
public void Stop()
{
_cts?.Cancel();
}
public void Dispose()
{
Stop();
GC.SuppressFinalize(this);
}
private async Task ListenLoop(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
try
{
using var client = new NamedPipeClientStream(
serverName: ".",
pipeName: PipeName,
direction: PipeDirection.In,
options: PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly);
await client.ConnectAsync(token).ConfigureAwait(false);
using var reader = new BinaryReader(client, Encoding.UTF8, leaveOpen: false);
while (client.IsConnected && !token.IsCancellationRequested)
{
// Length-prefixed frame
int length = reader.ReadInt32();
const int MaxFrameSize = 8 * 1024 * 1024;
if (length <= 0 || length > MaxFrameSize)
{
Debug.WriteLine($"KeystrokeListener: invalid frame length {length}, reconnecting");
break;
}
// Read JSON payload
byte[] buffer = reader.ReadBytes(length);
if (buffer.Length != length)
{
Debug.WriteLine($"KeystrokeListener: short read {buffer.Length}/{length}, reconnecting");
break;
}
string json = Encoding.UTF8.GetString(buffer);
try
{
// Deserialize batch
var root = JsonSerializer.Deserialize(
json,
KeystrokeEventJsonContext.Default.KeystrokeBatchRoot);
if (root?.Events == null || root.Events.Length == 0)
{
continue;
}
// Process only DOWN events to avoid duplicates
foreach (var e in root.Events)
{
bool isDown = string.Equals(e.Type, "down", StringComparison.OrdinalIgnoreCase);
bool isChar = string.Equals(e.Type, "char", StringComparison.OrdinalIgnoreCase);
if (!isDown && !isChar)
{
continue;
}
var uiEvent = new KeystrokeEvent
{
VirtualKey = (uint)e.VirtualKey,
IsPressed = true, // Both down and char imply a press for UI purposes
Modifiers = e.Modifiers != null ? new List<string>(e.Modifiers) : new List<string>(),
Text = e.Text, // Capture the text from backend
EventType = e.Type, // Capture the type
};
OnBatchReceived?.Invoke(uiEvent);
}
}
catch (JsonException je)
{
Debug.WriteLine($"KeystrokeListener: JSON parse error: {je.Message}");
}
catch (Exception ex)
{
Debug.WriteLine($"KeystrokeListener: error processing frame: {ex}");
}
}
}
catch (OperationCanceledException)
{
break;
}
catch (Exception ex)
{
Debug.WriteLine($"KeystrokeListener: connect error: {ex}");
await Task.Delay(1000, token).ConfigureAwait(false);
}
}
}
}
}

View File

@@ -0,0 +1,120 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using KeystrokeOverlayUI.Controls;
using KeystrokeOverlayUI.Models;
namespace KeystrokeOverlayUI.Services
{
public class KeystrokeProcessor
{
private string _streamBuffer = string.Empty;
/// <summary>
/// Determines what visual action to take based on the incoming key and current mode.
/// </summary>
public KeystrokeResult Process(KeystrokeEvent kEvent, DisplayMode displayMode)
{
string formattedText = kEvent.ToString();
// Early out for no text
if (string.IsNullOrEmpty(formattedText))
{
return new KeystrokeResult { Action = KeystrokeAction.None };
}
bool isShortcut = kEvent.IsShortcut;
// Mode-specific logic
switch (displayMode)
{
case DisplayMode.Last5:
return new KeystrokeResult { Action = KeystrokeAction.Add, Text = formattedText };
case DisplayMode.SingleCharactersOnly:
if (isShortcut)
{
return new KeystrokeResult { Action = KeystrokeAction.None };
}
return new KeystrokeResult { Action = KeystrokeAction.Add, Text = formattedText };
case DisplayMode.ShortcutsOnly:
if (!isShortcut)
{
return new KeystrokeResult { Action = KeystrokeAction.None };
}
return new KeystrokeResult { Action = KeystrokeAction.Add, Text = formattedText };
case DisplayMode.Stream:
return ProcessStreamMode(kEvent, isShortcut, formattedText);
default:
return new KeystrokeResult { Action = KeystrokeAction.None };
}
}
private KeystrokeResult ProcessStreamMode(KeystrokeEvent kEvent, bool isShortcut, string formattedText)
{
// handle Backspace
if (kEvent.VirtualKey == (uint)Windows.System.VirtualKey.Back)
{
if (_streamBuffer.Length > 0)
{
_streamBuffer = _streamBuffer.Substring(0, _streamBuffer.Length - 1);
if (string.IsNullOrEmpty(_streamBuffer))
{
return new KeystrokeResult { Action = KeystrokeAction.RemoveLast };
}
else
{
return new KeystrokeResult { Action = KeystrokeAction.ReplaceLast, Text = _streamBuffer };
}
}
return new KeystrokeResult { Action = KeystrokeAction.None };
}
// If a shortcut is pressed (and it's not Space), we reset the stream.
if (isShortcut && kEvent.VirtualKey != (uint)Windows.System.VirtualKey.Space)
{
ResetBuffer();
return new KeystrokeResult { Action = KeystrokeAction.Add, Text = formattedText };
}
// If it's a space/tab, we clear the buffer so the next character starts a new bubble.
if (string.IsNullOrWhiteSpace(kEvent.Text))
{
ResetBuffer();
return new KeystrokeResult { Action = KeystrokeAction.None };
}
_streamBuffer += kEvent.Text;
// If this is the start of a word (Length 1), Add it.
// If we are appending to a word (Length > 1), Replace the last bubble.
if (_streamBuffer.Length == 1)
{
return new KeystrokeResult { Action = KeystrokeAction.Add, Text = _streamBuffer };
}
else
{
return new KeystrokeResult { Action = KeystrokeAction.ReplaceLast, Text = _streamBuffer };
}
}
public void ResetBuffer()
{
_streamBuffer = string.Empty;
}
}
}

View File

@@ -0,0 +1,130 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.ComponentModel;
using System.Globalization;
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using KeystrokeOverlayUI;
using KeystrokeOverlayUI.Models;
using Microsoft.UI;
using Microsoft.UI.Xaml.Media;
using Windows.UI;
namespace KeystrokeOverlayUI.Services
{
public class OverlaySettings : IDisposable
{
// Event to notify ViewModel when settings change
public event Action<ModuleProperties> SettingsUpdated;
private const string ModuleName = "KeystrokeOverlay";
private readonly string _settingsFilePath;
private FileSystemWatcher _watcher;
public OverlaySettings()
{
// Path: %LOCALAPPDATA%\Microsoft\PowerToys\KeystrokeOverlay\settings.json
string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
_settingsFilePath = Path.Combine(localAppData, "Microsoft", "PowerToys", ModuleName, "settings.json");
}
public void Initialize()
{
// 1. Load initial settings
LoadSettings();
// 2. Watch for changes
SetupWatcher();
}
private void SetupWatcher()
{
try
{
string folder = Path.GetDirectoryName(_settingsFilePath);
if (string.IsNullOrEmpty(folder) || !Directory.Exists(folder))
{
return;
}
_watcher = new FileSystemWatcher(folder, "settings.json")
{
NotifyFilter = NotifyFilters.LastWrite,
EnableRaisingEvents = true,
};
// Debounce logic (FileWatcher often fires twice)
DateTime lastRead = DateTime.MinValue;
_watcher.Changed += (s, e) =>
{
if ((DateTime.Now - lastRead).TotalMilliseconds < 500)
{
return;
}
lastRead = DateTime.Now;
// Give the writing process (dllmain) a moment to close the file
Task.Delay(100).ContinueWith(_ => LoadSettings());
};
}
catch
{
// Watcher might fail if permissions are weird, just ignore
}
}
private void LoadSettings()
{
if (!File.Exists(_settingsFilePath))
{
return;
}
try
{
// Retry loop in case file is locked by dllmain
for (int i = 0; i < 3; i++)
{
try
{
string json = File.ReadAllText(_settingsFilePath);
var root = JsonSerializer.Deserialize(json, ModuleSettingsJsonContext.Default.ModuleSettingsRoot);
if (root?.Properties != null)
{
SettingsUpdated?.Invoke(root.Properties);
}
break;
}
catch (IOException)
{
System.Threading.Thread.Sleep(50);
}
}
}
catch
{
// Log error or ignore
}
}
public void Dispose()
{
if (_watcher != null)
{
_watcher.Dispose();
_watcher = null;
}
GC.SuppressFinalize(this);
}
}
}

View File

@@ -0,0 +1,129 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Runtime.InteropServices;
namespace KeystrokeOverlayUI.Services
{
/// <summary>
/// Uses a Windows Job Object to ensure child processes terminate when this
/// process exits — even if killed via Task Manager.
/// </summary>
public sealed class ProcessJob : IDisposable
{
private IntPtr _hJob;
public ProcessJob()
{
_hJob = CreateJobObject(IntPtr.Zero, null);
var info = new JOBOBJECT_BASIC_LIMIT_INFORMATION
{
LimitFlags = JOBOBJECTLIMIT.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
};
var extendedInfo = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION
{
BasicLimitInformation = info,
};
int length = Marshal.SizeOf(typeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION));
IntPtr ptr = Marshal.AllocHGlobal(length);
Marshal.StructureToPtr(extendedInfo, ptr, false);
_ = SetInformationJobObject(
_hJob,
JobObjectInfoType.ExtendedLimitInformation,
ptr,
(uint)length);
Marshal.FreeHGlobal(ptr);
}
/// <summary>
/// Adds a process to the job object.
/// </summary>
public void AddProcess(IntPtr processHandle)
{
_ = AssignProcessToJobObject(_hJob, processHandle);
}
/// <inheritdoc/>
public void Dispose()
{
if (_hJob != IntPtr.Zero)
{
_ = CloseHandle(_hJob);
_hJob = IntPtr.Zero;
}
}
// ====================================================================
// P/Invoke
// ====================================================================
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
private static extern IntPtr CreateJobObject(IntPtr jobAttributes, string name);
[DllImport("kernel32.dll")]
private static extern bool SetInformationJobObject(
IntPtr job,
JobObjectInfoType infoType,
IntPtr jobObjectInfo,
uint jobObjectInfoLength);
[DllImport("kernel32.dll")]
private static extern bool AssignProcessToJobObject(IntPtr job, IntPtr process);
[DllImport("kernel32.dll")]
private static extern bool CloseHandle(IntPtr handle);
private enum JobObjectInfoType
{
ExtendedLimitInformation = 9,
}
[Flags]
private enum JOBOBJECTLIMIT : uint
{
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000,
}
[StructLayout(LayoutKind.Sequential)]
private struct JOBOBJECT_BASIC_LIMIT_INFORMATION
{
public long PerProcessUserTimeLimit;
public long PerJobUserTimeLimit;
public JOBOBJECTLIMIT LimitFlags;
public UIntPtr MinimumWorkingSetSize;
public UIntPtr MaximumWorkingSetSize;
public uint ActiveProcessLimit;
public long Affinity;
public uint PriorityClass;
public uint SchedulingClass;
}
[StructLayout(LayoutKind.Sequential)]
private struct IO_COUNTERS
{
public ulong ReadOperationCount;
public ulong WriteOperationCount;
public ulong OtherOperationCount;
public ulong ReadTransferCount;
public ulong WriteTransferCount;
public ulong OtherTransferCount;
}
[StructLayout(LayoutKind.Sequential)]
private struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION
{
public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation;
public IO_COUNTERS IoInfo;
public UIntPtr ProcessMemoryLimit;
public UIntPtr JobMemoryLimit;
public UIntPtr PeakProcessMemoryUsed;
public UIntPtr PeakJobMemoryUsed;
}
}
}

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</windowsSettings>
</application>
</assembly>

View File

@@ -50,6 +50,7 @@ enum class ScheduleMode
Off,
FixedHours,
SunsetToSunrise,
FollowNightLight,
// add more later
};
@@ -61,6 +62,8 @@ inline std::wstring ToString(ScheduleMode mode)
return L"SunsetToSunrise";
case ScheduleMode::FixedHours:
return L"FixedHours";
case ScheduleMode::FollowNightLight:
return L"FollowNightLight";
default:
return L"Off";
}
@@ -72,6 +75,8 @@ inline ScheduleMode FromString(const std::wstring& str)
return ScheduleMode::SunsetToSunrise;
if (str == L"FixedHours")
return ScheduleMode::FixedHours;
if (str == L"FollowNightLight")
return ScheduleMode::FollowNightLight;
return ScheduleMode::Off;
}
@@ -167,7 +172,9 @@ public:
ToString(g_settings.m_scheduleMode),
{ { L"Off", L"Disable the schedule" },
{ L"FixedHours", L"Set hours manually" },
{ L"SunsetToSunrise", L"Use sunrise/sunset times" } });
{ L"SunsetToSunrise", L"Use sunrise/sunset times" },
{ L"FollowNightLight", L"Follow Windows Night Light state" }
});
// Integer spinners
settings.add_int_spinner(

View File

@@ -1,4 +1,4 @@
#include <windows.h>
#include <windows.h>
#include <tchar.h>
#include "ThemeScheduler.h"
#include "ThemeHelper.h"
@@ -13,10 +13,12 @@
#include <utils/logger_helper.h>
#include "LightSwitchStateManager.h"
#include <LightSwitchUtils.h>
#include <NightLightRegistryObserver.h>
SERVICE_STATUS g_ServiceStatus = {};
SERVICE_STATUS_HANDLE g_StatusHandle = nullptr;
HANDLE g_ServiceStopEvent = nullptr;
static LightSwitchStateManager* g_stateManagerPtr = nullptr;
VOID WINAPI ServiceMain(DWORD argc, LPTSTR* argv);
VOID WINAPI ServiceCtrlHandler(DWORD dwCtrl);
@@ -168,7 +170,15 @@ static void DetectAndHandleExternalThemeChange(LightSwitchStateManager& stateMan
}
// Use shared helper (handles wraparound logic)
bool shouldBeLight = ShouldBeLight(nowMinutes, effectiveLight, effectiveDark);
bool shouldBeLight = false;
if (s.scheduleMode == ScheduleMode::FollowNightLight)
{
shouldBeLight = !IsNightLightEnabled();
}
else
{
shouldBeLight = ShouldBeLight(nowMinutes, effectiveLight, effectiveDark);
}
// Compare current system/apps theme
bool currentSystemLight = GetCurrentSystemTheme();
@@ -199,15 +209,40 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
// Initialization
// ────────────────────────────────────────────────────────────────
static LightSwitchStateManager stateManager;
g_stateManagerPtr = &stateManager;
LightSwitchSettings::instance().InitFileWatcher();
HANDLE hManualOverride = OpenEventW(SYNCHRONIZE | EVENT_MODIFY_STATE, FALSE, L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE");
HANDLE hSettingsChanged = LightSwitchSettings::instance().GetSettingsChangedEvent();
static std::unique_ptr<NightLightRegistryObserver> g_nightLightWatcher;
LightSwitchSettings::instance().LoadSettings();
const auto& settings = LightSwitchSettings::instance().settings();
// after loading settings:
bool nightLightNeeded = (settings.scheduleMode == ScheduleMode::FollowNightLight);
if (nightLightNeeded && !g_nightLightWatcher)
{
Logger::info(L"[LightSwitchService] Starting Night Light registry watcher...");
g_nightLightWatcher = std::make_unique<NightLightRegistryObserver>(
HKEY_CURRENT_USER,
NIGHT_LIGHT_REGISTRY_PATH,
[]() {
if (g_stateManagerPtr)
g_stateManagerPtr->OnNightLightChange();
});
}
else if (!nightLightNeeded && g_nightLightWatcher)
{
Logger::info(L"[LightSwitchService] Stopping Night Light registry watcher...");
g_nightLightWatcher->Stop();
g_nightLightWatcher.reset();
}
SYSTEMTIME st;
GetLocalTime(&st);
int nowMinutes = st.wHour * 60 + st.wMinute;
@@ -274,6 +309,31 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
ResetEvent(hSettingsChanged);
LightSwitchSettings::instance().LoadSettings();
stateManager.OnSettingsChanged();
const auto& settings = LightSwitchSettings::instance().settings();
bool nightLightNeeded = (settings.scheduleMode == ScheduleMode::FollowNightLight);
if (nightLightNeeded && !g_nightLightWatcher)
{
Logger::info(L"[LightSwitchService] Starting Night Light registry watcher...");
g_nightLightWatcher = std::make_unique<NightLightRegistryObserver>(
HKEY_CURRENT_USER,
NIGHT_LIGHT_REGISTRY_PATH,
[]() {
if (g_stateManagerPtr)
g_stateManagerPtr->OnNightLightChange();
});
stateManager.OnNightLightChange();
}
else if (!nightLightNeeded && g_nightLightWatcher)
{
Logger::info(L"[LightSwitchService] Stopping Night Light registry watcher...");
g_nightLightWatcher->Stop();
g_nightLightWatcher.reset();
}
continue;
}
}
@@ -285,6 +345,11 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
CloseHandle(hManualOverride);
if (hParent)
CloseHandle(hParent);
if (g_nightLightWatcher)
{
g_nightLightWatcher->Stop();
g_nightLightWatcher.reset();
}
Logger::info(L"[LightSwitchService] Worker thread exiting cleanly.");
return 0;

View File

@@ -76,6 +76,7 @@
<ClCompile Include="LightSwitchService.cpp" />
<ClCompile Include="LightSwitchSettings.cpp" />
<ClCompile Include="LightSwitchStateManager.cpp" />
<ClCompile Include="NightLightRegistryObserver.cpp" />
<ClCompile Include="SettingsConstants.cpp" />
<ClCompile Include="ThemeHelper.cpp" />
<ClCompile Include="ThemeScheduler.cpp" />
@@ -88,6 +89,7 @@
<ClInclude Include="LightSwitchSettings.h" />
<ClInclude Include="LightSwitchStateManager.h" />
<ClInclude Include="LightSwitchUtils.h" />
<ClInclude Include="NightLightRegistryObserver.h" />
<ClInclude Include="SettingsConstants.h" />
<ClInclude Include="SettingsObserver.h" />
<ClInclude Include="ThemeHelper.h" />

View File

@@ -36,6 +36,9 @@
<ClCompile Include="LightSwitchStateManager.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="NightLightRegistryObserver.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="ThemeScheduler.h">
@@ -62,6 +65,9 @@
<ClInclude Include="LightSwitchUtils.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="NightLightRegistryObserver.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<Natvis Include="$(MSBuildThisFileDirectory)..\..\natvis\wil.natvis" />

View File

@@ -19,7 +19,8 @@ enum class ScheduleMode
{
Off,
FixedHours,
SunsetToSunrise
SunsetToSunrise,
FollowNightLight,
// Add more in the future
};
@@ -31,6 +32,8 @@ inline std::wstring ToString(ScheduleMode mode)
return L"FixedHours";
case ScheduleMode::SunsetToSunrise:
return L"SunsetToSunrise";
case ScheduleMode::FollowNightLight:
return L"FollowNightLight";
default:
return L"Off";
}
@@ -42,6 +45,8 @@ inline ScheduleMode FromString(const std::wstring& str)
return ScheduleMode::SunsetToSunrise;
if (str == L"FixedHours")
return ScheduleMode::FixedHours;
if (str == L"FollowNightLight")
return ScheduleMode::FollowNightLight;
else
return ScheduleMode::Off;
}

View File

@@ -31,7 +31,10 @@ void LightSwitchStateManager::OnSettingsChanged()
void LightSwitchStateManager::OnTick(int currentMinutes)
{
std::lock_guard<std::mutex> lock(_stateMutex);
EvaluateAndApplyIfNeeded();
if (_state.lastAppliedMode != ScheduleMode::FollowNightLight)
{
EvaluateAndApplyIfNeeded();
}
}
// Called when manual override is triggered
@@ -49,8 +52,38 @@ void LightSwitchStateManager::OnManualOverride()
_state.isAppsLightActive = GetCurrentAppsTheme();
Logger::debug(L"[LightSwitchStateManager] Synced internal theme state to current system theme ({}) and apps theme ({}).",
(_state.isSystemLightActive ? L"light" : L"dark"),
(_state.isAppsLightActive ? L"light" : L"dark"));
(_state.isSystemLightActive ? L"light" : L"dark"),
(_state.isAppsLightActive ? L"light" : L"dark"));
}
EvaluateAndApplyIfNeeded();
}
// Runs with the registry observer detects a change in Night Light settings.
void LightSwitchStateManager::OnNightLightChange()
{
std::lock_guard<std::mutex> lock(_stateMutex);
bool newNightLightState = IsNightLightEnabled();
// In Follow Night Light mode, treat a Night Light toggle as a boundary
if (_state.lastAppliedMode == ScheduleMode::FollowNightLight && _state.isManualOverride)
{
Logger::info(L"[LightSwitchStateManager] Night Light changed while manual override active; "
L"treating as a boundary and clearing manual override.");
_state.isManualOverride = false;
}
if (newNightLightState != _state.isNightLightActive)
{
Logger::info(L"[LightSwitchStateManager] Night Light toggled to {}",
newNightLightState ? L"ON" : L"OFF");
_state.isNightLightActive = newNightLightState;
}
else
{
Logger::debug(L"[LightSwitchStateManager] Night Light change event fired, but no actual change.");
}
EvaluateAndApplyIfNeeded();
@@ -77,9 +110,9 @@ void LightSwitchStateManager::SyncInitialThemeState()
_state.isSystemLightActive = GetCurrentSystemTheme();
_state.isAppsLightActive = GetCurrentAppsTheme();
Logger::debug(L"[LightSwitchStateManager] Synced initial state to current system theme ({})",
_state.isSystemLightActive ? L"light" : L"dark");
_state.isSystemLightActive ? L"light" : L"dark");
Logger::debug(L"[LightSwitchStateManager] Synced initial state to current apps theme ({})",
_state.isAppsLightActive ? L"light" : L"dark");
_state.isAppsLightActive ? L"light" : L"dark");
}
static std::pair<int, int> update_sun_times(auto& settings)
@@ -194,7 +227,15 @@ void LightSwitchStateManager::EvaluateAndApplyIfNeeded()
_state.lastAppliedMode = _currentSettings.scheduleMode;
bool shouldBeLight = ShouldBeLight(now, _state.effectiveLightMinutes, _state.effectiveDarkMinutes);
bool shouldBeLight = false;
if (_currentSettings.scheduleMode == ScheduleMode::FollowNightLight)
{
shouldBeLight = !_state.isNightLightActive;
}
else
{
shouldBeLight = ShouldBeLight(now, _state.effectiveLightMinutes, _state.effectiveDarkMinutes);
}
bool appsNeedsToChange = _currentSettings.changeApps && (_state.isAppsLightActive != shouldBeLight);
bool systemNeedsToChange = _currentSettings.changeSystem && (_state.isSystemLightActive != shouldBeLight);
@@ -227,6 +268,3 @@ void LightSwitchStateManager::EvaluateAndApplyIfNeeded()
_state.lastTickMinutes = now;
}

View File

@@ -9,6 +9,7 @@ struct LightSwitchState
bool isManualOverride = false;
bool isSystemLightActive = false;
bool isAppsLightActive = false;
bool isNightLightActive = false;
int lastEvaluatedDay = -1;
int lastTickMinutes = -1;
@@ -32,6 +33,9 @@ public:
// Called when manual override is toggled (via shortcut or system change).
void OnManualOverride();
// Called when night light changes in windows settings
void OnNightLightChange();
// Initial sync at startup to align internal state with system theme
void SyncInitialThemeState();

View File

@@ -0,0 +1 @@
#include "NightLightRegistryObserver.h"

View File

@@ -0,0 +1,134 @@
#pragma once
#include <wtypes.h>
#include <string>
#include <functional>
#include <thread>
#include <atomic>
#include <mutex>
class NightLightRegistryObserver
{
public:
NightLightRegistryObserver(HKEY root, const std::wstring& subkey, std::function<void()> callback) :
_root(root), _subkey(subkey), _callback(std::move(callback)), _stop(false)
{
_thread = std::thread([this]() { this->Run(); });
}
~NightLightRegistryObserver()
{
Stop();
}
void Stop()
{
_stop = true;
{
std::lock_guard<std::mutex> lock(_mutex);
if (_event)
SetEvent(_event);
}
if (_thread.joinable())
_thread.join();
std::lock_guard<std::mutex> lock(_mutex);
if (_hKey)
{
RegCloseKey(_hKey);
_hKey = nullptr;
}
if (_event)
{
CloseHandle(_event);
_event = nullptr;
}
}
private:
void Run()
{
{
std::lock_guard<std::mutex> lock(_mutex);
if (RegOpenKeyExW(_root, _subkey.c_str(), 0, KEY_NOTIFY, &_hKey) != ERROR_SUCCESS)
return;
_event = CreateEventW(nullptr, TRUE, FALSE, nullptr);
if (!_event)
{
RegCloseKey(_hKey);
_hKey = nullptr;
return;
}
}
while (!_stop)
{
HKEY hKeyLocal = nullptr;
HANDLE eventLocal = nullptr;
{
std::lock_guard<std::mutex> lock(_mutex);
if (_stop)
break;
hKeyLocal = _hKey;
eventLocal = _event;
}
if (!hKeyLocal || !eventLocal)
break;
if (_stop)
break;
if (RegNotifyChangeKeyValue(hKeyLocal, FALSE, REG_NOTIFY_CHANGE_LAST_SET, eventLocal, TRUE) != ERROR_SUCCESS)
break;
DWORD wait = WaitForSingleObject(eventLocal, INFINITE);
if (_stop || wait == WAIT_FAILED)
break;
ResetEvent(eventLocal);
if (!_stop && _callback)
{
try
{
_callback();
}
catch (...)
{
}
}
}
{
std::lock_guard<std::mutex> lock(_mutex);
if (_hKey)
{
RegCloseKey(_hKey);
_hKey = nullptr;
}
if (_event)
{
CloseHandle(_event);
_event = nullptr;
}
}
}
HKEY _root;
std::wstring _subkey;
std::function<void()> _callback;
HANDLE _event = nullptr;
HKEY _hKey = nullptr;
std::thread _thread;
std::atomic<bool> _stop;
std::mutex _mutex;
};

View File

@@ -11,4 +11,7 @@ enum class SettingId
Sunset_Offset,
ChangeSystem,
ChangeApps
};
};
constexpr wchar_t PERSONALIZATION_REGISTRY_PATH[] = L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize";
constexpr wchar_t NIGHT_LIGHT_REGISTRY_PATH[] = L"Software\\Microsoft\\Windows\\CurrentVersion\\CloudStore\\Store\\DefaultAccount\\Current\\default$windows.data.bluelightreduction.bluelightreductionstate\\windows.data.bluelightreduction.bluelightreductionstate";

View File

@@ -3,6 +3,7 @@
#include <logger/logger.h>
#include <utils/logger_helper.h>
#include "ThemeHelper.h"
#include <SettingsConstants.h>
// Controls changing the themes.
@@ -10,7 +11,7 @@ static void ResetColorPrevalence()
{
HKEY hKey;
if (RegOpenKeyEx(HKEY_CURRENT_USER,
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
PERSONALIZATION_REGISTRY_PATH,
0,
KEY_SET_VALUE,
&hKey) == ERROR_SUCCESS)
@@ -31,7 +32,7 @@ void SetAppsTheme(bool mode)
{
HKEY hKey;
if (RegOpenKeyEx(HKEY_CURRENT_USER,
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
PERSONALIZATION_REGISTRY_PATH,
0,
KEY_SET_VALUE,
&hKey) == ERROR_SUCCESS)
@@ -50,7 +51,7 @@ void SetSystemTheme(bool mode)
{
HKEY hKey;
if (RegOpenKeyEx(HKEY_CURRENT_USER,
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
PERSONALIZATION_REGISTRY_PATH,
0,
KEY_SET_VALUE,
&hKey) == ERROR_SUCCESS)
@@ -79,7 +80,7 @@ bool GetCurrentSystemTheme()
DWORD size = sizeof(value);
if (RegOpenKeyEx(HKEY_CURRENT_USER,
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
PERSONALIZATION_REGISTRY_PATH,
0,
KEY_READ,
&hKey) == ERROR_SUCCESS)
@@ -98,7 +99,7 @@ bool GetCurrentAppsTheme()
DWORD size = sizeof(value);
if (RegOpenKeyEx(HKEY_CURRENT_USER,
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
PERSONALIZATION_REGISTRY_PATH,
0,
KEY_READ,
&hKey) == ERROR_SUCCESS)
@@ -109,3 +110,30 @@ bool GetCurrentAppsTheme()
return value == 1; // true = light, false = dark
}
bool IsNightLightEnabled()
{
HKEY hKey;
const wchar_t* path = NIGHT_LIGHT_REGISTRY_PATH;
if (RegOpenKeyExW(HKEY_CURRENT_USER, path, 0, KEY_READ, &hKey) != ERROR_SUCCESS)
return false;
// RegGetValueW will set size to the size of the data and we expect that to be at least 25 bytes (we need to access bytes 23 and 24)
DWORD size = 0;
if (RegGetValueW(hKey, nullptr, L"Data", RRF_RT_REG_BINARY, nullptr, nullptr, &size) != ERROR_SUCCESS || size < 25)
{
RegCloseKey(hKey);
return false;
}
std::vector<BYTE> data(size);
if (RegGetValueW(hKey, nullptr, L"Data", RRF_RT_REG_BINARY, nullptr, data.data(), &size) != ERROR_SUCCESS)
{
RegCloseKey(hKey);
return false;
}
RegCloseKey(hKey);
return data[23] == 0x10 && data[24] == 0x00;
}

View File

@@ -3,3 +3,4 @@ void SetSystemTheme(bool dark);
void SetAppsTheme(bool dark);
bool GetCurrentSystemTheme();
bool GetCurrentAppsTheme();
bool IsNightLightEnabled();

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Microsoft.Windows.CppWinRT" version="2.0.240111.5" targetFramework="native" />
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.231216.1" targetFramework="native" />

View File

@@ -1,16 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Label="NuGet">
<!-- Tell NuGet this is PackageReference style -->
<RestoreProjectStyle>PackageReference</RestoreProjectStyle>
<!-- Tell NuGet we're a native project -->
<NuGetTargetMoniker>native,Version=v0.0</NuGetTargetMoniker>
<!-- Tell NuGet we target Windows (use your existing WindowsTargetPlatformVersion) -->
<NuGetTargetPlatformIdentifier>Windows</NuGetTargetPlatformIdentifier>
<NuGetTargetPlatformVersion>$(WindowsTargetPlatformVersion)</NuGetTargetPlatformVersion>
</PropertyGroup>
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.1.8.250907003\build\native\Microsoft.WindowsAppSDK.props" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.8.250907003\build\native\Microsoft.WindowsAppSDK.props')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.Base.1.8.250831001\build\native\Microsoft.WindowsAppSDK.Base.props" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.Base.1.8.250831001\build\native\Microsoft.WindowsAppSDK.Base.props')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.Foundation.1.8.250906002\build\native\Microsoft.WindowsAppSDK.Foundation.props" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.Foundation.1.8.250906002\build\native\Microsoft.WindowsAppSDK.Foundation.props')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.WinUI.1.8.250906003\build\native\Microsoft.WindowsAppSDK.WinUI.props" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.WinUI.1.8.250906003\build\native\Microsoft.WindowsAppSDK.WinUI.props')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.Runtime.1.8.250907003\build\native\Microsoft.WindowsAppSDK.Runtime.props" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.Runtime.1.8.250907003\build\native\Microsoft.WindowsAppSDK.Runtime.props')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.DWrite.1.8.25090401\build\Microsoft.WindowsAppSDK.DWrite.props" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.DWrite.1.8.25090401\build\Microsoft.WindowsAppSDK.DWrite.props')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.InteractiveExperiences.1.8.250906004\build\native\Microsoft.WindowsAppSDK.InteractiveExperiences.props" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.InteractiveExperiences.1.8.250906004\build\native\Microsoft.WindowsAppSDK.InteractiveExperiences.props')" />
<Import Project="..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.26100.4188\build\Microsoft.Windows.SDK.BuildTools.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.26100.4188\build\Microsoft.Windows.SDK.BuildTools.props')" />
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" />
<PropertyGroup Label="Globals">
<CppWinRTOptimized>true</CppWinRTOptimized>
<CppWinRTRootNamespaceAutoMerge>true</CppWinRTRootNamespaceAutoMerge>
@@ -33,11 +31,6 @@
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
<EnablePreviewMsixTooling>true</EnablePreviewMsixTooling>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.WindowsAppSDK" GeneratePathProperty="true" />
<PackageReference Include="Microsoft.Windows.CppWinRT" GeneratePathProperty="true" />
<PackageReference Include="Microsoft.Windows.ImplementationLibrary" GeneratePathProperty="true" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
@@ -45,6 +38,7 @@
<DesktopCompatible>true</DesktopCompatible>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<Import Project="..\..\..\..\deps\spdlog.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="PropertySheets">
@@ -124,6 +118,9 @@
<WarnAsError>true</WarnAsError>
</Midl>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\common\Display\Display.vcxproj">
<Project>{caba8dfb-823b-4bf2-93ac-3f31984150d9}</Project>
@@ -145,5 +142,42 @@
<ResourceCompile Include="PowerToys.MeasureToolCore.rc" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<Import Project="..\..\..\..\deps\spdlog.props" />
<ImportGroup Label="ExtensionTargets">
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.26100.4188\build\Microsoft.Windows.SDK.BuildTools.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.26100.4188\build\Microsoft.Windows.SDK.BuildTools.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.1.8.250907003\build\native\Microsoft.WindowsAppSDK.targets" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.8.250907003\build\native\Microsoft.WindowsAppSDK.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.Base.1.8.250831001\build\native\Microsoft.WindowsAppSDK.Base.targets" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.Base.1.8.250831001\build\native\Microsoft.WindowsAppSDK.Base.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.Foundation.1.8.250906002\build\native\Microsoft.WindowsAppSDK.Foundation.targets" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.Foundation.1.8.250906002\build\native\Microsoft.WindowsAppSDK.Foundation.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.WinUI.1.8.250906003\build\native\Microsoft.WindowsAppSDK.WinUI.targets" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.WinUI.1.8.250906003\build\native\Microsoft.WindowsAppSDK.WinUI.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.Runtime.1.8.250907003\build\native\Microsoft.WindowsAppSDK.Runtime.targets" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.Runtime.1.8.250907003\build\native\Microsoft.WindowsAppSDK.Runtime.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.DWrite.1.8.25090401\build\Microsoft.WindowsAppSDK.DWrite.targets" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.DWrite.1.8.25090401\build\Microsoft.WindowsAppSDK.DWrite.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.InteractiveExperiences.1.8.250906004\build\native\Microsoft.WindowsAppSDK.InteractiveExperiences.targets" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.InteractiveExperiences.1.8.250906004\build\native\Microsoft.WindowsAppSDK.InteractiveExperiences.targets')" />
</ImportGroup>
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.26100.4188\build\Microsoft.Windows.SDK.BuildTools.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.26100.4188\build\Microsoft.Windows.SDK.BuildTools.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.26100.4188\build\Microsoft.Windows.SDK.BuildTools.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.26100.4188\build\Microsoft.Windows.SDK.BuildTools.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.8.250907003\build\native\Microsoft.WindowsAppSDK.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.1.8.250907003\build\native\Microsoft.WindowsAppSDK.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.8.250907003\build\native\Microsoft.WindowsAppSDK.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.1.8.250907003\build\native\Microsoft.WindowsAppSDK.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.Base.1.8.250831001\build\native\Microsoft.WindowsAppSDK.Base.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.Base.1.8.250831001\build\native\Microsoft.WindowsAppSDK.Base.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.Base.1.8.250831001\build\native\Microsoft.WindowsAppSDK.Base.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.Base.1.8.250831001\build\native\Microsoft.WindowsAppSDK.Base.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.Foundation.1.8.250906002\build\native\Microsoft.WindowsAppSDK.Foundation.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.Foundation.1.8.250906002\build\native\Microsoft.WindowsAppSDK.Foundation.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.Foundation.1.8.250906002\build\native\Microsoft.WindowsAppSDK.Foundation.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.Foundation.1.8.250906002\build\native\Microsoft.WindowsAppSDK.Foundation.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.WinUI.1.8.250906003\build\native\Microsoft.WindowsAppSDK.WinUI.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.WinUI.1.8.250906003\build\native\Microsoft.WindowsAppSDK.WinUI.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.WinUI.1.8.250906003\build\native\Microsoft.WindowsAppSDK.WinUI.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.WinUI.1.8.250906003\build\native\Microsoft.WindowsAppSDK.WinUI.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.Runtime.1.8.250907003\build\native\Microsoft.WindowsAppSDK.Runtime.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.Runtime.1.8.250907003\build\native\Microsoft.WindowsAppSDK.Runtime.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.Runtime.1.8.250907003\build\native\Microsoft.WindowsAppSDK.Runtime.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.Runtime.1.8.250907003\build\native\Microsoft.WindowsAppSDK.Runtime.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.DWrite.1.8.25090401\build\Microsoft.WindowsAppSDK.DWrite.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.DWrite.1.8.25090401\build\Microsoft.WindowsAppSDK.DWrite.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.DWrite.1.8.25090401\build\Microsoft.WindowsAppSDK.DWrite.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.DWrite.1.8.25090401\build\Microsoft.WindowsAppSDK.DWrite.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.InteractiveExperiences.1.8.250906004\build\native\Microsoft.WindowsAppSDK.InteractiveExperiences.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.InteractiveExperiences.1.8.250906004\build\native\Microsoft.WindowsAppSDK.InteractiveExperiences.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.InteractiveExperiences.1.8.250906004\build\native\Microsoft.WindowsAppSDK.InteractiveExperiences.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.InteractiveExperiences.1.8.250906004\build\native\Microsoft.WindowsAppSDK.InteractiveExperiences.targets'))" />
</Target>
</Project>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Microsoft.Web.WebView2" version="1.0.2903.40" targetFramework="native" />
<package id="Microsoft.Windows.CppWinRT" version="2.0.240111.5" targetFramework="native" />
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.231216.1" targetFramework="native" />
<package id="Microsoft.Windows.SDK.BuildTools" version="10.0.26100.4188" targetFramework="native" />
<package id="Microsoft.WindowsAppSDK" version="1.8.250907003" targetFramework="native" />
<package id="Microsoft.WindowsAppSDK.Base" version="1.8.250831001" targetFramework="native" />
<package id="Microsoft.WindowsAppSDK.Foundation" version="1.8.250906002" targetFramework="native" />
<package id="Microsoft.WindowsAppSDK.WinUI" version="1.8.250906003" targetFramework="native" />
<package id="Microsoft.WindowsAppSDK.Runtime" version="1.8.250907003" targetFramework="native" />
<package id="Microsoft.WindowsAppSDK.DWrite" version="1.8.25090401" targetFramework="native" />
<package id="Microsoft.WindowsAppSDK.InteractiveExperiences" version="1.8.250906004" targetFramework="native" />
<package id="Microsoft.WindowsAppSDK.Widgets" version="1.8.250904007" targetFramework="native" />
<package id="Microsoft.WindowsAppSDK.AI" version="1.8.37" targetFramework="native" />
<package id="Microsoft.Windows.SDK.BuildTools.MSIX" version="1.7.20250829.1" targetFramework="native" />
</packages>

Some files were not shown because too many files have changed in this diff Show More