diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 1a85de1e06..0dc822e741 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -82,6 +82,7 @@ body: - Workspaces - Welcome / PowerToys Tour window - ZoomIt + - Keystroke Overlay validations: required: true diff --git a/.github/ISSUE_TEMPLATE/translation_issue.yml b/.github/ISSUE_TEMPLATE/translation_issue.yml index 63b998822f..b66c77651d 100644 --- a/.github/ISSUE_TEMPLATE/translation_issue.yml +++ b/.github/ISSUE_TEMPLATE/translation_issue.yml @@ -55,6 +55,7 @@ body: - Workspaces - Welcome / PowerToys Tour window - ZoomIt + - Keystroke Overlay validations: required: true - type: input diff --git a/.github/actions/spell-check/allow/code.txt b/.github/actions/spell-check/allow/code.txt index c655bb1b55..700a70bf41 100644 --- a/.github/actions/spell-check/allow/code.txt +++ b/.github/actions/spell-check/allow/code.txt @@ -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 diff --git a/.github/actions/spell-check/excludes.txt b/.github/actions/spell-check/excludes.txt index 551c248923..c466456ca9 100644 --- a/.github/actions/spell-check/excludes.txt +++ b/.github/actions/spell-check/excludes.txt @@ -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$ diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index 4a3305217e..5bde8978c4 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -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 diff --git a/.pipelines/ESRPSigning_core.json b/.pipelines/ESRPSigning_core.json index e3ebffc20c..47ba4f342b 100644 --- a/.pipelines/ESRPSigning_core.json +++ b/.pipelines/ESRPSigning_core.json @@ -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", diff --git a/.pipelines/versionAndSignCheck.ps1 b/.pipelines/versionAndSignCheck.ps1 index f90e59afd6..cf1f515e78 100644 --- a/.pipelines/versionAndSignCheck.ps1 +++ b/.pipelines/versionAndSignCheck.ps1 @@ -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", diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000..555383dfb3 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "files.associations": { + "mutex": "cpp", + "vector": "cpp" + } +} \ No newline at end of file diff --git a/Cpp.Build.props b/Cpp.Build.props index 7b988f0d6f..f146a4d770 100644 --- a/Cpp.Build.props +++ b/Cpp.Build.props @@ -42,11 +42,6 @@ - - true - TurnOffAllWarnings - true - true Use pch.h @@ -116,11 +111,13 @@ - + true true - + false true false diff --git a/DATA_AND_PRIVACY.md b/DATA_AND_PRIVACY.md index 8aba94f12f..6f7d452cc3 100644 --- a/DATA_AND_PRIVACY.md +++ b/DATA_AND_PRIVACY.md @@ -1109,6 +1109,9 @@ _If you want to find diagnostic data events in the source code, these two links +### Keystroke Overlay + + - - - - - + + + @@ -116,7 +112,6 @@ - diff --git a/PowerToys.slnx b/PowerToys.slnx index 1884b2d58b..9e268c18fe 100644 --- a/PowerToys.slnx +++ b/PowerToys.slnx @@ -469,6 +469,14 @@ + + + + + + + + diff --git a/doc/devdocs/modules/keystrokeoverlay.md b/doc/devdocs/modules/keystrokeoverlay.md new file mode 100644 index 0000000000..b8fdc41b69 --- /dev/null +++ b/doc/devdocs/modules/keystrokeoverlay.md @@ -0,0 +1,104 @@ +# Keystroke Overlay - Will live in doc/devdocs/modules + + +[Public overview - Microsoft Learn](#) + +## Quick Links + +[All Issues](#)
+[Bugs](#)
+[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 diff --git a/doc/dsc/Settings.md b/doc/dsc/Settings.md index 24fab1e2dd..1ec95b4af7 100644 --- a/doc/dsc/Settings.md +++ b/doc/dsc/Settings.md @@ -32,6 +32,7 @@ RegistryPreview ShortcutGuide Workspaces ZoomIt +KeystrokeOverlay ``` ### 📄 Get diff --git a/installer/PowerToysSetup/Common.wxi b/installer/PowerToysSetup/Common.wxi new file mode 100644 index 0000000000..4b2e843f57 --- /dev/null +++ b/installer/PowerToysSetup/Common.wxi @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/installer/PowerToysSetup/KeystrokeOverlay.wxs b/installer/PowerToysSetup/KeystrokeOverlay.wxs new file mode 100644 index 0000000000..4d71cea263 --- /dev/null +++ b/installer/PowerToysSetup/KeystrokeOverlay.wxs @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/installer/PowerToysSetup/PowerToysInstaller.wixproj b/installer/PowerToysSetup/PowerToysInstaller.wixproj new file mode 100644 index 0000000000..0ca88ac89a --- /dev/null +++ b/installer/PowerToysSetup/PowerToysInstaller.wixproj @@ -0,0 +1,205 @@ + + + + + + + Version=$(Version);MonacoSRCHarvestPath=$(ProjectDir)..\..\x64\$(Configuration)\Assets\Monaco\monacoSRC;CmdPalVersion=$(CmdPalVersion) + + 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)" + + + + Version=$(Version);MonacoSRCHarvestPath=$(ProjectDir)..\..\ARM64\$(Configuration)\Assets\Monaco\monacoSRC;CmdPalVersion=$(CmdPalVersion); + 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)" + + + + Always + + 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 + + + + PowerToysInstaller + + + $(DefineConstants);PerUser=true + + + $(DefineConstants);PerUser=false + + + $(DefineConstants);CIBuild=true + + + $(DefineConstants);CIBuild=false + + + + Release + $(Platform) + 3.10 + 022a9d30-7c4f-416d-a9df-5ff2661cc0ad + 2.0 + PowerToysSetup-$(Version)-$(Platform) + PowerToysUserSetup-$(Version)-$(Platform) + Package + True + + + + + ICE91 + 1026;1076 + + + $(Platform)\$(Configuration)\MachineSetup + $(Platform)\$(Configuration)\UserSetup + obj\$(Platform)\$(Configuration)\MachineSetup + obj\$(Platform)\$(Configuration)\UserSetup + ICE40 + + + + + -v -sh -sw1108 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + $(WixExtDir)\WixFirewallExtension.dll + WixFirewallExtension + + + $(WixExtDir)\WixUtilExtension.dll + WixUtilExtension + + + $(WixExtDir)\WixUIExtension.dll + WixUIExtension + + + $(WixExtDir)\WixNetFxExtension.dll + WixNetFxExtension + + + + + + + + PowerToysSetupCustomActions + {32f3882b-f2d6-4586-b5ed-11e39e522bd3} + True + True + Binaries;Content;Satellites + INSTALLFOLDER + + + + + + + + + + + + + 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}. + + + + + + + + + + + <_ValidProjectsForRestore Include="$(MSBuildProjectFullPath)" /> + + + + diff --git a/installer/PowerToysSetup/generateAllFileComponents.ps1 b/installer/PowerToysSetup/generateAllFileComponents.ps1 new file mode 100644 index 0000000000..52a665db16 --- /dev/null +++ b/installer/PowerToysSetup/generateAllFileComponents.ps1 @@ -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)=)", "") { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'fileList', + Justification = 'variable is used in another scope')] + + $fileList = $matches[2] -split ';' + return + } + } + + $componentId = "$($fileListName)_Component" + + $componentDefs = "`r`n" + $componentDefs += + @" + + + + `r`n +"@ + + foreach ($file in $fileList) { + $fileTmp = $file -replace "-", "_" + $componentDefs += + @" + `r`n +"@ + } + + $componentDefs += + @" + `r`n +"@ + + $wxsFile = $wxsFile -replace "\s+()", $componentDefs + + $componentRef = + @" + +"@ + + $wxsFile = $wxsFile -replace "\s+()", "$componentRef`r`n " + + 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 diff --git a/installer/PowerToysSetupVNext/PowerToysInstallerVNext.wixproj b/installer/PowerToysSetupVNext/PowerToysInstallerVNext.wixproj index 18d6232140..0b32f3263a 100644 --- a/installer/PowerToysSetupVNext/PowerToysInstallerVNext.wixproj +++ b/installer/PowerToysSetupVNext/PowerToysInstallerVNext.wixproj @@ -136,6 +136,7 @@ call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuil + diff --git a/installer/PowerToysSetupVNext/Product.wxs b/installer/PowerToysSetupVNext/Product.wxs index 3e812beb2e..49b3f729ed 100644 --- a/installer/PowerToysSetupVNext/Product.wxs +++ b/installer/PowerToysSetupVNext/Product.wxs @@ -62,6 +62,7 @@ + diff --git a/installer/PowerToysSetupVNext/generateAllFileComponents.ps1 b/installer/PowerToysSetupVNext/generateAllFileComponents.ps1 index 6724d95170..9b5d2bd65e 100644 --- a/installer/PowerToysSetupVNext/generateAllFileComponents.ps1 +++ b/installer/PowerToysSetupVNext/generateAllFileComponents.ps1 @@ -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 diff --git a/src/common/GPOWrapper/GPOWrapper.cpp b/src/common/GPOWrapper/GPOWrapper.cpp index 2b256cd926..87c94cf676 100644 --- a/src/common/GPOWrapper/GPOWrapper.cpp +++ b/src/common/GPOWrapper/GPOWrapper.cpp @@ -288,4 +288,8 @@ namespace winrt::PowerToys::GPOWrapper::implementation { return static_cast(powertoys_gpo::getConfiguredRunAtStartupValue()); } + GpoRuleConfigured GPOWrapper::GetConfiguredKeystrokeOverlayEnabledValue() + { + return static_cast(powertoys_gpo::getConfiguredKeystrokeOverlayEnabledValue()); + } } diff --git a/src/common/GPOWrapper/GPOWrapper.h b/src/common/GPOWrapper/GPOWrapper.h index e57cccccd9..7c5377e401 100644 --- a/src/common/GPOWrapper/GPOWrapper.h +++ b/src/common/GPOWrapper/GPOWrapper.h @@ -77,6 +77,7 @@ namespace winrt::PowerToys::GPOWrapper::implementation static GpoRuleConfigured GetAllowDataDiagnosticsValue(); static GpoRuleConfigured GetConfiguredRunAtStartupValue(); static GpoRuleConfigured GetConfiguredNewPlusReplaceVariablesValue(); + static GpoRuleConfigured GetConfiguredKeystrokeOverlayEnabledValue(); }; } diff --git a/src/common/GPOWrapper/GPOWrapper.idl b/src/common/GPOWrapper/GPOWrapper.idl index 06d035aa35..8657e33758 100644 --- a/src/common/GPOWrapper/GPOWrapper.idl +++ b/src/common/GPOWrapper/GPOWrapper.idl @@ -81,6 +81,7 @@ namespace PowerToys static GpoRuleConfigured GetAllowDataDiagnosticsValue(); static GpoRuleConfigured GetConfiguredRunAtStartupValue(); static GpoRuleConfigured GetConfiguredNewPlusReplaceVariablesValue(); + static GpoRuleConfigured GetConfiguredKeystrokeOverlayEnabledValue(); } } } diff --git a/src/common/GPOWrapperProjection/GPOWrapper.cs b/src/common/GPOWrapperProjection/GPOWrapper.cs index 6cb91a69ac..d1d2d8bf8e 100644 --- a/src/common/GPOWrapperProjection/GPOWrapper.cs +++ b/src/common/GPOWrapperProjection/GPOWrapper.cs @@ -66,5 +66,10 @@ namespace PowerToys.GPOWrapperProjection { return (GpoRuleConfigured)PowerToys.GPOWrapper.GPOWrapper.GetConfiguredWorkspacesEnabledValue(); } + + public static GpoRuleConfigured GetConfiguredKeystrokeOverlayEnabledValue() + { + return (GpoRuleConfigured)PowerToys.GPOWrapper.GPOWrapper.GetConfiguredKeystrokeOverlayEnabledValue(); + } } } diff --git a/src/common/ManagedCommon/ModuleType.cs b/src/common/ManagedCommon/ModuleType.cs index d7ae386191..c4496f57c1 100644 --- a/src/common/ManagedCommon/ModuleType.cs +++ b/src/common/ManagedCommon/ModuleType.cs @@ -20,6 +20,7 @@ namespace ManagedCommon Hosts, ImageResizer, KeyboardManager, + KeystrokeOverlay, LightSwitch, MouseHighlighter, MouseJump, diff --git a/src/common/UITestAutomation/ScreenRecording.cs b/src/common/UITestAutomation/ScreenRecording.cs new file mode 100644 index 0000000000..57e844936d --- /dev/null +++ b/src/common/UITestAutomation/ScreenRecording.cs @@ -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 +{ + /// + /// Provides methods for recording the screen during UI tests. + /// Requires FFmpeg to be installed and available in PATH. + /// + internal class ScreenRecording : IDisposable + { + private readonly string outputDirectory; + private readonly string framesDirectory; + private readonly string outputFilePath; + private readonly List 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 + + /// + /// Initializes a new instance of the class. + /// + /// Directory where the recording will be saved. + 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(); + 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"); + } + } + + /// + /// Gets a value indicating whether screen recording is available (FFmpeg found). + /// + public bool IsAvailable => ffmpegPath != null; + + /// + /// Starts recording the screen. + /// + /// A task representing the asynchronous operation. + 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(); + } + } + + /// + /// Stops recording and encodes video. + /// + /// A task representing the asynchronous operation. + 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(); + } + } + + /// + /// Records frames from the screen. + /// + 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}"); + } + } + + /// + /// Captures a single frame. + /// + 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(); + 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++; + } + } + + /// + /// Encodes captured frames to video using ffmpeg. + /// + 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}"); + } + } + + /// + /// Finds ffmpeg executable. + /// + private static string? FindFfmpeg() + { + // Check if ffmpeg is in PATH + var pathDirs = Environment.GetEnvironmentVariable("PATH")?.Split(Path.PathSeparator) ?? Array.Empty(); + + 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; + } + + /// + /// Gets the path to the recorded video file. + /// + public string OutputFilePath => outputFilePath; + + /// + /// Gets the directory containing recordings. + /// + public string OutputDirectory => outputDirectory; + + /// + /// Cleans up resources. + /// + 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}"); + } + } + + /// + /// Disposes resources. + /// + public void Dispose() + { + if (isRecording) + { + StopRecordingAsync().GetAwaiter().GetResult(); + } + + Cleanup(); + recordingLock.Dispose(); + GC.SuppressFinalize(this); + } + } +} diff --git a/src/common/UITestAutomation/SessionHelper.cs b/src/common/UITestAutomation/SessionHelper.cs index 0ca3eb3ddd..fef220a647 100644 --- a/src/common/UITestAutomation/SessionHelper.cs +++ b/src/common/UITestAutomation/SessionHelper.cs @@ -130,9 +130,13 @@ namespace Microsoft.PowerToys.UITest /// /// The path to the application executable. /// Optional command line arguments to pass to the application. - 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; } /// @@ -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}"); } } /// /// Restarts now exe and takes control of it. /// - public void RestartScopeExe() + public void RestartScopeExe(string? enableModules = null) { ExitScopeExe(); - StartExe(locationPath + sessionPath, this.commandLineArgs); + StartExe(locationPath + sessionPath, commandLineArgs, enableModules); } public WindowsDriver 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}"); + } + } + } } } diff --git a/src/common/UITestAutomation/SettingsConfigHelper.cs b/src/common/UITestAutomation/SettingsConfigHelper.cs index 0a01891dc4..81e5e3c180 100644 --- a/src/common/UITestAutomation/SettingsConfigHelper.cs +++ b/src/common/UITestAutomation/SettingsConfigHelper.cs @@ -26,14 +26,13 @@ namespace Microsoft.PowerToys.UITest /// /// Configures global PowerToys settings to enable only specified modules and disable all others. /// - /// Array of module names to enable (e.g., "Peek", "FancyZones"). All other modules will be disabled. - /// Thrown when modulesToEnable is null. + /// 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. /// Thrown when settings file operations fail. [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(); try { diff --git a/src/common/UITestAutomation/UITestBase.cs b/src/common/UITestAutomation/UITestBase.cs index 1c72be05f4..877f384104 100644 --- a/src/common/UITestAutomation/UITestBase.cs +++ b/src/common/UITestAutomation/UITestBase.cs @@ -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() }; 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 } } + /// + /// Adds screen recordings to test results directory when test fails. + /// + 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."); + } + } + } + + /// + /// Cleans up recording directory when test passes. + /// + 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}"); + } + } + } + /// /// 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 /// /// Restart scope exe. /// - 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; } /// diff --git a/src/common/utils/gpo.h b/src/common/utils/gpo.h index ab71d09d0b..d7b515c5a1 100644 --- a/src/common/utils/gpo.h +++ b/src/common/utils/gpo.h @@ -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); diff --git a/src/dsc/v3/PowerToys.DSC/DSCResources/SettingsResource.cs b/src/dsc/v3/PowerToys.DSC/DSCResources/SettingsResource.cs index 5f69b20227..2e6e2a5cc5 100644 --- a/src/dsc/v3/PowerToys.DSC/DSCResources/SettingsResource.cs +++ b/src/dsc/v3/PowerToys.DSC/DSCResources/SettingsResource.cs @@ -50,6 +50,7 @@ public sealed class SettingsResource : BaseResource { nameof(ModuleType.Hosts), CreateModuleFunctionData }, { nameof(ModuleType.ImageResizer), CreateModuleFunctionData }, { nameof(ModuleType.KeyboardManager), CreateModuleFunctionData }, + { nameof(ModuleType.KeystrokeOverlay), CreateModuleFunctionData }, { nameof(ModuleType.MouseHighlighter), CreateModuleFunctionData }, { nameof(ModuleType.MouseJump), CreateModuleFunctionData }, { nameof(ModuleType.MousePointerCrosshairs), CreateModuleFunctionData }, diff --git a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Pages/MainPage.xaml.cs b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Pages/MainPage.xaml.cs index 80376e5f72..5bef6389f0 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Pages/MainPage.xaml.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Pages/MainPage.xaml.cs @@ -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); } } } diff --git a/src/modules/KeystrokeOverlay/.vscode/settings.json b/src/modules/KeystrokeOverlay/.vscode/settings.json new file mode 100644 index 0000000000..fb160f7b48 --- /dev/null +++ b/src/modules/KeystrokeOverlay/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "files.associations": { + "*.embeddedhtml": "html", + "xstring": "cpp" + } +} \ No newline at end of file diff --git a/src/modules/KeystrokeOverlay/KeystrokeOverlayKeyboardService/.gitignore b/src/modules/KeystrokeOverlay/KeystrokeOverlayKeyboardService/.gitignore new file mode 100644 index 0000000000..a54591907f --- /dev/null +++ b/src/modules/KeystrokeOverlay/KeystrokeOverlayKeyboardService/.gitignore @@ -0,0 +1 @@ +# Ignore all Windows executable files diff --git a/src/modules/KeystrokeOverlay/KeystrokeOverlayKeyboardService/Batcher.cpp b/src/modules/KeystrokeOverlay/KeystrokeOverlayKeyboardService/Batcher.cpp new file mode 100644 index 0000000000..1147b20352 --- /dev/null +++ b/src/modules/KeystrokeOverlay/KeystrokeOverlayKeyboardService/Batcher.cpp @@ -0,0 +1,86 @@ +// Batcher.cpp +// Worker thread that batches and sends keystroke data through IPC. +#include "pch.h" +#include "Batcher.h" +#include + +// 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 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(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 +#include + +class Batcher +{ +public: + Batcher(SpscRing &q) : _q(q) {} + void Start(); + void Stop(); + +private: + SpscRing &_q; + PipeServer _pipe; + std::atomic _run{false}; + std::thread _t; +}; diff --git a/src/modules/KeystrokeOverlay/KeystrokeOverlayKeyboardService/EventQueue.h b/src/modules/KeystrokeOverlay/KeystrokeOverlayKeyboardService/EventQueue.h new file mode 100644 index 0000000000..8ea781536c --- /dev/null +++ b/src/modules/KeystrokeOverlay/KeystrokeOverlayKeyboardService/EventQueue.h @@ -0,0 +1,36 @@ +// EventQueue.h +// Custom SPSC queue to store KeyStrokeEvent objects between producer and consumer threads. +#pragma once +#include // atomic type for head and tail +#include + +template +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 _buf{}; + std::atomic _head{0}, _tail{0}; +}; diff --git a/src/modules/KeystrokeOverlay/KeystrokeOverlayKeyboardService/KeyboardListener.cpp b/src/modules/KeystrokeOverlay/KeystrokeOverlayKeyboardService/KeyboardListener.cpp new file mode 100644 index 0000000000..7fe584c00d --- /dev/null +++ b/src/modules/KeystrokeOverlay/KeystrokeOverlayKeyboardService/KeyboardListener.cpp @@ -0,0 +1,135 @@ +#include "pch.h" +#include +#include +#include +#include +#include +#include + +#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 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((c.QuadPart * 1'000'000) / freq.QuadPart); +} + +// Modifier snapshot (Ctrl, Alt, Shift, Win) +static inline std::array snapshot_mods() { + auto down = [](int vk){ return (GetKeyState(vk) & 0x8000) != 0; }; + return std::array{ + 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(buf[0]); +} + +// Push helpers +static inline void emit_down(UINT vk, UINT sc) { + KeystrokeEvent e{}; + e.type = KeystrokeEvent::Type::Down; + e.vk = static_cast(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(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(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; +} \ No newline at end of file diff --git a/src/modules/KeystrokeOverlay/KeystrokeOverlayKeyboardService/KeystrokeEvent.h b/src/modules/KeystrokeOverlay/KeystrokeOverlayKeyboardService/KeystrokeEvent.h new file mode 100644 index 0000000000..4bbc7040a0 --- /dev/null +++ b/src/modules/KeystrokeOverlay/KeystrokeOverlayKeyboardService/KeystrokeEvent.h @@ -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 +#include +#include + +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 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. \ No newline at end of file diff --git a/src/modules/KeystrokeOverlay/KeystrokeOverlayKeyboardService/KeystrokeOverlayKeyboardService.vcxproj b/src/modules/KeystrokeOverlay/KeystrokeOverlayKeyboardService/KeystrokeOverlayKeyboardService.vcxproj new file mode 100644 index 0000000000..3e399a0a23 --- /dev/null +++ b/src/modules/KeystrokeOverlay/KeystrokeOverlayKeyboardService/KeystrokeOverlayKeyboardService.vcxproj @@ -0,0 +1,102 @@ + + + + + + + + + 15.0 + {A1B2C3D4-E5F6-4321-8765-123456789ABC} + Win32Proj + KeystrokeOverlay + KeystrokeOverlayKeyboardService + + + + + + + + Application + Unicode + + + + + + + + + + + + + + $(SolutionDir)x64\$(Configuration)\WinUI3Apps\ + $(ProjectDir)Intermediate\$(Platform)\$(Configuration)\ + PowerToys.KeystrokeOverlayKeystrokeServer + .exe + + + + + + Level3 + MaxSpeed + + WIN32;_WINDOWS;%(PreprocessorDefinitions) + + + $(ProjectDir); + $(ProjectDir)..\..\..\common\inc; + $(ProjectDir)..\..\..\common\Telemetry; + $(ProjectDir)..\..\..; + %(AdditionalIncludeDirectories) + + + + + Console + true + + user32.lib; + gdi32.lib; + kernel32.lib; + advapi32.lib; + %(AdditionalDependencies) + + + + + + + + + + + + + + + + + + + + Create + + + + + + + + + + + + diff --git a/src/modules/KeystrokeOverlay/KeystrokeOverlayKeyboardService/Pipeserver.cpp b/src/modules/KeystrokeOverlay/KeystrokeOverlayKeyboardService/Pipeserver.cpp new file mode 100644 index 0000000000..4b3cd4edca --- /dev/null +++ b/src/modules/KeystrokeOverlay/KeystrokeOverlayKeyboardService/Pipeserver.cpp @@ -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(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; + } +} diff --git a/src/modules/KeystrokeOverlay/KeystrokeOverlayKeyboardService/Pipeserver.h b/src/modules/KeystrokeOverlay/KeystrokeOverlayKeyboardService/Pipeserver.h new file mode 100644 index 0000000000..d63ee75e03 --- /dev/null +++ b/src/modules/KeystrokeOverlay/KeystrokeOverlayKeyboardService/Pipeserver.h @@ -0,0 +1,21 @@ +// Pipeserver.h +// Administrates named pipes server for IPC communications. +#pragma once +#include +#include +#include + +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(); +}; diff --git a/src/modules/KeystrokeOverlay/KeystrokeOverlayKeyboardService/pch.cpp b/src/modules/KeystrokeOverlay/KeystrokeOverlayKeyboardService/pch.cpp new file mode 100644 index 0000000000..1d9f38c57d --- /dev/null +++ b/src/modules/KeystrokeOverlay/KeystrokeOverlayKeyboardService/pch.cpp @@ -0,0 +1 @@ +#include "pch.h" diff --git a/src/modules/KeystrokeOverlay/KeystrokeOverlayKeyboardService/pch.h b/src/modules/KeystrokeOverlay/KeystrokeOverlayKeyboardService/pch.h new file mode 100644 index 0000000000..dac8eb2042 --- /dev/null +++ b/src/modules/KeystrokeOverlay/KeystrokeOverlayKeyboardService/pch.h @@ -0,0 +1,13 @@ +#define WIN32_LEAN_AND_MEAN +#include + +// C++ Standard Library Header Files used in your project +#include +#include +#include +#include +#include +#include +#include +#include +#include \ No newline at end of file diff --git a/src/modules/KeystrokeOverlay/KeystrokeOverlayModuleInterface/KeystrokeOverlay.rc b/src/modules/KeystrokeOverlay/KeystrokeOverlayModuleInterface/KeystrokeOverlay.rc new file mode 100644 index 0000000000..af288a92d3 --- /dev/null +++ b/src/modules/KeystrokeOverlay/KeystrokeOverlayModuleInterface/KeystrokeOverlay.rc @@ -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 \ No newline at end of file diff --git a/src/modules/KeystrokeOverlay/KeystrokeOverlayModuleInterface/KeystrokeOverlay.vcxproj b/src/modules/KeystrokeOverlay/KeystrokeOverlayModuleInterface/KeystrokeOverlay.vcxproj new file mode 100644 index 0000000000..47561cdc94 --- /dev/null +++ b/src/modules/KeystrokeOverlay/KeystrokeOverlayModuleInterface/KeystrokeOverlay.vcxproj @@ -0,0 +1,89 @@ + + + + + 15.0 + {D66DAAF0-84FC-477D-A980-43370F4499C2} + Win32Proj + KeystrokeOverlay + KeystrokeOverlayModuleInterface + 10.0.26100.0 + + + + DynamicLibrary + + + v143 + + + + + + + + + + + + ..\..\..\..\$(Platform)\$(Configuration)\WinUI3Apps + + + PowerToys.KeystrokeOverlayModuleInterface + + + + KEYSTROKEOVERLAY_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + + ..\KeystrokeOverlayKeyboardService;..\..\..\common\inc;..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories) + + + $(OutDir)$(TargetName)$(TargetExt) + + + + + + + + + + + Create + + + + + + + + + + {d9b8fc84-322a-4f9f-bbb9-20915c47ddfd} + + + {6955446d-23f7-4023-9bb3-8657f904af99} + + + + {A1B2C3D4-E5F6-4321-8765-123456789ABC} + + + + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + \ No newline at end of file diff --git a/src/modules/KeystrokeOverlay/KeystrokeOverlayModuleInterface/dllmain.cpp b/src/modules/KeystrokeOverlay/KeystrokeOverlayModuleInterface/dllmain.cpp new file mode 100644 index 0000000000..f267d1b314 --- /dev/null +++ b/src/modules/KeystrokeOverlay/KeystrokeOverlayModuleInterface/dllmain.cpp @@ -0,0 +1,438 @@ +#include "pch.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "trace.h" +#include + +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""; + +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(&__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(&__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(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(); +} \ No newline at end of file diff --git a/src/modules/KeystrokeOverlay/KeystrokeOverlayModuleInterface/pch.cpp b/src/modules/KeystrokeOverlay/KeystrokeOverlayModuleInterface/pch.cpp new file mode 100644 index 0000000000..a83d3bb2cc --- /dev/null +++ b/src/modules/KeystrokeOverlay/KeystrokeOverlayModuleInterface/pch.cpp @@ -0,0 +1,2 @@ +#include "pch.h" +#pragma comment(lib, "windowsapp") \ No newline at end of file diff --git a/src/modules/KeystrokeOverlay/KeystrokeOverlayModuleInterface/pch.h b/src/modules/KeystrokeOverlay/KeystrokeOverlayModuleInterface/pch.h new file mode 100644 index 0000000000..329705f63b --- /dev/null +++ b/src/modules/KeystrokeOverlay/KeystrokeOverlayModuleInterface/pch.h @@ -0,0 +1,6 @@ +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#include +#include \ No newline at end of file diff --git a/src/modules/KeystrokeOverlay/KeystrokeOverlayModuleInterface/resource.h b/src/modules/KeystrokeOverlay/KeystrokeOverlayModuleInterface/resource.h new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/modules/KeystrokeOverlay/KeystrokeOverlayModuleInterface/trace.cpp b/src/modules/KeystrokeOverlay/KeystrokeOverlayModuleInterface/trace.cpp new file mode 100644 index 0000000000..19c5c90ed8 --- /dev/null +++ b/src/modules/KeystrokeOverlay/KeystrokeOverlayModuleInterface/trace.cpp @@ -0,0 +1,31 @@ +#include "pch.h" +#include "trace.h" + +#include + +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")); +} \ No newline at end of file diff --git a/src/modules/KeystrokeOverlay/KeystrokeOverlayModuleInterface/trace.h b/src/modules/KeystrokeOverlay/KeystrokeOverlayModuleInterface/trace.h new file mode 100644 index 0000000000..5ea46529f2 --- /dev/null +++ b/src/modules/KeystrokeOverlay/KeystrokeOverlayModuleInterface/trace.h @@ -0,0 +1,14 @@ +#pragma once + +#include + +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; + +}; diff --git a/src/modules/KeystrokeOverlay/KeystrokeOverlayXAML/App.xaml b/src/modules/KeystrokeOverlay/KeystrokeOverlayXAML/App.xaml new file mode 100644 index 0000000000..4d6780ed65 --- /dev/null +++ b/src/modules/KeystrokeOverlay/KeystrokeOverlayXAML/App.xaml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + diff --git a/src/modules/KeystrokeOverlay/KeystrokeOverlayXAML/App.xaml.cs b/src/modules/KeystrokeOverlay/KeystrokeOverlayXAML/App.xaml.cs new file mode 100644 index 0000000000..ef977af82c --- /dev/null +++ b/src/modules/KeystrokeOverlay/KeystrokeOverlayXAML/App.xaml.cs @@ -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; + } + } + } +} diff --git a/src/modules/KeystrokeOverlay/KeystrokeOverlayXAML/Assets/KeystrokeOverlay/Icon.ico b/src/modules/KeystrokeOverlay/KeystrokeOverlayXAML/Assets/KeystrokeOverlay/Icon.ico new file mode 100644 index 0000000000..271058de5d Binary files /dev/null and b/src/modules/KeystrokeOverlay/KeystrokeOverlayXAML/Assets/KeystrokeOverlay/Icon.ico differ diff --git a/src/modules/KeystrokeOverlay/KeystrokeOverlayXAML/Controls/KeyVisual/KeyCharPresenter.xaml b/src/modules/KeystrokeOverlay/KeystrokeOverlayXAML/Controls/KeyVisual/KeyCharPresenter.xaml new file mode 100644 index 0000000000..f315f00548 --- /dev/null +++ b/src/modules/KeystrokeOverlay/KeystrokeOverlayXAML/Controls/KeyVisual/KeyCharPresenter.xaml @@ -0,0 +1,106 @@ + + + + + + + + + diff --git a/src/modules/KeystrokeOverlay/KeystrokeOverlayXAML/Controls/KeyVisual/KeyCharPresenter.xaml.cs b/src/modules/KeystrokeOverlay/KeystrokeOverlayXAML/Controls/KeyVisual/KeyCharPresenter.xaml.cs new file mode 100644 index 0000000000..43ba496712 --- /dev/null +++ b/src/modules/KeystrokeOverlay/KeystrokeOverlayXAML/Controls/KeyVisual/KeyCharPresenter.xaml.cs @@ -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))); +} diff --git a/src/modules/KeystrokeOverlay/KeystrokeOverlayXAML/Controls/KeyVisual/KeyVisual.xaml b/src/modules/KeystrokeOverlay/KeystrokeOverlayXAML/Controls/KeyVisual/KeyVisual.xaml new file mode 100644 index 0000000000..a32a7abb41 --- /dev/null +++ b/src/modules/KeystrokeOverlay/KeystrokeOverlayXAML/Controls/KeyVisual/KeyVisual.xaml @@ -0,0 +1,213 @@ + + + + + + + + +