mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-14 18:57:55 +01:00
KeystrokeOverlay: New PowerToys module for on screen key strokes (#44250)
<!-- Enter a brief description/summary of your PR here. What does it fix/what does it change/how was it tested (even manually, if necessary)? --> ## Summary of the Pull Request This pull request introduces the new KeystrokeOverlay module to PowerToys, including its documentation, build configuration, installer setup, and spell-check dictionary updates. KeystrokeOverlay can be used to show keys pressed on the screen like Visual Studio Code's Screencast Mode. It provides customization options for text size and background, offers four types of display modes, and three shortcuts for toggling on/off the display, cycling through monitors, and through different display modes (see below for more details). <!-- Please review the items on the PR checklist before submitting--> ## PR Checklist - [x] Closes: #981 <!-- - [ ] Closes: #yyy (add separate lines for additional resolved issues) --> - [x] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [ ] **Tests:** Added/updated and all pass - [ ] **Localization:** All end-user-facing strings can be localized - [x] **Dev docs:** Added/updated - [ ] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx <!-- Provide a more detailed description of the PR, other things fixed, or any additional comments/features here --> ## Detailed Description of the Pull Request / Additional comments **KeystrokeOverlay Module Integration:** * Added KeystrokeOverlay as a new module in the solution, with corresponding projects for the keyboard service, UI, and module interface (`PowerToys.slnx`). * Added documentation for the Keystroke Overlay module, including architecture, usage, and debugging instructions (`doc/devdocs/modules/keystrokeoverlay.md`). * Updated installer configuration to include Keystroke Overlay, adding new WiX include and setup files (`installer/PowerToysSetup/Common.wxi`, `installer/PowerToysSetup/KeystrokeOverlay.wxs`). [[1]](diffhunk://#diff-ecd2ee19d18433ed47b8f13b44cfff7b00c8009c17bc71139cec0d8571f8f607R1-R59) [[2]](diffhunk://#diff-af48b984b168acbe5aeb021e97a9e826ab9b52c20c08b36f40dd3e5ad00342b1R1-R9) * Registered Keystroke Overlay in the settings documentation and issue templates (`doc/dsc/Settings.md`, `.github/ISSUE_TEMPLATE/bug_report.yml`, `.github/ISSUE_TEMPLATE/translation_issue.yml`). [[1]](diffhunk://#diff-bde46f469b76ba994ea938853f169553846221b329a21e9b59d37c5a0b7d63aeR35) [[2]](diffhunk://#diff-637f7b97bba458badb691a1557c3d4648686292e948dbe3e8360564378b653efR85) [[3]](diffhunk://#diff-135b470e9875068a1085599402d6f89bea163068568c426b22f493f35fbfbea6R58) * Added KeystrokeOverlay binaries to the signing pipeline (`.pipelines/ESRPSigning_core.json`). **Customization options** | Category | Setting | Description | Default / Example | | :--- | :--- | :--- | :--- | | **General** | **Enable Keystroke Overlay** | Master toggle to turn the module on or off. | `On` | | **Shortcuts** | **Activation Shortcut** | Hotkey to toggle the overlay visibility. | `Win` + `Shift` + `K` | | | **Switch Monitor Shortcut** | Hotkey to move the overlay to another display. | `Win` + `Shift` + `/` | | | **Switch Display Mode** | Hotkey to cycle through different visualization modes. | `Win` + `Shift` + `D` | | **Behavior** | **Draggable Overlay** | Allows you to manually move the overlay position with your mouse. | `On` | | | **Display Mode** | Controls what history of keys is shown (e.g., history vs. single). | `Last Five Keystrokes` | | | **Overlay Timeout** | How long (in ms) the keys stay visible before fading. | `2000` ms | | **Appearance** | **Text Size** | Adjusts the font size of the keys inside the overlay. | `37` | | | **Text Color** | Sets the font color and transparency level. | `White` (Example) | | | **Background Color** | Sets the background box color and transparency level. | `Purple` (Example) | <!-- Describe how you validated the behavior. Add automated tests wherever possible, but list manual validation steps taken as well --> ## Validation Steps Performed The new module was tested on multiple different Windows Devices and all configurations work as expected. --------- Signed-off-by: Shawn Yuan (from Dev Box) <shuaiyuan@microsoft.com> Co-authored-by: Sátvik Karanam <89281036+skara9@users.noreply.github.com> Co-authored-by: Jiří Polášek <me@jiripolasek.com> Co-authored-by: Software2 <software-2@users.noreply.github.com> Co-authored-by: Niels Laute <niels.laute@live.nl> Co-authored-by: Mike Griese <migrie@microsoft.com> Co-authored-by: Jaylyn Barbee <51131738+Jaylyn-Barbee@users.noreply.github.com> Co-authored-by: Gordon Lam (SH) <yeelam@microsoft.com> Co-authored-by: Kai Tao <69313318+vanzue@users.noreply.github.com> Co-authored-by: Mark Russinovich <markruss@microsoft.com> Co-authored-by: Mark Russinovich <markruss@ntdev.microsoft.com> Co-authored-by: Clint Rutkas <clint@rutkas.com> Co-authored-by: Alex Mihaiuc <69110671+foxmsft@users.noreply.github.com> Co-authored-by: Michael Jolley <mike@baldbeardedbuilder.com> Co-authored-by: Gordon Lam <73506701+yeelam-gordon@users.noreply.github.com> Co-authored-by: leileizhang <leilzh@microsoft.com> Co-authored-by: Dave Rayment <dave.rayment@gmail.com> Co-authored-by: Shawn Yuan <128874481+shuaiyuanxx@users.noreply.github.com> Co-authored-by: Kai Tao <kaitao@microsoft.com> Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: jiripolasek <4773077+jiripolasek@users.noreply.github.com> Co-authored-by: Dustin L. Howett <duhowett@microsoft.com> Co-authored-by: Sam Rueby <samrueby@gmail.com> Co-authored-by: Lee Won Jun <dldnjs1013@nate.com> Co-authored-by: Mason Bergstrom <13530957+MasonBergstrom@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Mike Hall <mikehall@microsoft.com> Co-authored-by: moooyo <42196638+moooyo@users.noreply.github.com> Co-authored-by: Yu Leng <yuleng@microsoft.com> Co-authored-by: dsm20 <74568547+dsm20@users.noreply.github.com> Co-authored-by: zackpaton <91792781+zackpaton@users.noreply.github.com> Co-authored-by: Gleb Khmyznikov <gleb.khmyznikov@gmail.com> Co-authored-by: Guilherme <57814418+DevLGuilherme@users.noreply.github.com>
This commit is contained in:
1
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
1
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -82,6 +82,7 @@ body:
|
||||
- Workspaces
|
||||
- Welcome / PowerToys Tour window
|
||||
- ZoomIt
|
||||
- Keystroke Overlay
|
||||
validations:
|
||||
required: true
|
||||
|
||||
|
||||
1
.github/ISSUE_TEMPLATE/translation_issue.yml
vendored
1
.github/ISSUE_TEMPLATE/translation_issue.yml
vendored
@@ -55,6 +55,7 @@ body:
|
||||
- Workspaces
|
||||
- Welcome / PowerToys Tour window
|
||||
- ZoomIt
|
||||
- Keystroke Overlay
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
|
||||
23
.github/actions/spell-check/allow/code.txt
vendored
23
.github/actions/spell-check/allow/code.txt
vendored
@@ -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
|
||||
|
||||
1
.github/actions/spell-check/excludes.txt
vendored
1
.github/actions/spell-check/excludes.txt
vendored
@@ -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$
|
||||
|
||||
8
.github/actions/spell-check/expect.txt
vendored
8
.github/actions/spell-check/expect.txt
vendored
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
|
||||
@@ -27,7 +27,8 @@ $versionExceptions = @(
|
||||
"WyHash.dll",
|
||||
"Microsoft.Recognizers.Text.DataTypes.TimexExpression.dll",
|
||||
"ObjectModelCsProjection.dll",
|
||||
"RendererCsProjection.dll") -join '|';
|
||||
"RendererCsProjection.dll",
|
||||
"Microsoft.ML.OnnxRuntime.dll") -join '|';
|
||||
$nullVersionExceptions = @(
|
||||
"SkiaSharp.Views.WinUI.Native.dll",
|
||||
"libSkiaSharp.dll",
|
||||
|
||||
6
.vscode/settings.json
vendored
Normal file
6
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"files.associations": {
|
||||
"mutex": "cpp",
|
||||
"vector": "cpp"
|
||||
}
|
||||
}
|
||||
@@ -42,11 +42,6 @@
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup>
|
||||
<ClCompile>
|
||||
<!-- Make angle-bracket includes external and turn off code analysis for them -->
|
||||
<TreatAngleIncludeAsExternal>true</TreatAngleIncludeAsExternal>
|
||||
<ExternalWarningLevel>TurnOffAllWarnings</ExternalWarningLevel>
|
||||
<DisableAnalyzeExternal>true</DisableAnalyzeExternal>
|
||||
|
||||
<MultiProcessorCompilation>true</MultiProcessorCompilation>
|
||||
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Use</PrecompiledHeader>
|
||||
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
|
||||
@@ -116,11 +111,13 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Debug/Release props -->
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Debug'" Label="Configuration">
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Debug'"
|
||||
Label="Configuration">
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<LinkIncremental>true</LinkIncremental>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Release'" Label="Configuration">
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Release'"
|
||||
Label="Configuration">
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<LinkIncremental>false</LinkIncremental>
|
||||
|
||||
@@ -1109,6 +1109,9 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
### Keystroke Overlay
|
||||
<!-- TODO: Fill out table -->
|
||||
|
||||
<!-- back up of table
|
||||
|
||||
<table style="width:100%">
|
||||
|
||||
@@ -7,8 +7,6 @@
|
||||
<PackageVersion Include="AdaptiveCards.ObjectModel.WinUI3" Version="2.0.0-beta" />
|
||||
<PackageVersion Include="AdaptiveCards.Rendering.WinUI3" Version="2.1.0-beta" />
|
||||
<PackageVersion Include="AdaptiveCards.Templating" Version="2.0.5" />
|
||||
<PackageVersion Include="boost" Version="1.87.0" TargetFramework="native" />
|
||||
<PackageVersion Include="boost_regex-vc143" Version="1.87.0" TargetFramework="native" />
|
||||
<PackageVersion Include="CommunityToolkit.Labs.WinUI.Controls.OpacityMaskView" Version="0.1.251101-build.2372" />
|
||||
<PackageVersion Include="Microsoft.Bot.AdaptiveExpressions.Core" Version="4.23.0" />
|
||||
<PackageVersion Include="Appium.WebDriver" Version="4.4.5" />
|
||||
@@ -72,12 +70,10 @@
|
||||
This is present due to a bug in CsWinRT where WPF projects cause the analyzer to fail.
|
||||
-->
|
||||
<PackageVersion Include="Microsoft.Windows.CsWinRT" Version="2.2.0" />
|
||||
<PackageVersion Include="Microsoft.Windows.ImplementationLibrary" Version="1.0.231216.1"/>
|
||||
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.6901" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.8.251106002" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK.Foundation" Version="1.8.251104000" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK.AI" Version="1.8.39" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK.Runtime" Version="1.8.251106002" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.8.250907003" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK.AI" Version="1.8.37" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK.Runtime" Version="1.8.250907003" />
|
||||
<PackageVersion Include="Microsoft.Xaml.Behaviors.WinUI.Managed" Version="2.0.9" />
|
||||
<PackageVersion Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.39" />
|
||||
<PackageVersion Include="ModernWpfUI" Version="0.9.4" />
|
||||
@@ -116,7 +112,6 @@
|
||||
<PackageVersion Include="System.IO.Abstractions.TestingHelpers" Version="22.0.13" />
|
||||
<PackageVersion Include="System.Management" Version="9.0.10" />
|
||||
<PackageVersion Include="System.Net.Http" Version="4.3.4" />
|
||||
<PackageVersion Include="System.Numerics.Tensors" Version="9.0.11" />
|
||||
<PackageVersion Include="System.Private.Uri" Version="4.3.2" />
|
||||
<PackageVersion Include="System.Reactive" Version="6.0.1" />
|
||||
<PackageVersion Include="System.Runtime.Caching" Version="9.0.10" />
|
||||
|
||||
@@ -469,6 +469,14 @@
|
||||
<Project Path="src/modules/keyboardmanager/KeyboardManagerEditorTest/KeyboardManagerEditorTest.vcxproj" Id="62173d9a-6724-4c00-a1c8-fb646480a9ec" />
|
||||
<Project Path="src/modules/keyboardmanager/KeyboardManagerEngineTest/KeyboardManagerEngineTest.vcxproj" Id="7f4b3a60-bc27-45a7-8000-68b0b6ea7466" />
|
||||
</Folder>
|
||||
<Folder Name="/modules/KeystrokeOverlay/">
|
||||
<Project Path="src/modules/KeystrokeOverlay/KeystrokeOverlayKeyboardService/KeystrokeOverlayKeyboardService.vcxproj" Id="a1b2c3d4-e5f6-4321-8765-123456789abc" />
|
||||
<Project Path="src/modules/KeystrokeOverlay/KeystrokeOverlayModuleInterface/KeystrokeOverlay.vcxproj" Id="d66daaf0-84fc-477d-a980-43370f4499c2" />
|
||||
<Project Path="src/modules/KeystrokeOverlay/KeystrokeOverlayXAML/KeystrokeOverlayUI.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
</Folder>
|
||||
<Folder Name="/modules/launcher/">
|
||||
<Project Path="src/modules/launcher/Microsoft.Launcher/Microsoft.Launcher.vcxproj" Id="e364f67b-bb12-4e91-b639-355866ebcd8b">
|
||||
<BuildDependency Project="src/modules/launcher/PowerLauncher/PowerLauncher.csproj" />
|
||||
|
||||
104
doc/devdocs/modules/keystrokeoverlay.md
Normal file
104
doc/devdocs/modules/keystrokeoverlay.md
Normal file
@@ -0,0 +1,104 @@
|
||||
# Keystroke Overlay - Will live in doc/devdocs/modules
|
||||
|
||||
|
||||
[Public overview - Microsoft Learn](#)
|
||||
|
||||
## Quick Links
|
||||
|
||||
[All Issues](#)<br>
|
||||
[Bugs](#)<br>
|
||||
[Pull Requests](#)
|
||||
|
||||
## Overview
|
||||
|
||||
Keystroke Overlay is a PowerToys module that displays keyboard input live. There are three different display modes. Single-key display shows the last key pressed, e.g. "o." Last five keys display shows the last five keys pressed, e.g. "h e l l o." Shortcut display supports shortcuts, e.g. "ctrl + v." This module is meant to be used as a helpful tool for educators, presenters, and/or visually impaired users.
|
||||
|
||||
## Architecture
|
||||
|
||||
The Keystroke Overlay module consists of three main components:
|
||||
|
||||
```
|
||||
keystrokeoverlay/
|
||||
├── KeystrokeOverlayKeyboardService/ # Keyboard Hook
|
||||
├── KeystrokeOverlayXAML/ # The overlaying display UI
|
||||
└── KeystrokeOverlayModuleInterface/ # DLL Interface
|
||||
```
|
||||
|
||||
### Keyboard Service (KeystrokeOverlayKeyboardService)
|
||||
|
||||
The Keyboard Service component is responsible for:
|
||||
- Compiles KeystrokeEvent.h, EventQueue.h, Batcher.cpp, and KeyboardListener.cpp into PowerToys.KeystrokeOverlayKeystrokeServer.exe
|
||||
- Implements keyboard hooks to detect key presses
|
||||
- Manages the trigger mechanism for displaying the keystroke overlay
|
||||
- Handles keyboard input processing
|
||||
|
||||
### UI Layer (KeystrokeOverlayXAML)
|
||||
|
||||
The UI component is responsible for:
|
||||
- Displaying the overlay of pressed keys
|
||||
- Managing the visual positioning of the overlay
|
||||
|
||||
### Module Interface (KeystrokeOverlayModuleInterface)
|
||||
|
||||
The Module Interface, implemented in `KeystrokeOverlayModuleInterface/dllmain.cpp`, is responsible for:
|
||||
- Handling communication between PowerToys Runner and the KeystrokeOverlay process
|
||||
- Managing module lifecycle (enable/disable/settings)
|
||||
- Launching and terminating the PowerToys.KeystrokeOverlay.exe process (as well as PowerToys.KeystrokeOverlayKeystrokeServer.exe as a child process)
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Activation Mechanism
|
||||
|
||||
The Keystroke Overlay is activated when:
|
||||
1. A user presses or holds any key
|
||||
2. After a brief delay (around 300ms per setting), the overlay appears
|
||||
3. Upon releasing the key(s), the overlay hovers for around 300ms or a value specified in settings, then disappears
|
||||
|
||||
### Character Sets
|
||||
The module supports multiple language-specific characters. Since the module uses keyboard codes to detect key presses and trigger the display, various keyboards and languages are supported by Keystroke Overlay, as long as they are supported by Windows.
|
||||
|
||||
### Known Behaviors
|
||||
- If a key is pressed and held, this is processed as several presses of the same key in rapid succession
|
||||
- Stream mode detects capital letters as shortcuts because the shift key is pressed and separates them from words; e.g. “Hello” in stream mode appears as “H ello”
|
||||
|
||||
### Future Considerations
|
||||
|
||||
- Add support for different appearances, other than left-to-right. Right-to-left or center-outwards would be beneficial, especially for users who communicate in languages that read right-to-left
|
||||
- Add support for default positioning, e.g. top-left, bottom-center, middle-right, …
|
||||
|
||||
|
||||
## Debugging
|
||||
|
||||
To debug the Keystroke Overlay module via **runner** approach, follow these steps:
|
||||
|
||||
0. Get familiar with the overall [Debugging Process](../development/debugging.md) for PowerToys.
|
||||
1. **Build** the entire PowerToys solution in Visual Studio
|
||||
2. Navigate to the **KeystrokeOverlay** folder in Solution Explorer
|
||||
3. Open the file you want to debug and set **breakpoints** at the relevant locations
|
||||
4. Find the **runner** project in the root of the solution
|
||||
5. Right-click on the **runner** project and select "*Set as Startup Project*"
|
||||
6. Start debugging by pressing `F5` or clicking the "*Start*" button
|
||||
7. When the PowerToys Runner launches, **enable** the Keystroke Overlay module in the UI
|
||||
8. Use the Visual Studio Debug menu or press `Ctrl+Alt+P` to open "*Reattach to Process*"
|
||||
9. Find and select "**PowerToys.KeystrokeOverlay.exe**" in the process list
|
||||
10. Trigger the action in Keystroke Overlay that should hit your breakpoint
|
||||
11. Verify that the debugger breaks at your breakpoint and you can inspect variables and step through code
|
||||
|
||||
This process allows you to debug the Keystroke Overlay module while it's running as part of the full PowerToys application.
|
||||
|
||||
### Alternative Debugging Approach
|
||||
|
||||
To directly debug the Keystroke Overlay UI component:
|
||||
|
||||
0. Get familiar with the overall [Debugging Process](../development/debugging.md) for PowerToys.
|
||||
1. **Build** the entire PowerToys solution in Visual Studio
|
||||
2. Navigate to the **KeystrokeOverlay** folder in Solution Explorer
|
||||
3. Open the file you want to debug and set **breakpoints** at the relevant locations
|
||||
4. Right-click on the **KeystrokeOverlayUI** project and select "*Set as Startup Project*"
|
||||
5. Start debugging by pressing `F5` or clicking the "*Start*" button
|
||||
6. Verify that the debugger breaks at your breakpoint and you can inspect variables and step through code
|
||||
|
||||
|
||||
|
||||
|
||||
## Any known issues with debugging here
|
||||
@@ -32,6 +32,7 @@ RegistryPreview
|
||||
ShortcutGuide
|
||||
Workspaces
|
||||
ZoomIt
|
||||
KeystrokeOverlay
|
||||
```
|
||||
|
||||
### 📄 Get
|
||||
|
||||
59
installer/PowerToysSetup/Common.wxi
Normal file
59
installer/PowerToysSetup/Common.wxi
Normal file
@@ -0,0 +1,59 @@
|
||||
<Include>
|
||||
<!-- Names of folders and projects -->
|
||||
<?define FancyZonesProjectName="FancyZones"?>
|
||||
<?define ImageResizerProjectName="ImageResizer"?>
|
||||
<?define KeyboardManagerProjectName="KeyboardManager"?>
|
||||
<?define PowerAccentProjectName="PowerAccent"?>
|
||||
<?define PowerRenameProjectName="PowerRename"?>
|
||||
<?define FileLocksmithProjectName="FileLocksmith"?>
|
||||
<?define ColorPickerProjectName="ColorPicker"?>
|
||||
<?define PowerOCRProjectName="PowerOCR"?>
|
||||
<?define AwakeProjectName="Awake"?>
|
||||
<?define MouseUtilsProjectName="MouseUtils"?>
|
||||
<?define AlwaysOnTopProjectName="AlwaysOnTop"?>
|
||||
<?define MeasureToolProjectName="MeasureTool"?>
|
||||
<?define HostsProjectName="Hosts"?>
|
||||
<?define MouseWithoutBordersProjectName="MouseWithoutBorders"?>
|
||||
<?define AdvancedPasteProjectName="AdvancedPaste"?>
|
||||
<?define RegistryPreviewProjectName="RegistryPreview"?>
|
||||
<?define PeekProjectName="Peek"?>
|
||||
<?define WorkspacesProjectName="Workspaces"?>
|
||||
<?define KeystrokeOverlayProjectName="KeystrokeOverlay"?>
|
||||
|
||||
<?define RepoDir="$(var.ProjectDir)..\..\" ?>
|
||||
<?if $(var.Platform) = x64?>
|
||||
<?define PowerToysPlatform="x64"?>
|
||||
<?define PlatformProgramFiles="[ProgramFiles64Folder]"?>
|
||||
<?define PlatformLK="x64" ?>
|
||||
<?define BinDir="$(var.RepoDir)x64\$(var.Configuration)\" ?>
|
||||
<?else?>
|
||||
<!-- stable WIX 3 doesn't support ARM64, so we build installers as x86 -->
|
||||
<?define PowerToysPlatform="ARM64"?>
|
||||
<!--TODO: define to ARM64 Program files once it's available-->
|
||||
<?define PlatformProgramFiles="[ProgramFiles6432Folder]"?>
|
||||
<?define PlatformLK="arm64" ?>
|
||||
<?define BinDir="$(var.RepoDir)ARM64\$(var.Configuration)\" ?>
|
||||
<?endif?>
|
||||
|
||||
<?if $(var.PerUser) = "true"?>
|
||||
<?define PerMachineYesNo="no"?>
|
||||
<?define MSIPath="UserSetup"?>
|
||||
<?define MSIName="PowerToysUserSetup-$(var.Version)-$(var.PowerToysPlatform).msi"?>
|
||||
<?define DefaultInstallDir="LocalAppDataFolder" ?>
|
||||
<?define RegistryScope="HKCU" ?>
|
||||
<?define InstallScope="perUser" ?>
|
||||
<?define InstallPrivileges="limited" ?>
|
||||
<?define UpgradeCodeGUID="D8B559DB-4C98-487A-A33F-50A8EEE42726" ?>
|
||||
<?else?>
|
||||
<?define PerMachineYesNo="yes"?>
|
||||
<?define MSIPath="MachineSetup"?>
|
||||
<?define MSIName="PowerToysSetup-$(var.Version)-$(var.PowerToysPlatform).msi"?>
|
||||
<?define DefaultInstallDir="ProgramFiles64Folder" ?>
|
||||
<?define RegistryScope="HKLM" ?>
|
||||
<?define InstallScope="perMachine" ?>
|
||||
<?define InstallPrivileges="elevated" ?>
|
||||
<?define UpgradeCodeGUID="42B84BF7-5FBF-473B-9C8B-049DC16F7708" ?>
|
||||
<?endif?>
|
||||
|
||||
<?define BinX32Dir="$(var.RepoDir)x86\$(var.Configuration)\" ?>
|
||||
</Include>
|
||||
9
installer/PowerToysSetup/KeystrokeOverlay.wxs
Normal file
9
installer/PowerToysSetup/KeystrokeOverlay.wxs
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"
|
||||
xmlns:util="http://schemas.microsoft.com/wix/UtilExtension" >
|
||||
|
||||
<?include $(sys.CURRENTDIR)\Common.wxi?>
|
||||
|
||||
<!-- TODO: keystroke overlay -->
|
||||
|
||||
</Wix>
|
||||
205
installer/PowerToysSetup/PowerToysInstaller.wixproj
Normal file
205
installer/PowerToysSetup/PowerToysInstaller.wixproj
Normal file
@@ -0,0 +1,205 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="4.0" DefaultTargets="Build" InitialTargets="EnsureNuGetPackageBuildImports"
|
||||
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="..\..\src\Version.props" Condition="Exists('..\..\src\Version.props')" />
|
||||
<Import Project="..\..\src\CmdPalVersion.props" Condition="Exists('..\..\src\CmdPalVersion.props')" />
|
||||
<Import Project="..\wix.props" Condition="Exists('..\wix.props')" />
|
||||
<PropertyGroup Condition="'$(Platform)' == 'x64'">
|
||||
<DefineConstants>Version=$(Version);MonacoSRCHarvestPath=$(ProjectDir)..\..\x64\$(Configuration)\Assets\Monaco\monacoSRC;CmdPalVersion=$(CmdPalVersion)</DefineConstants>
|
||||
<!-- THIS IS AN INNER LOOP OPTIMIZATION
|
||||
The build pipeline builds the Settings and Launcher projects for Publication
|
||||
using a specific profile. If you're doing local installer builds, this will
|
||||
simulate the build pipeline doing that for you. -->
|
||||
<PreBuildEvent>IF NOT DEFINED IsPipeline (
|
||||
call "$([MSBuild]::GetVsInstallRoot())\Common7\Tools\VsDevCmd.bat" -arch=amd64 -host_arch=amd64 -winsdk=10.0.19041.0 -vcvars_ver=$(VCToolsVersion)
|
||||
SET PTRoot=$(SolutionDir)\..
|
||||
call "..\..\..\publish.cmd" x64
|
||||
)
|
||||
call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuildThisFileDirectory)\generateMonacoWxs.ps1 -monacoWxsFile "$(MSBuildThisFileDirectory)\MonacoSRC.wxs"
|
||||
call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuildThisFileDirectory)\generateDscResourcesWxs.ps1 -dscWxsFile "$(MSBuildThisFileDirectory)\DscResources.wxs" -Platform "$(Platform)" -Configuration "$(Configuration)"
|
||||
</PreBuildEvent>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Platform)' != 'x64'">
|
||||
<DefineConstants>Version=$(Version);MonacoSRCHarvestPath=$(ProjectDir)..\..\ARM64\$(Configuration)\Assets\Monaco\monacoSRC;CmdPalVersion=$(CmdPalVersion);</DefineConstants>
|
||||
<PreBuildEvent>IF NOT DEFINED IsPipeline (
|
||||
call "$([MSBuild]::GetVsInstallRoot())\Common7\Tools\VsDevCmd.bat" -arch=arm64 -host_arch=amd64 -winsdk=10.0.19041.0 -vcvars_ver=$(VCToolsVersion)
|
||||
SET PTRoot=$(SolutionDir)\..
|
||||
call "..\..\..\publish.cmd" arm64
|
||||
)
|
||||
call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuildThisFileDirectory)\generateMonacoWxs.ps1 -monacoWxsFile "$(MSBuildThisFileDirectory)\MonacoSRC.wxs"
|
||||
call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuildThisFileDirectory)\generateDscResourcesWxs.ps1 -dscWxsFile "$(MSBuildThisFileDirectory)\DscResources.wxs" -Platform "$(Platform)" -Configuration "$(Configuration)"
|
||||
</PreBuildEvent>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<RunPostBuildEvent>Always</RunPostBuildEvent>
|
||||
<PostBuildEvent>
|
||||
call move /Y ..\..\..\AdvancedPaste.wxs.bk ..\..\..\AdvancedPaste.wxs
|
||||
call move /Y ..\..\..\Awake.wxs.bk ..\..\..\Awake.wxs
|
||||
call move /Y ..\..\..\BaseApplications.wxs.bk ..\..\..\BaseApplications.wxs
|
||||
call move /Y ..\..\..\CmdPal.wxs.bk ..\..\..\CmdPal.wxs
|
||||
call move /Y ..\..\..\ColorPicker.wxs.bk ..\..\..\ColorPicker.wxs
|
||||
call move /Y ..\..\..\Core.wxs.bk ..\..\..\Core.wxs
|
||||
call move /Y ..\..\..\EnvironmentVariables.wxs.bk ..\..\..\EnvironmentVariables.wxs
|
||||
call move /Y ..\..\..\FileExplorerPreview.wxs.bk ..\..\..\FileExplorerPreview.wxs
|
||||
call move /Y ..\..\..\FileLocksmith.wxs.bk ..\..\..\FileLocksmith.wxs
|
||||
call move /Y ..\..\..\Hosts.wxs.bk ..\..\..\Hosts.wxs
|
||||
call move /Y ..\..\..\ImageResizer.wxs.bk ..\..\..\ImageResizer.wxs
|
||||
call move /Y ..\..\..\KeyboardManager.wxs.bk ..\..\..\KeyboardManager.wxs
|
||||
call move /Y ..\..\..\MouseWithoutBorders.wxs.bk ..\..\..\MouseWithoutBorders.wxs
|
||||
call move /Y ..\..\..\NewPlus.wxs.bk ..\..\..\NewPlus.wxs
|
||||
call move /Y ..\..\..\Peek.wxs.bk ..\..\..\Peek.wxs
|
||||
call move /Y ..\..\..\PowerRename.wxs.bk ..\..\..\PowerRename.wxs
|
||||
call move /Y ..\..\..\Product.wxs.bk ..\..\..\Product.wxs
|
||||
call move /Y ..\..\..\RegistryPreview.wxs.bk ..\..\..\RegistryPreview.wxs
|
||||
call move /Y ..\..\..\Resources.wxs.bk ..\..\..\Resources.wxs
|
||||
call move /Y ..\..\..\Run.wxs.bk ..\..\..\Run.wxs
|
||||
call move /Y ..\..\..\Settings.wxs.bk ..\..\..\Settings.wxs
|
||||
call move /Y ..\..\..\ShortcutGuide.wxs.bk ..\..\..\ShortcutGuide.wxs
|
||||
call move /Y ..\..\..\KeystrokeOverlay.wxs.bk ..\..\..\KeystrokeOverlay.wxs
|
||||
call move /Y ..\..\..\Tools.wxs.bk ..\..\..\Tools.wxs
|
||||
call move /Y ..\..\..\WinAppSDK.wxs.bk ..\..\..\WinAppSDK.wxs
|
||||
call move /Y ..\..\..\WinUI3Applications.wxs.bk ..\..\..\WinUI3Applications.wxs
|
||||
call move /Y ..\..\..\Workspaces.wxs.bk ..\..\..\Workspaces.wxs
|
||||
</PostBuildEvent>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<Name>PowerToysInstaller</Name>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Label="UserMacros" Condition=" '$(PerUser)' == 'true' ">
|
||||
<DefineConstants>$(DefineConstants);PerUser=true</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Label="UserMacros" Condition=" '$(PerUser)' != 'true' ">
|
||||
<DefineConstants>$(DefineConstants);PerUser=false</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Label="UserMacros" Condition=" '$(CIBuild)' == 'true' ">
|
||||
<DefineConstants>$(DefineConstants);CIBuild=true</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Label="UserMacros" Condition=" '$(CIBuild)' != 'true' ">
|
||||
<DefineConstants>$(DefineConstants);CIBuild=false</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<!-- We do not support debug installer builds -->
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Release</Configuration>
|
||||
<Platform>$(Platform)</Platform>
|
||||
<ProductVersion>3.10</ProductVersion>
|
||||
<ProjectGuid>022a9d30-7c4f-416d-a9df-5ff2661cc0ad</ProjectGuid>
|
||||
<SchemaVersion>2.0</SchemaVersion>
|
||||
<OutputName Condition=" '$(PerUser)' != 'true' ">PowerToysSetup-$(Version)-$(Platform)</OutputName>
|
||||
<OutputName Condition=" '$(PerUser)' == 'true' ">PowerToysUserSetup-$(Version)-$(Platform)</OutputName>
|
||||
<OutputType>Package</OutputType>
|
||||
<SuppressAclReset>True</SuppressAclReset>
|
||||
<NuGetPackageImportStamp>
|
||||
</NuGetPackageImportStamp>
|
||||
<!-- 1076 and ICE91 - warning: using this configuration for perMachine install could cause problems. -->
|
||||
<!-- 1026 - warning: file ID is too long -->
|
||||
<SuppressIces>ICE91</SuppressIces>
|
||||
<SuppressSpecificWarnings>1026;1076</SuppressSpecificWarnings>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<OutputPath Condition=" '$(PerUser)' != 'true' ">$(Platform)\$(Configuration)\MachineSetup</OutputPath>
|
||||
<OutputPath Condition=" '$(PerUser)' == 'true' ">$(Platform)\$(Configuration)\UserSetup</OutputPath>
|
||||
<IntermediateOutputPath Condition=" '$(PerUser)' != 'true' ">obj\$(Platform)\$(Configuration)\MachineSetup</IntermediateOutputPath>
|
||||
<IntermediateOutputPath Condition=" '$(PerUser)' == 'true' ">obj\$(Platform)\$(Configuration)\UserSetup</IntermediateOutputPath>
|
||||
<SuppressIces>ICE40</SuppressIces>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<!-- suppress warning 1108 regarding -sh being deprecated -->
|
||||
<!-- -sh suppresses file information which was causing wix build to hang in CI -->
|
||||
<LinkerAdditionalOptions>-v -sh -sw1108</LinkerAdditionalOptions>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="CustomDialogs\PTInstallDirDlg.wxs" />
|
||||
<Compile Include="CustomDialogs\PTLicenseDlg.wxs" />
|
||||
<Compile Include="CustomDialogs\WixUI_PTInstallDir.wxs" />
|
||||
<Compile Include="NewPlus.wxs" />
|
||||
<Compile Include="Product.wxs" />
|
||||
<Compile Include="AdvancedPaste.wxs" />
|
||||
<Compile Include="Awake.wxs" />
|
||||
<Compile Include="BaseApplications.wxs" />
|
||||
<Compile Include="CmdPal.wxs" />
|
||||
<Compile Include="ColorPicker.wxs" />
|
||||
<Compile Include="EnvironmentVariables.wxs" />
|
||||
<Compile Include="FileExplorerPreview.wxs" />
|
||||
<Compile Include="FileLocksmith.wxs" />
|
||||
<Compile Include="Hosts.wxs" />
|
||||
<Compile Include="ImageResizer.wxs" />
|
||||
<Compile Include="KeyboardManager.wxs" />
|
||||
<Compile Include="DscResources.wxs" />
|
||||
<Compile Include="Peek.wxs" />
|
||||
<Compile Include="PowerRename.wxs" />
|
||||
<Compile Include="RegistryPreview.wxs" />
|
||||
<Compile Include="Run.wxs" />
|
||||
<Compile Include="Settings.wxs" />
|
||||
<Compile Include="ShortcutGuide.wxs" />
|
||||
<Compile Include="Tools.wxs" />
|
||||
<Compile Include="MouseWithoutBorders.wxs" />
|
||||
<Compile Include="WinUI3Applications.wxs" />
|
||||
<Compile Include="MonacoSRC.wxs" />
|
||||
<Compile Include="Core.wxs" />
|
||||
<Compile Include="Resources.wxs" />
|
||||
<Compile Include="WinAppSDK.wxs" />
|
||||
<Compile Include="Workspaces.wxs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<WixExtension Include="WixFirewallExtension">
|
||||
<HintPath>$(WixExtDir)\WixFirewallExtension.dll</HintPath>
|
||||
<Name>WixFirewallExtension</Name>
|
||||
</WixExtension>
|
||||
<WixExtension Include="WixUtilExtension">
|
||||
<HintPath>$(WixExtDir)\WixUtilExtension.dll</HintPath>
|
||||
<Name>WixUtilExtension</Name>
|
||||
</WixExtension>
|
||||
<WixExtension Include="WixUIExtension">
|
||||
<HintPath>$(WixExtDir)\WixUIExtension.dll</HintPath>
|
||||
<Name>WixUIExtension</Name>
|
||||
</WixExtension>
|
||||
<WixExtension Include="WixNetFxExtension">
|
||||
<HintPath>$(WixExtDir)\WixNetFxExtension.dll</HintPath>
|
||||
<Name>WixNetFxExtension</Name>
|
||||
</WixExtension>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="CustomDialogs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\PowerToysSetupCustomActions\PowerToysSetupCustomActions.vcxproj">
|
||||
<Name>PowerToysSetupCustomActions</Name>
|
||||
<Project>{32f3882b-f2d6-4586-b5ed-11e39e522bd3}</Project>
|
||||
<Private>True</Private>
|
||||
<DoNotHarvest>True</DoNotHarvest>
|
||||
<RefProjectOutputGroups>Binaries;Content;Satellites</RefProjectOutputGroups>
|
||||
<RefTargetDir>INSTALLFOLDER</RefTargetDir>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(WixTargetsPath)" Condition=" '$(WixTargetsPath)' != '' " />
|
||||
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\WiX\v3.x\Wix.targets" Condition=" '$(WixTargetsPath)' == '' AND Exists('$(MSBuildExtensionsPath32)\Microsoft\WiX\v3.x\Wix.targets') " />
|
||||
<Target Name="EnsureWixToolsetInstalled" Condition=" '$(WixTargetsImported)' != 'true' ">
|
||||
<Error Text="The WiX Toolset v3 build tools must be installed to build this project. To download the WiX Toolset, see http://wixtoolset.org/releases/" />
|
||||
</Target>
|
||||
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
|
||||
<PropertyGroup>
|
||||
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
|
||||
</PropertyGroup>
|
||||
<Error Condition="!Exists('..\wix.props')" Text="$([System.String]::Format('$(ErrorText)', '..\wix.props'))" />
|
||||
</Target>
|
||||
<!--
|
||||
To modify your build process, add your task inside one of the targets below and uncomment it.
|
||||
Other similar extension points exist, see Wix.targets.
|
||||
<Target Name="BeforeBuild">
|
||||
</Target>
|
||||
<Target Name="AfterBuild">
|
||||
</Target> -->
|
||||
<Target Name="BeforeBuild">
|
||||
<HeatDirectory Directory="..\..\src\Monaco\monacoSRC" PreprocessorVariable="var.MonacoSRCHarvestPath" OutputFile="MonacoSRC.wxs" ComponentGroupName="MonacoSRCHeatGenerated" DirectoryRefId="MonacoPreviewHandlerMonacoSRCFolder" AutogenerateGuids="false" GenerateGuidsNow="true" ToolPath="$(WixToolPath)" RunAsSeparateProcess="true" SuppressFragments="false" SuppressRegistry="false" SuppressRootDirectory="true" />
|
||||
</Target>
|
||||
<!-- Prevents NU1503 -->
|
||||
<Target Name="_IsProjectRestoreSupported" Returns="@(_ValidProjectsForRestore)">
|
||||
<ItemGroup>
|
||||
<_ValidProjectsForRestore Include="$(MSBuildProjectFullPath)" />
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
<Target Name="Restore" />
|
||||
</Project>
|
||||
327
installer/PowerToysSetup/generateAllFileComponents.ps1
Normal file
327
installer/PowerToysSetup/generateAllFileComponents.ps1
Normal file
@@ -0,0 +1,327 @@
|
||||
[CmdletBinding()]
|
||||
Param(
|
||||
[Parameter(Mandatory = $True, Position = 1)]
|
||||
[string]$platform,
|
||||
[Parameter(Mandatory = $False, Position = 2)]
|
||||
[string]$installscopeperuser = "false"
|
||||
)
|
||||
|
||||
Function Generate-FileList() {
|
||||
[CmdletBinding()]
|
||||
Param(
|
||||
# Can be multiple files separated by ; as long as they're on the same directory
|
||||
[Parameter(Mandatory = $True, Position = 1)]
|
||||
[AllowEmptyString()]
|
||||
[string]$fileDepsJson,
|
||||
[Parameter(Mandatory = $True, Position = 2)]
|
||||
[string]$fileListName,
|
||||
[Parameter(Mandatory = $True, Position = 3)]
|
||||
[string]$wxsFilePath,
|
||||
# If there is no deps.json file, just pass path to files
|
||||
[Parameter(Mandatory = $False, Position = 4)]
|
||||
[string]$depsPath,
|
||||
# launcher plugins are being loaded into launcher process,
|
||||
# so there are some additional dependencies to skip
|
||||
[Parameter(Mandatory = $False, Position = 5)]
|
||||
[bool]$isLauncherPlugin
|
||||
)
|
||||
|
||||
$fileWxs = Get-Content $wxsFilePath;
|
||||
|
||||
$fileExclusionList = @("*.pdb", "*.lastcodeanalysissucceeded", "createdump.exe", "powertoys.exe")
|
||||
|
||||
$fileInclusionList = @("*.dll", "*.exe", "*.json", "*.msix", "*.png", "*.gif", "*.ico", "*.cur", "*.svg", "index.html", "reg.js", "gitignore.js", "srt.js", "monacoSpecialLanguages.js", "customTokenThemeRules.js", "*.pri")
|
||||
|
||||
$dllsToIgnore = @("System.CodeDom.dll", "WindowsBase.dll")
|
||||
|
||||
if ($fileDepsJson -eq [string]::Empty) {
|
||||
$fileDepsRoot = $depsPath
|
||||
} else {
|
||||
$multipleDepsJson = $fileDepsJson.Split(";")
|
||||
|
||||
foreach ( $singleDepsJson in $multipleDepsJson )
|
||||
{
|
||||
|
||||
$fileDepsRoot = (Get-ChildItem $singleDepsJson).Directory.FullName
|
||||
$depsJson = Get-Content $singleDepsJson | ConvertFrom-Json
|
||||
|
||||
$runtimeList = ([array]$depsJson.targets.PSObject.Properties)[-1].Value.PSObject.Properties | Where-Object {
|
||||
$_.Name -match "runtimepack.*Runtime"
|
||||
};
|
||||
|
||||
$runtimeList | ForEach-Object {
|
||||
$_.Value.PSObject.Properties.Value | ForEach-Object {
|
||||
$fileExclusionList += $_.PSObject.Properties.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$fileExclusionList = $fileExclusionList | Where-Object {$_ -notin $dllsToIgnore}
|
||||
|
||||
if ($isLauncherPlugin -eq $True) {
|
||||
$fileInclusionList += @("*.deps.json")
|
||||
$fileExclusionList += @("Ijwhost.dll", "PowerToys.Common.UI.dll", "PowerToys.GPOWrapper.dll", "PowerToys.GPOWrapperProjection.dll", "PowerToys.PowerLauncher.Telemetry.dll", "PowerToys.ManagedCommon.dll", "PowerToys.Settings.UI.Lib.dll", "Wox.Infrastructure.dll", "Wox.Plugin.dll")
|
||||
}
|
||||
|
||||
$fileList = Get-ChildItem $fileDepsRoot -Include $fileInclusionList -Exclude $fileExclusionList -File -Name
|
||||
|
||||
$fileWxs = $fileWxs -replace "(<\?define $($fileListName)=)", "<?define $fileListName=$($fileList -join ';')"
|
||||
|
||||
Set-Content -Path $wxsFilePath -Value $fileWxs
|
||||
}
|
||||
|
||||
Function Generate-FileComponents() {
|
||||
[CmdletBinding()]
|
||||
Param(
|
||||
[Parameter(Mandatory = $True, Position = 1)]
|
||||
[string]$fileListName,
|
||||
[Parameter(Mandatory = $True, Position = 2)]
|
||||
[string]$wxsFilePath,
|
||||
[Parameter(Mandatory = $True, Position = 3)]
|
||||
[string]$regroot
|
||||
)
|
||||
|
||||
$wxsFile = Get-Content $wxsFilePath;
|
||||
|
||||
$wxsFile | ForEach-Object {
|
||||
if ($_ -match "(<?define $fileListName=)(.*)\?>") {
|
||||
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'fileList',
|
||||
Justification = 'variable is used in another scope')]
|
||||
|
||||
$fileList = $matches[2] -split ';'
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
$componentId = "$($fileListName)_Component"
|
||||
|
||||
$componentDefs = "`r`n"
|
||||
$componentDefs +=
|
||||
@"
|
||||
<Component Id="$($componentId)" Win64="yes" Guid="$((New-Guid).ToString().ToUpper())">
|
||||
<RegistryKey Root="$($regroot)" Key="Software\Classes\powertoys\components">
|
||||
<RegistryValue Type="string" Name="$($componentId)" Value="" KeyPath="yes"/>
|
||||
</RegistryKey>`r`n
|
||||
"@
|
||||
|
||||
foreach ($file in $fileList) {
|
||||
$fileTmp = $file -replace "-", "_"
|
||||
$componentDefs +=
|
||||
@"
|
||||
<File Id="$($fileListName)_File_$($fileTmp)" Source="`$(var.$($fileListName)Path)\$($file)" />`r`n
|
||||
"@
|
||||
}
|
||||
|
||||
$componentDefs +=
|
||||
@"
|
||||
</Component>`r`n
|
||||
"@
|
||||
|
||||
$wxsFile = $wxsFile -replace "\s+(<!--$($fileListName)_Component_Def-->)", $componentDefs
|
||||
|
||||
$componentRef =
|
||||
@"
|
||||
<ComponentRef Id="$($componentId)" />
|
||||
"@
|
||||
|
||||
$wxsFile = $wxsFile -replace "\s+(</ComponentGroup>)", "$componentRef`r`n </ComponentGroup>"
|
||||
|
||||
Set-Content -Path $wxsFilePath -Value $wxsFile
|
||||
}
|
||||
|
||||
if ($platform -ceq "arm64") {
|
||||
$platform = "ARM64"
|
||||
}
|
||||
|
||||
if ($installscopeperuser -eq "true") {
|
||||
$registryroot = "HKCU"
|
||||
} else {
|
||||
$registryroot = "HKLM"
|
||||
}
|
||||
|
||||
#BaseApplications
|
||||
Generate-FileList -fileDepsJson "" -fileListName BaseApplicationsFiles -wxsFilePath $PSScriptRoot\BaseApplications.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release"
|
||||
Generate-FileComponents -fileListName "BaseApplicationsFiles" -wxsFilePath $PSScriptRoot\BaseApplications.wxs -regroot $registryroot
|
||||
|
||||
#WinUI3Applications
|
||||
Generate-FileList -fileDepsJson "" -fileListName WinUI3ApplicationsFiles -wxsFilePath $PSScriptRoot\WinUI3Applications.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps"
|
||||
Generate-FileComponents -fileListName "WinUI3ApplicationsFiles" -wxsFilePath $PSScriptRoot\WinUI3Applications.wxs -regroot $registryroot
|
||||
|
||||
#AdvancedPaste
|
||||
Generate-FileList -fileDepsJson "" -fileListName AdvancedPasteAssetsFiles -wxsFilePath $PSScriptRoot\AdvancedPaste.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\AdvancedPaste"
|
||||
Generate-FileComponents -fileListName "AdvancedPasteAssetsFiles" -wxsFilePath $PSScriptRoot\AdvancedPaste.wxs -regroot $registryroot
|
||||
|
||||
#AwakeFiles
|
||||
Generate-FileList -fileDepsJson "" -fileListName AwakeImagesFiles -wxsFilePath $PSScriptRoot\Awake.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\Assets\Awake"
|
||||
Generate-FileComponents -fileListName "AwakeImagesFiles" -wxsFilePath $PSScriptRoot\Awake.wxs -regroot $registryroot
|
||||
|
||||
#ColorPicker
|
||||
Generate-FileList -fileDepsJson "" -fileListName ColorPickerAssetsFiles -wxsFilePath $PSScriptRoot\ColorPicker.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\Assets\ColorPicker"
|
||||
Generate-FileComponents -fileListName "ColorPickerAssetsFiles" -wxsFilePath $PSScriptRoot\ColorPicker.wxs -regroot $registryroot
|
||||
|
||||
#Environment Variables
|
||||
Generate-FileList -fileDepsJson "" -fileListName EnvironmentVariablesAssetsFiles -wxsFilePath $PSScriptRoot\EnvironmentVariables.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\EnvironmentVariables"
|
||||
Generate-FileComponents -fileListName "EnvironmentVariablesAssetsFiles" -wxsFilePath $PSScriptRoot\EnvironmentVariables.wxs -regroot $registryroot
|
||||
|
||||
#FileExplorerAdd-ons
|
||||
Generate-FileList -fileDepsJson "" -fileListName MonacoPreviewHandlerMonacoAssetsFiles -wxsFilePath $PSScriptRoot\FileExplorerPreview.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\Assets\Monaco"
|
||||
Generate-FileList -fileDepsJson "" -fileListName MonacoPreviewHandlerCustomLanguagesFiles -wxsFilePath $PSScriptRoot\FileExplorerPreview.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\Assets\Monaco\customLanguages"
|
||||
Generate-FileComponents -fileListName "MonacoPreviewHandlerMonacoAssetsFiles" -wxsFilePath $PSScriptRoot\FileExplorerPreview.wxs -regroot $registryroot
|
||||
Generate-FileComponents -fileListName "MonacoPreviewHandlerCustomLanguagesFiles" -wxsFilePath $PSScriptRoot\FileExplorerPreview.wxs -regroot $registryroot
|
||||
|
||||
#FileLocksmith
|
||||
Generate-FileList -fileDepsJson "" -fileListName FileLocksmithAssetsFiles -wxsFilePath $PSScriptRoot\FileLocksmith.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\FileLocksmith"
|
||||
Generate-FileComponents -fileListName "FileLocksmithAssetsFiles" -wxsFilePath $PSScriptRoot\FileLocksmith.wxs -regroot $registryroot
|
||||
|
||||
#Hosts
|
||||
Generate-FileList -fileDepsJson "" -fileListName HostsAssetsFiles -wxsFilePath $PSScriptRoot\Hosts.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\Hosts"
|
||||
Generate-FileComponents -fileListName "HostsAssetsFiles" -wxsFilePath $PSScriptRoot\Hosts.wxs -regroot $registryroot
|
||||
|
||||
#ImageResizer
|
||||
Generate-FileList -fileDepsJson "" -fileListName ImageResizerAssetsFiles -wxsFilePath $PSScriptRoot\ImageResizer.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\ImageResizer"
|
||||
Generate-FileComponents -fileListName "ImageResizerAssetsFiles" -wxsFilePath $PSScriptRoot\ImageResizer.wxs -regroot $registryroot
|
||||
|
||||
#New+
|
||||
Generate-FileList -fileDepsJson "" -fileListName NewPlusAssetsFiles -wxsFilePath $PSScriptRoot\NewPlus.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\NewPlus"
|
||||
Generate-FileComponents -fileListName "NewPlusAssetsFiles" -wxsFilePath $PSScriptRoot\NewPlus.wxs -regroot $registryroot
|
||||
|
||||
#Peek
|
||||
Generate-FileList -fileDepsJson "" -fileListName PeekAssetsFiles -wxsFilePath $PSScriptRoot\Peek.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\Peek\"
|
||||
Generate-FileComponents -fileListName "PeekAssetsFiles" -wxsFilePath $PSScriptRoot\Peek.wxs -regroot $registryroot
|
||||
|
||||
#PowerRename
|
||||
Generate-FileList -fileDepsJson "" -fileListName PowerRenameAssetsFiles -wxsFilePath $PSScriptRoot\PowerRename.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\PowerRename\"
|
||||
Generate-FileComponents -fileListName "PowerRenameAssetsFiles" -wxsFilePath $PSScriptRoot\PowerRename.wxs -regroot $registryroot
|
||||
|
||||
#RegistryPreview
|
||||
Generate-FileList -fileDepsJson "" -fileListName RegistryPreviewAssetsFiles -wxsFilePath $PSScriptRoot\RegistryPreview.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\RegistryPreview\"
|
||||
Generate-FileComponents -fileListName "RegistryPreviewAssetsFiles" -wxsFilePath $PSScriptRoot\RegistryPreview.wxs -regroot $registryroot
|
||||
|
||||
#KeystrokeOverlay
|
||||
Generate-FileList -fileDepsJson "" -fileListName KeystrokeOverlayAssetsFiles -wxsFilePath $PSScriptRoot\KeystrokeOverlay.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\KeystrokeOverlay\"
|
||||
Generate-FileComponents -fileListName "KeystrokeOverlayAssetsFiles" -wxsFilePath $PSScriptRoot\KeystrokeOverlay.wxs -regroot $registryroot
|
||||
|
||||
#Run
|
||||
Generate-FileList -fileDepsJson "" -fileListName launcherImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\Assets\PowerLauncher"
|
||||
Generate-FileComponents -fileListName "launcherImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
## Plugins
|
||||
###Calculator
|
||||
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Calculator\Microsoft.PowerToys.Run.Plugin.Calculator.deps.json" -fileListName calcComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
|
||||
Generate-FileList -fileDepsJson "" -fileListName calcImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Calculator\Images"
|
||||
Generate-FileComponents -fileListName "calcComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
Generate-FileComponents -fileListName "calcImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
###Folder
|
||||
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Folder\Microsoft.Plugin.Folder.deps.json" -fileListName FolderComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
|
||||
Generate-FileList -fileDepsJson "" -fileListName FolderImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Folder\Images"
|
||||
Generate-FileComponents -fileListName "FolderComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
Generate-FileComponents -fileListName "FolderImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
###Program
|
||||
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Program\Microsoft.Plugin.Program.deps.json" -fileListName ProgramComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
|
||||
Generate-FileList -fileDepsJson "" -fileListName ProgramImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Program\Images"
|
||||
Generate-FileComponents -fileListName "ProgramComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
Generate-FileComponents -fileListName "ProgramImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
###Shell
|
||||
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Shell\Microsoft.Plugin.Shell.deps.json" -fileListName ShellComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
|
||||
Generate-FileList -fileDepsJson "" -fileListName ShellImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Shell\Images"
|
||||
Generate-FileComponents -fileListName "ShellComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
Generate-FileComponents -fileListName "ShellImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
###Indexer
|
||||
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Indexer\Microsoft.Plugin.Indexer.deps.json" -fileListName IndexerComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
|
||||
Generate-FileList -fileDepsJson "" -fileListName IndexerImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Indexer\Images"
|
||||
Generate-FileComponents -fileListName "IndexerComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
Generate-FileComponents -fileListName "IndexerImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
###UnitConverter
|
||||
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\UnitConverter\Community.PowerToys.Run.Plugin.UnitConverter.deps.json" -fileListName UnitConvCompFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
|
||||
Generate-FileList -fileDepsJson "" -fileListName UnitConvImagesCompFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\UnitConverter\Images"
|
||||
Generate-FileComponents -fileListName "UnitConvCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
Generate-FileComponents -fileListName "UnitConvImagesCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
###WebSearch
|
||||
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\WebSearch\Community.PowerToys.Run.Plugin.WebSearch.deps.json" -fileListName WebSrchCompFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
|
||||
Generate-FileList -fileDepsJson "" -fileListName WebSrchImagesCompFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\WebSearch\Images"
|
||||
Generate-FileComponents -fileListName "WebSrchCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
Generate-FileComponents -fileListName "WebSrchImagesCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
###History
|
||||
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\History\Microsoft.PowerToys.Run.Plugin.History.deps.json" -fileListName HistoryPluginComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
|
||||
Generate-FileList -fileDepsJson "" -fileListName HistoryPluginImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\History\Images"
|
||||
Generate-FileComponents -fileListName "HistoryPluginComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
Generate-FileComponents -fileListName "HistoryPluginImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
###Uri
|
||||
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Uri\Microsoft.Plugin.Uri.deps.json" -fileListName UriComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
|
||||
Generate-FileList -fileDepsJson "" -fileListName UriImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Uri\Images"
|
||||
Generate-FileComponents -fileListName "UriComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
Generate-FileComponents -fileListName "UriImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
###VSCodeWorkspaces
|
||||
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\VSCodeWorkspaces\Community.PowerToys.Run.Plugin.VSCodeWorkspaces.deps.json" -fileListName VSCWrkCompFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
|
||||
Generate-FileList -fileDepsJson "" -fileListName VSCWrkImagesCompFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\VSCodeWorkspaces\Images"
|
||||
Generate-FileComponents -fileListName "VSCWrkCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
Generate-FileComponents -fileListName "VSCWrkImagesCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
###WindowWalker
|
||||
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\WindowWalker\Microsoft.Plugin.WindowWalker.deps.json" -fileListName WindowWlkrCompFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
|
||||
Generate-FileList -fileDepsJson "" -fileListName WindowWlkrImagesCompFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\WindowWalker\Images"
|
||||
Generate-FileComponents -fileListName "WindowWlkrCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
Generate-FileComponents -fileListName "WindowWlkrImagesCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
###OneNote
|
||||
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\OneNote\Microsoft.PowerToys.Run.Plugin.OneNote.deps.json" -fileListName OneNoteComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
|
||||
Generate-FileList -fileDepsJson "" -fileListName OneNoteImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\OneNote\Images"
|
||||
Generate-FileComponents -fileListName "OneNoteComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
Generate-FileComponents -fileListName "OneNoteImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
###Registry
|
||||
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Registry\Microsoft.PowerToys.Run.Plugin.Registry.deps.json" -fileListName RegistryComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
|
||||
Generate-FileList -fileDepsJson "" -fileListName RegistryImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Registry\Images"
|
||||
Generate-FileComponents -fileListName "RegistryComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
Generate-FileComponents -fileListName "RegistryImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
###Service
|
||||
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Service\Microsoft.PowerToys.Run.Plugin.Service.deps.json" -fileListName ServiceComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
|
||||
Generate-FileList -fileDepsJson "" -fileListName ServiceImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Service\Images"
|
||||
Generate-FileComponents -fileListName "ServiceComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
Generate-FileComponents -fileListName "ServiceImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
###System
|
||||
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\System\Microsoft.PowerToys.Run.Plugin.System.deps.json" -fileListName SystemComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
|
||||
Generate-FileList -fileDepsJson "" -fileListName SystemImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\System\Images"
|
||||
Generate-FileComponents -fileListName "SystemComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
Generate-FileComponents -fileListName "SystemImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
###TimeDate
|
||||
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\TimeDate\Microsoft.PowerToys.Run.Plugin.TimeDate.deps.json" -fileListName TimeDateComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
|
||||
Generate-FileList -fileDepsJson "" -fileListName TimeDateImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\TimeDate\Images"
|
||||
Generate-FileComponents -fileListName "TimeDateComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
Generate-FileComponents -fileListName "TimeDateImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
###WindowsSettings
|
||||
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\WindowsSettings\Microsoft.PowerToys.Run.Plugin.WindowsSettings.deps.json" -fileListName WinSetCmpFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
|
||||
Generate-FileList -fileDepsJson "" -fileListName WinSetImagesCmpFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\WindowsSettings\Images"
|
||||
Generate-FileComponents -fileListName "WinSetCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
Generate-FileComponents -fileListName "WinSetImagesCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
###WindowsTerminal
|
||||
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\WindowsTerminal\Microsoft.PowerToys.Run.Plugin.WindowsTerminal.deps.json" -fileListName WinTermCmpFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
|
||||
Generate-FileList -fileDepsJson "" -fileListName WinTermImagesCmpFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\WindowsTerminal\Images"
|
||||
Generate-FileComponents -fileListName "WinTermCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
Generate-FileComponents -fileListName "WinTermImagesCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
###PowerToys
|
||||
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\PowerToys\Microsoft.PowerToys.Run.Plugin.PowerToys.deps.json" -fileListName PowerToysCmpFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
|
||||
Generate-FileList -fileDepsJson "" -fileListName PowerToysImagesCmpFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\PowerToys\Images"
|
||||
Generate-FileComponents -fileListName "PowerToysCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
Generate-FileComponents -fileListName "PowerToysImagesCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
###ValueGenerator
|
||||
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\ValueGenerator\Community.PowerToys.Run.Plugin.ValueGenerator.deps.json" -fileListName ValueGeneratorCmpFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
|
||||
Generate-FileList -fileDepsJson "" -fileListName ValueGeneratorImagesCmpFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\ValueGenerator\Images"
|
||||
Generate-FileComponents -fileListName "ValueGeneratorCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
Generate-FileComponents -fileListName "ValueGeneratorImagesCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
## Plugins
|
||||
|
||||
#ShortcutGuide
|
||||
Generate-FileList -fileDepsJson "" -fileListName ShortcutGuideSvgFiles -wxsFilePath $PSScriptRoot\ShortcutGuide.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\Assets\ShortcutGuide\"
|
||||
Generate-FileComponents -fileListName "ShortcutGuideSvgFiles" -wxsFilePath $PSScriptRoot\ShortcutGuide.wxs -regroot $registryroot
|
||||
|
||||
#Settings
|
||||
Generate-FileList -fileDepsJson "" -fileListName SettingsV2AssetsFiles -wxsFilePath $PSScriptRoot\Settings.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\Settings\"
|
||||
Generate-FileList -fileDepsJson "" -fileListName SettingsV2AssetsModulesFiles -wxsFilePath $PSScriptRoot\Settings.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\Settings\Modules\"
|
||||
Generate-FileList -fileDepsJson "" -fileListName SettingsV2OOBEAssetsModulesFiles -wxsFilePath $PSScriptRoot\Settings.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\Settings\Modules\OOBE\"
|
||||
Generate-FileList -fileDepsJson "" -fileListName SettingsV2OOBEAssetsFluentIconsFiles -wxsFilePath $PSScriptRoot\Settings.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\Settings\Icons\"
|
||||
Generate-FileComponents -fileListName "SettingsV2AssetsFiles" -wxsFilePath $PSScriptRoot\Settings.wxs -regroot $registryroot
|
||||
Generate-FileComponents -fileListName "SettingsV2AssetsModulesFiles" -wxsFilePath $PSScriptRoot\Settings.wxs -regroot $registryroot
|
||||
Generate-FileComponents -fileListName "SettingsV2OOBEAssetsModulesFiles" -wxsFilePath $PSScriptRoot\Settings.wxs -regroot $registryroot
|
||||
Generate-FileComponents -fileListName "SettingsV2OOBEAssetsFluentIconsFiles" -wxsFilePath $PSScriptRoot\Settings.wxs -regroot $registryroot
|
||||
|
||||
#Workspaces
|
||||
Generate-FileList -fileDepsJson "" -fileListName WorkspacesImagesComponentFiles -wxsFilePath $PSScriptRoot\Workspaces.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\Assets\Workspaces\"
|
||||
Generate-FileComponents -fileListName "WorkspacesImagesComponentFiles" -wxsFilePath $PSScriptRoot\Workspaces.wxs -regroot $registryroot
|
||||
@@ -136,6 +136,7 @@ call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuil
|
||||
<Compile Include="Resources.wxs" />
|
||||
<Compile Include="WinAppSDK.wxs" />
|
||||
<Compile Include="Workspaces.wxs" />
|
||||
<Compile Include="KeystrokeOverlay.wxs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="CustomDialogs" />
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
<ComponentGroupRef Id="AdvancedPasteComponentGroup" />
|
||||
<ComponentGroupRef Id="NewPlusComponentGroup" />
|
||||
<ComponentGroupRef Id="NewPlusTemplatesComponentGroup" />
|
||||
<ComponentGroupRef Id="KeystrokeOverlayComponentGroup" />
|
||||
<ComponentGroupRef Id="ResourcesComponentGroup" />
|
||||
<ComponentGroupRef Id="DscResourcesComponentGroup" />
|
||||
<ComponentGroupRef Id="WindowsAppSDKComponentGroup" />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -288,4 +288,8 @@ namespace winrt::PowerToys::GPOWrapper::implementation
|
||||
{
|
||||
return static_cast<GpoRuleConfigured>(powertoys_gpo::getConfiguredRunAtStartupValue());
|
||||
}
|
||||
GpoRuleConfigured GPOWrapper::GetConfiguredKeystrokeOverlayEnabledValue()
|
||||
{
|
||||
return static_cast<GpoRuleConfigured>(powertoys_gpo::getConfiguredKeystrokeOverlayEnabledValue());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,6 +77,7 @@ namespace winrt::PowerToys::GPOWrapper::implementation
|
||||
static GpoRuleConfigured GetAllowDataDiagnosticsValue();
|
||||
static GpoRuleConfigured GetConfiguredRunAtStartupValue();
|
||||
static GpoRuleConfigured GetConfiguredNewPlusReplaceVariablesValue();
|
||||
static GpoRuleConfigured GetConfiguredKeystrokeOverlayEnabledValue();
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -81,6 +81,7 @@ namespace PowerToys
|
||||
static GpoRuleConfigured GetAllowDataDiagnosticsValue();
|
||||
static GpoRuleConfigured GetConfiguredRunAtStartupValue();
|
||||
static GpoRuleConfigured GetConfiguredNewPlusReplaceVariablesValue();
|
||||
static GpoRuleConfigured GetConfiguredKeystrokeOverlayEnabledValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,5 +66,10 @@ namespace PowerToys.GPOWrapperProjection
|
||||
{
|
||||
return (GpoRuleConfigured)PowerToys.GPOWrapper.GPOWrapper.GetConfiguredWorkspacesEnabledValue();
|
||||
}
|
||||
|
||||
public static GpoRuleConfigured GetConfiguredKeystrokeOverlayEnabledValue()
|
||||
{
|
||||
return (GpoRuleConfigured)PowerToys.GPOWrapper.GPOWrapper.GetConfiguredKeystrokeOverlayEnabledValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ namespace ManagedCommon
|
||||
Hosts,
|
||||
ImageResizer,
|
||||
KeyboardManager,
|
||||
KeystrokeOverlay,
|
||||
LightSwitch,
|
||||
MouseHighlighter,
|
||||
MouseJump,
|
||||
|
||||
399
src/common/UITestAutomation/ScreenRecording.cs
Normal file
399
src/common/UITestAutomation/ScreenRecording.cs
Normal file
@@ -0,0 +1,399 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Imaging;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.PowerToys.UITest
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides methods for recording the screen during UI tests.
|
||||
/// Requires FFmpeg to be installed and available in PATH.
|
||||
/// </summary>
|
||||
internal class ScreenRecording : IDisposable
|
||||
{
|
||||
private readonly string outputDirectory;
|
||||
private readonly string framesDirectory;
|
||||
private readonly string outputFilePath;
|
||||
private readonly List<string> capturedFrames;
|
||||
private readonly SemaphoreSlim recordingLock = new(1, 1);
|
||||
private readonly Stopwatch recordingStopwatch = new();
|
||||
private readonly string? ffmpegPath;
|
||||
private CancellationTokenSource? recordingCancellation;
|
||||
private Task? recordingTask;
|
||||
private bool isRecording;
|
||||
private int frameCount;
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr GetDC(IntPtr hWnd);
|
||||
|
||||
[DllImport("gdi32.dll")]
|
||||
private static extern int GetDeviceCaps(IntPtr hdc, int nIndex);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr ReleaseDC(IntPtr hWnd, IntPtr hDC);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool GetCursorInfo(out ScreenCapture.CURSORINFO pci);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool DrawIconEx(IntPtr hdc, int x, int y, IntPtr hIcon, int cx, int cy, int istepIfAniCur, IntPtr hbrFlickerFreeDraw, int diFlags);
|
||||
|
||||
private const int CURSORSHOWING = 0x00000001;
|
||||
private const int DESKTOPHORZRES = 118;
|
||||
private const int DESKTOPVERTRES = 117;
|
||||
private const int DINORMAL = 0x0003;
|
||||
private const int TargetFps = 15; // 15 FPS for good balance of quality and size
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ScreenRecording"/> class.
|
||||
/// </summary>
|
||||
/// <param name="outputDirectory">Directory where the recording will be saved.</param>
|
||||
public ScreenRecording(string outputDirectory)
|
||||
{
|
||||
this.outputDirectory = outputDirectory;
|
||||
string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
|
||||
framesDirectory = Path.Combine(outputDirectory, $"frames_{timestamp}");
|
||||
outputFilePath = Path.Combine(outputDirectory, $"recording_{timestamp}.mp4");
|
||||
capturedFrames = new List<string>();
|
||||
frameCount = 0;
|
||||
|
||||
// Check if FFmpeg is available
|
||||
ffmpegPath = FindFfmpeg();
|
||||
if (ffmpegPath == null)
|
||||
{
|
||||
Console.WriteLine("FFmpeg not found. Screen recording will be disabled.");
|
||||
Console.WriteLine("To enable video recording, install FFmpeg: https://ffmpeg.org/download.html");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether screen recording is available (FFmpeg found).
|
||||
/// </summary>
|
||||
public bool IsAvailable => ffmpegPath != null;
|
||||
|
||||
/// <summary>
|
||||
/// Starts recording the screen.
|
||||
/// </summary>
|
||||
/// <returns>A task representing the asynchronous operation.</returns>
|
||||
public async Task StartRecordingAsync()
|
||||
{
|
||||
await recordingLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
if (isRecording || !IsAvailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Create frames directory
|
||||
Directory.CreateDirectory(framesDirectory);
|
||||
|
||||
recordingCancellation = new CancellationTokenSource();
|
||||
isRecording = true;
|
||||
recordingStopwatch.Start();
|
||||
|
||||
// Start the recording task
|
||||
recordingTask = Task.Run(() => RecordFrames(recordingCancellation.Token));
|
||||
|
||||
Console.WriteLine($"Started screen recording at {TargetFps} FPS");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Failed to start recording: {ex.Message}");
|
||||
isRecording = false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
recordingLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops recording and encodes video.
|
||||
/// </summary>
|
||||
/// <returns>A task representing the asynchronous operation.</returns>
|
||||
public async Task StopRecordingAsync()
|
||||
{
|
||||
await recordingLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
if (!isRecording || recordingCancellation == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Signal cancellation
|
||||
recordingCancellation.Cancel();
|
||||
|
||||
// Wait for recording task to complete
|
||||
if (recordingTask != null)
|
||||
{
|
||||
await recordingTask;
|
||||
}
|
||||
|
||||
recordingStopwatch.Stop();
|
||||
isRecording = false;
|
||||
|
||||
double duration = recordingStopwatch.Elapsed.TotalSeconds;
|
||||
Console.WriteLine($"Recording stopped. Captured {capturedFrames.Count} frames in {duration:F2} seconds");
|
||||
|
||||
// Encode to video
|
||||
await EncodeToVideoAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error stopping recording: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Cleanup();
|
||||
recordingLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records frames from the screen.
|
||||
/// </summary>
|
||||
private void RecordFrames(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
int frameInterval = 1000 / TargetFps;
|
||||
var frameTimer = Stopwatch.StartNew();
|
||||
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
var frameStart = frameTimer.ElapsedMilliseconds;
|
||||
|
||||
try
|
||||
{
|
||||
CaptureFrame();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error capturing frame: {ex.Message}");
|
||||
}
|
||||
|
||||
// Sleep for remaining time to maintain target FPS
|
||||
var frameTime = frameTimer.ElapsedMilliseconds - frameStart;
|
||||
var sleepTime = Math.Max(0, frameInterval - (int)frameTime);
|
||||
|
||||
if (sleepTime > 0)
|
||||
{
|
||||
Thread.Sleep(sleepTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Expected when stopping
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error during recording: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Captures a single frame.
|
||||
/// </summary>
|
||||
private void CaptureFrame()
|
||||
{
|
||||
IntPtr hdc = GetDC(IntPtr.Zero);
|
||||
int screenWidth = GetDeviceCaps(hdc, DESKTOPHORZRES);
|
||||
int screenHeight = GetDeviceCaps(hdc, DESKTOPVERTRES);
|
||||
ReleaseDC(IntPtr.Zero, hdc);
|
||||
|
||||
Rectangle bounds = new Rectangle(0, 0, screenWidth, screenHeight);
|
||||
using (Bitmap bitmap = new Bitmap(bounds.Width, bounds.Height, PixelFormat.Format24bppRgb))
|
||||
{
|
||||
using (Graphics g = Graphics.FromImage(bitmap))
|
||||
{
|
||||
g.CopyFromScreen(bounds.Location, Point.Empty, bounds.Size);
|
||||
|
||||
ScreenCapture.CURSORINFO cursorInfo;
|
||||
cursorInfo.CbSize = Marshal.SizeOf<ScreenCapture.CURSORINFO>();
|
||||
if (GetCursorInfo(out cursorInfo) && cursorInfo.Flags == CURSORSHOWING)
|
||||
{
|
||||
IntPtr hdcDest = g.GetHdc();
|
||||
DrawIconEx(hdcDest, cursorInfo.PTScreenPos.X, cursorInfo.PTScreenPos.Y, cursorInfo.HCursor, 0, 0, 0, IntPtr.Zero, DINORMAL);
|
||||
g.ReleaseHdc(hdcDest);
|
||||
}
|
||||
}
|
||||
|
||||
string framePath = Path.Combine(framesDirectory, $"frame_{frameCount:D6}.jpg");
|
||||
bitmap.Save(framePath, ImageFormat.Jpeg);
|
||||
capturedFrames.Add(framePath);
|
||||
frameCount++;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encodes captured frames to video using ffmpeg.
|
||||
/// </summary>
|
||||
private async Task EncodeToVideoAsync()
|
||||
{
|
||||
if (capturedFrames.Count == 0)
|
||||
{
|
||||
Console.WriteLine("No frames captured");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Build ffmpeg command with proper non-interactive flags
|
||||
string inputPattern = Path.Combine(framesDirectory, "frame_%06d.jpg");
|
||||
|
||||
// -y: overwrite without asking
|
||||
// -nostdin: disable interaction
|
||||
// -loglevel error: only show errors
|
||||
// -stats: show encoding progress
|
||||
string args = $"-y -nostdin -loglevel error -stats -framerate {TargetFps} -i \"{inputPattern}\" -c:v libx264 -pix_fmt yuv420p -crf 23 \"{outputFilePath}\"";
|
||||
|
||||
Console.WriteLine($"Encoding {capturedFrames.Count} frames to video...");
|
||||
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = ffmpegPath!,
|
||||
Arguments = args,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
RedirectStandardInput = true, // Important: redirect stdin to prevent hanging
|
||||
CreateNoWindow = true,
|
||||
};
|
||||
|
||||
using var process = Process.Start(startInfo);
|
||||
if (process != null)
|
||||
{
|
||||
// Close stdin immediately to ensure FFmpeg doesn't wait for input
|
||||
process.StandardInput.Close();
|
||||
|
||||
// Read output streams asynchronously to prevent deadlock
|
||||
var outputTask = process.StandardOutput.ReadToEndAsync();
|
||||
var errorTask = process.StandardError.ReadToEndAsync();
|
||||
|
||||
// Wait for process to exit
|
||||
await process.WaitForExitAsync();
|
||||
|
||||
// Get the output
|
||||
string stdout = await outputTask;
|
||||
string stderr = await errorTask;
|
||||
|
||||
if (process.ExitCode == 0 && File.Exists(outputFilePath))
|
||||
{
|
||||
var fileInfo = new FileInfo(outputFilePath);
|
||||
Console.WriteLine($"Video created: {outputFilePath} ({fileInfo.Length / 1024 / 1024:F1} MB)");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"FFmpeg encoding failed with exit code {process.ExitCode}");
|
||||
if (!string.IsNullOrWhiteSpace(stderr))
|
||||
{
|
||||
Console.WriteLine($"FFmpeg error: {stderr}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error encoding video: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds ffmpeg executable.
|
||||
/// </summary>
|
||||
private static string? FindFfmpeg()
|
||||
{
|
||||
// Check if ffmpeg is in PATH
|
||||
var pathDirs = Environment.GetEnvironmentVariable("PATH")?.Split(Path.PathSeparator) ?? Array.Empty<string>();
|
||||
|
||||
foreach (var dir in pathDirs)
|
||||
{
|
||||
var ffmpegPath = Path.Combine(dir, "ffmpeg.exe");
|
||||
if (File.Exists(ffmpegPath))
|
||||
{
|
||||
return ffmpegPath;
|
||||
}
|
||||
}
|
||||
|
||||
// Check common installation locations
|
||||
var commonPaths = new[]
|
||||
{
|
||||
@"C:\.tools\ffmpeg\bin\ffmpeg.exe",
|
||||
@"C:\ffmpeg\bin\ffmpeg.exe",
|
||||
@"C:\Program Files\ffmpeg\bin\ffmpeg.exe",
|
||||
@"C:\Program Files (x86)\ffmpeg\bin\ffmpeg.exe",
|
||||
@$"{Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)}\Microsoft\WinGet\Links\ffmpeg.exe",
|
||||
};
|
||||
|
||||
foreach (var path in commonPaths)
|
||||
{
|
||||
if (File.Exists(path))
|
||||
{
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path to the recorded video file.
|
||||
/// </summary>
|
||||
public string OutputFilePath => outputFilePath;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the directory containing recordings.
|
||||
/// </summary>
|
||||
public string OutputDirectory => outputDirectory;
|
||||
|
||||
/// <summary>
|
||||
/// Cleans up resources.
|
||||
/// </summary>
|
||||
private void Cleanup()
|
||||
{
|
||||
recordingCancellation?.Dispose();
|
||||
recordingCancellation = null;
|
||||
recordingTask = null;
|
||||
|
||||
// Clean up frames directory if it exists
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(framesDirectory))
|
||||
{
|
||||
Directory.Delete(framesDirectory, true);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Failed to cleanup frames directory: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (isRecording)
|
||||
{
|
||||
StopRecordingAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
Cleanup();
|
||||
recordingLock.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -130,9 +130,13 @@ namespace Microsoft.PowerToys.UITest
|
||||
/// </summary>
|
||||
/// <param name="appPath">The path to the application executable.</param>
|
||||
/// <param name="args">Optional command line arguments to pass to the application.</param>
|
||||
public void StartExe(string appPath, string[]? args = null)
|
||||
public void StartExe(string appPath, string[]? args = null, string? enableModules = null)
|
||||
{
|
||||
var opts = new AppiumOptions();
|
||||
if (!string.IsNullOrEmpty(enableModules))
|
||||
{
|
||||
opts.AddAdditionalCapability("enableModules", enableModules);
|
||||
}
|
||||
|
||||
if (scope == PowerToysModule.PowerToysSettings)
|
||||
{
|
||||
@@ -169,27 +173,66 @@ namespace Microsoft.PowerToys.UITest
|
||||
|
||||
private void TryLaunchPowerToysSettings(AppiumOptions opts)
|
||||
{
|
||||
try
|
||||
if (opts.ToCapabilities().HasCapability("enableModules"))
|
||||
{
|
||||
var runnerProcessInfo = new ProcessStartInfo
|
||||
var modulesString = (string)opts.ToCapabilities().GetCapability("enableModules");
|
||||
var modulesArray = modulesString.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
SettingsConfigHelper.ConfigureGlobalModuleSettings(modulesArray);
|
||||
}
|
||||
else
|
||||
{
|
||||
SettingsConfigHelper.ConfigureGlobalModuleSettings();
|
||||
}
|
||||
|
||||
const int maxTries = 3;
|
||||
const int delayMs = 5000;
|
||||
const int maxRetries = 3;
|
||||
|
||||
for (int tryCount = 1; tryCount <= maxTries; tryCount++)
|
||||
{
|
||||
try
|
||||
{
|
||||
FileName = locationPath + runnerPath,
|
||||
Verb = "runas",
|
||||
Arguments = "--open-settings",
|
||||
};
|
||||
var runnerProcessInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = locationPath + runnerPath,
|
||||
Verb = "runas",
|
||||
Arguments = "--open-settings",
|
||||
};
|
||||
|
||||
ExitExe(runnerProcessInfo.FileName);
|
||||
runner = Process.Start(runnerProcessInfo);
|
||||
ExitExe(runnerProcessInfo.FileName);
|
||||
|
||||
WaitForWindowAndSetCapability(opts, "PowerToys Settings", 5000, 5);
|
||||
// Verify process was killed
|
||||
string exeName = Path.GetFileNameWithoutExtension(runnerProcessInfo.FileName);
|
||||
var remainingProcesses = Process.GetProcessesByName(exeName);
|
||||
|
||||
// Exit CmdPal UI before launching new process if use installer for test
|
||||
ExitExeByName("Microsoft.CmdPal.UI");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to launch PowerToys Settings: {ex.Message}", ex);
|
||||
runner = Process.Start(runnerProcessInfo);
|
||||
|
||||
if (WaitForWindowAndSetCapability(opts, "PowerToys Settings", delayMs, maxRetries))
|
||||
{
|
||||
// Exit CmdPal UI before launching new process if use installer for test
|
||||
ExitExeByName("Microsoft.CmdPal.UI");
|
||||
return;
|
||||
}
|
||||
|
||||
// Window not found, kill all PowerToys processes and retry
|
||||
if (tryCount < maxTries)
|
||||
{
|
||||
KillPowerToysProcesses();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (tryCount == maxTries)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to launch PowerToys Settings after {maxTries} attempts: {ex.Message}", ex);
|
||||
}
|
||||
|
||||
// Kill processes and retry
|
||||
KillPowerToysProcesses();
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Failed to launch PowerToys Settings: Window not found after {maxTries} attempts.");
|
||||
}
|
||||
|
||||
private void TryLaunchCommandPalette(AppiumOptions opts)
|
||||
@@ -211,7 +254,10 @@ namespace Microsoft.PowerToys.UITest
|
||||
var process = Process.Start(processStartInfo);
|
||||
process?.WaitForExit();
|
||||
|
||||
WaitForWindowAndSetCapability(opts, "Command Palette", 5000, 10);
|
||||
if (!WaitForWindowAndSetCapability(opts, "Command Palette", 5000, 10))
|
||||
{
|
||||
throw new TimeoutException("Failed to find Command Palette window after multiple attempts.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -219,7 +265,7 @@ namespace Microsoft.PowerToys.UITest
|
||||
}
|
||||
}
|
||||
|
||||
private void WaitForWindowAndSetCapability(AppiumOptions opts, string windowName, int delayMs, int maxRetries)
|
||||
private bool WaitForWindowAndSetCapability(AppiumOptions opts, string windowName, int delayMs, int maxRetries)
|
||||
{
|
||||
for (int attempt = 1; attempt <= maxRetries; attempt++)
|
||||
{
|
||||
@@ -230,18 +276,16 @@ namespace Microsoft.PowerToys.UITest
|
||||
{
|
||||
var hexHwnd = window[0].HWnd.ToString("x");
|
||||
opts.AddAdditionalCapability("appTopLevelWindow", hexHwnd);
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (attempt < maxRetries)
|
||||
{
|
||||
Thread.Sleep(delayMs);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new TimeoutException($"Failed to find {windowName} window after multiple attempts.");
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -292,17 +336,17 @@ namespace Microsoft.PowerToys.UITest
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Handle exceptions if needed
|
||||
Debug.WriteLine($"Exception during Cleanup: {ex.Message}");
|
||||
Console.WriteLine($"Exception during Cleanup: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restarts now exe and takes control of it.
|
||||
/// </summary>
|
||||
public void RestartScopeExe()
|
||||
public void RestartScopeExe(string? enableModules = null)
|
||||
{
|
||||
ExitScopeExe();
|
||||
StartExe(locationPath + sessionPath, this.commandLineArgs);
|
||||
StartExe(locationPath + sessionPath, commandLineArgs, enableModules);
|
||||
}
|
||||
|
||||
public WindowsDriver<WindowsElement> GetRoot()
|
||||
@@ -327,5 +371,31 @@ namespace Microsoft.PowerToys.UITest
|
||||
this.ExitExe(winAppDriverProcessInfo.FileName);
|
||||
SessionHelper.appDriver = Process.Start(winAppDriverProcessInfo);
|
||||
}
|
||||
|
||||
private void KillPowerToysProcesses()
|
||||
{
|
||||
var powerToysProcessNames = new[] { "PowerToys", "Microsoft.CmdPal.UI" };
|
||||
|
||||
foreach (var processName in powerToysProcessNames)
|
||||
{
|
||||
try
|
||||
{
|
||||
var processes = Process.GetProcessesByName(processName);
|
||||
|
||||
foreach (var process in processes)
|
||||
{
|
||||
process.Kill();
|
||||
process.WaitForExit();
|
||||
}
|
||||
|
||||
// Verify processes are actually gone
|
||||
var remainingProcesses = Process.GetProcessesByName(processName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[KillPowerToysProcesses] Failed to kill process {processName}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,14 +26,13 @@ namespace Microsoft.PowerToys.UITest
|
||||
/// <summary>
|
||||
/// Configures global PowerToys settings to enable only specified modules and disable all others.
|
||||
/// </summary>
|
||||
/// <param name="modulesToEnable">Array of module names to enable (e.g., "Peek", "FancyZones"). All other modules will be disabled.</param>
|
||||
/// <exception cref="ArgumentNullException">Thrown when modulesToEnable is null.</exception>
|
||||
/// <param name="modulesToEnable">Array of module names to enable (e.g., "Peek", "FancyZones"). All other modules will be disabled. If null or empty, all modules will be disabled.</param>
|
||||
/// <exception cref="InvalidOperationException">Thrown when settings file operations fail.</exception>
|
||||
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "This is test code and will not be trimmed")]
|
||||
[UnconditionalSuppressMessage("AOT", "IL3050", Justification = "This is test code and will not be AOT compiled")]
|
||||
public static void ConfigureGlobalModuleSettings(params string[] modulesToEnable)
|
||||
public static void ConfigureGlobalModuleSettings(params string[]? modulesToEnable)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(modulesToEnable);
|
||||
modulesToEnable ??= Array.Empty<string>();
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
@@ -29,6 +29,8 @@ namespace Microsoft.PowerToys.UITest
|
||||
|
||||
public string? ScreenshotDirectory { get; set; }
|
||||
|
||||
public string? RecordingDirectory { get; set; }
|
||||
|
||||
public static MonitorInfoData.ParamsWrapper MonitorInfoData { get; set; } = new MonitorInfoData.ParamsWrapper() { Monitors = new List<MonitorInfoData.MonitorInfoDataWrapper>() };
|
||||
|
||||
private readonly PowerToysModule scope;
|
||||
@@ -36,6 +38,7 @@ namespace Microsoft.PowerToys.UITest
|
||||
private readonly string[]? commandLineArgs;
|
||||
private SessionHelper? sessionHelper;
|
||||
private System.Threading.Timer? screenshotTimer;
|
||||
private ScreenRecording? screenRecording;
|
||||
|
||||
public UITestBase(PowerToysModule scope = PowerToysModule.PowerToysSettings, WindowSize size = WindowSize.UnSpecified, string[]? commandLineArgs = null)
|
||||
{
|
||||
@@ -65,12 +68,35 @@ namespace Microsoft.PowerToys.UITest
|
||||
CloseOtherApplications();
|
||||
if (IsInPipeline)
|
||||
{
|
||||
ScreenshotDirectory = Path.Combine(this.TestContext.TestResultsDirectory ?? string.Empty, "UITestScreenshots_" + Guid.NewGuid().ToString());
|
||||
string baseDirectory = this.TestContext.TestResultsDirectory ?? string.Empty;
|
||||
ScreenshotDirectory = Path.Combine(baseDirectory, "UITestScreenshots_" + Guid.NewGuid().ToString());
|
||||
Directory.CreateDirectory(ScreenshotDirectory);
|
||||
|
||||
RecordingDirectory = Path.Combine(baseDirectory, "UITestRecordings_" + Guid.NewGuid().ToString());
|
||||
Directory.CreateDirectory(RecordingDirectory);
|
||||
|
||||
// Take screenshot every 1 second
|
||||
screenshotTimer = new System.Threading.Timer(ScreenCapture.TimerCallback, ScreenshotDirectory, TimeSpan.Zero, TimeSpan.FromMilliseconds(1000));
|
||||
|
||||
// Start screen recording (requires FFmpeg)
|
||||
try
|
||||
{
|
||||
screenRecording = new ScreenRecording(RecordingDirectory);
|
||||
if (screenRecording.IsAvailable)
|
||||
{
|
||||
_ = screenRecording.StartRecordingAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
screenRecording = null;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Failed to start screen recording: {ex.Message}");
|
||||
screenRecording = null;
|
||||
}
|
||||
|
||||
// Escape Popups before starting
|
||||
System.Windows.Forms.SendKeys.SendWait("{ESC}");
|
||||
}
|
||||
@@ -88,15 +114,36 @@ namespace Microsoft.PowerToys.UITest
|
||||
if (IsInPipeline)
|
||||
{
|
||||
screenshotTimer?.Change(Timeout.Infinite, Timeout.Infinite);
|
||||
Dispose();
|
||||
|
||||
// Stop screen recording
|
||||
if (screenRecording != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
screenRecording.StopRecordingAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Failed to stop screen recording: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
if (TestContext.CurrentTestOutcome is UnitTestOutcome.Failed
|
||||
or UnitTestOutcome.Error
|
||||
or UnitTestOutcome.Unknown)
|
||||
{
|
||||
Task.Delay(1000).Wait();
|
||||
AddScreenShotsToTestResultsDirectory();
|
||||
AddRecordingsToTestResultsDirectory();
|
||||
AddLogFilesToTestResultsDirectory();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Clean up recording if test passed
|
||||
CleanupRecordingDirectory();
|
||||
}
|
||||
|
||||
Dispose();
|
||||
}
|
||||
|
||||
this.Session.Cleanup();
|
||||
@@ -106,6 +153,7 @@ namespace Microsoft.PowerToys.UITest
|
||||
public void Dispose()
|
||||
{
|
||||
screenshotTimer?.Dispose();
|
||||
screenRecording?.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
@@ -600,6 +648,47 @@ namespace Microsoft.PowerToys.UITest
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds screen recordings to test results directory when test fails.
|
||||
/// </summary>
|
||||
protected void AddRecordingsToTestResultsDirectory()
|
||||
{
|
||||
if (RecordingDirectory != null && Directory.Exists(RecordingDirectory))
|
||||
{
|
||||
// Add video files (MP4)
|
||||
var videoFiles = Directory.GetFiles(RecordingDirectory, "*.mp4");
|
||||
foreach (string file in videoFiles)
|
||||
{
|
||||
this.TestContext.AddResultFile(file);
|
||||
var fileInfo = new FileInfo(file);
|
||||
Console.WriteLine($"Added video recording: {Path.GetFileName(file)} ({fileInfo.Length / 1024 / 1024:F1} MB)");
|
||||
}
|
||||
|
||||
if (videoFiles.Length == 0)
|
||||
{
|
||||
Console.WriteLine("No video recording available (FFmpeg not found). Screenshots are still captured.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cleans up recording directory when test passes.
|
||||
/// </summary>
|
||||
private void CleanupRecordingDirectory()
|
||||
{
|
||||
if (RecordingDirectory != null && Directory.Exists(RecordingDirectory))
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.Delete(RecordingDirectory, true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Failed to cleanup recording directory: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies PowerToys log files to test results directory when test fails.
|
||||
/// Renames files to include the directory structure after \PowerToys.
|
||||
@@ -689,11 +778,11 @@ namespace Microsoft.PowerToys.UITest
|
||||
/// <summary>
|
||||
/// Restart scope exe.
|
||||
/// </summary>
|
||||
public void RestartScopeExe()
|
||||
public Session RestartScopeExe(string? enableModules = null)
|
||||
{
|
||||
this.sessionHelper!.RestartScopeExe();
|
||||
this.sessionHelper!.RestartScopeExe(enableModules);
|
||||
this.Session = new Session(this.sessionHelper.GetRoot(), this.sessionHelper.GetDriver(), this.scope, this.size);
|
||||
return;
|
||||
return Session;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -50,6 +50,7 @@ public sealed class SettingsResource : BaseResource
|
||||
{ nameof(ModuleType.Hosts), CreateModuleFunctionData<HostsSettings> },
|
||||
{ nameof(ModuleType.ImageResizer), CreateModuleFunctionData<ImageResizerSettings> },
|
||||
{ nameof(ModuleType.KeyboardManager), CreateModuleFunctionData<KeyboardManagerSettings> },
|
||||
{ nameof(ModuleType.KeystrokeOverlay), CreateModuleFunctionData<KeystrokeOverlaySettings> },
|
||||
{ nameof(ModuleType.MouseHighlighter), CreateModuleFunctionData<MouseHighlighterSettings> },
|
||||
{ nameof(ModuleType.MouseJump), CreateModuleFunctionData<MouseJumpSettings> },
|
||||
{ nameof(ModuleType.MousePointerCrosshairs), CreateModuleFunctionData<MousePointerCrosshairsSettings> },
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
6
src/modules/KeystrokeOverlay/.vscode/settings.json
vendored
Normal file
6
src/modules/KeystrokeOverlay/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"files.associations": {
|
||||
"*.embeddedhtml": "html",
|
||||
"xstring": "cpp"
|
||||
}
|
||||
}
|
||||
1
src/modules/KeystrokeOverlay/KeystrokeOverlayKeyboardService/.gitignore
vendored
Normal file
1
src/modules/KeystrokeOverlay/KeystrokeOverlayKeyboardService/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
# Ignore all Windows executable files
|
||||
@@ -0,0 +1,86 @@
|
||||
// Batcher.cpp
|
||||
// Worker thread that batches and sends keystroke data through IPC.
|
||||
#include "pch.h"
|
||||
#include "Batcher.h"
|
||||
#include <sstream>
|
||||
|
||||
// Escapes a string for JSON inclusion
|
||||
static std::string Escape(const std::string &s)
|
||||
{
|
||||
std::string o;
|
||||
o.reserve(s.size() + 4);
|
||||
for (char c : s)
|
||||
{
|
||||
switch (c)
|
||||
{
|
||||
case '"':
|
||||
o += "\\\"";
|
||||
break;
|
||||
case '\\':
|
||||
o += "\\\\";
|
||||
break;
|
||||
case '\n':
|
||||
o += "\\n";
|
||||
break;
|
||||
case '\r':
|
||||
o += "\\r";
|
||||
break;
|
||||
case '\t':
|
||||
o += "\\t";
|
||||
break;
|
||||
default:
|
||||
o += c;
|
||||
}
|
||||
}
|
||||
return o;
|
||||
}
|
||||
|
||||
// Starts the batcher thread
|
||||
void Batcher::Start()
|
||||
{
|
||||
if (_run.exchange(true))
|
||||
return;
|
||||
_t = std::thread([this]
|
||||
{
|
||||
std::vector<KeystrokeEvent> batch; batch.reserve(32);
|
||||
while (_run.load()) {
|
||||
// drain up to 32 events
|
||||
KeystrokeEvent ev;
|
||||
batch.clear();
|
||||
for (int i=0; i<32 && _q.try_pop(ev); ++i) batch.push_back(ev);
|
||||
|
||||
if (!batch.empty()) {
|
||||
std::ostringstream oss;
|
||||
oss << R"({"schema":1,"events":[)";
|
||||
for (size_t i=0; i<batch.size(); ++i) {
|
||||
const auto& e = batch[i];
|
||||
oss << R"({"t":")" << (e.type==KeystrokeEvent::Type::Down?"down":e.type==KeystrokeEvent::Type::Up?"up":"char") << R"(",)"
|
||||
<< R"("vk":)" << e.vk << ','
|
||||
<< R"("text":")" << (e.ch ? Escape(std::string{ static_cast<char>(e.ch) }) : "") << R"(",)"
|
||||
<< R"("mods":[)" << (e.mods[0]?"\"Ctrl\"":"")
|
||||
<< ((e.mods[0]&&e.mods[1])?",":"")
|
||||
<< (e.mods[1]?"\"Alt\"":"")
|
||||
<< (((e.mods[0]||e.mods[1])&&e.mods[2])?",":"")
|
||||
<< (e.mods[2]?"\"Shift\"":"")
|
||||
<< (((e.mods[0]||e.mods[1]||e.mods[2])&&e.mods[3])?",":"")
|
||||
<< (e.mods[3]?"\"Win\"":"")
|
||||
<< R"(],)"
|
||||
<< R"("ts":)" << (e.ts_micros/1'000'000.0)
|
||||
<< "}";
|
||||
if (i+1<batch.size()) oss << ",";
|
||||
}
|
||||
oss << "]}";
|
||||
_pipe.EnsureClient();
|
||||
_pipe.SendFrame(oss.str());
|
||||
}
|
||||
Sleep(8); // ~120Hz batching
|
||||
} });
|
||||
}
|
||||
|
||||
void Batcher::Stop()
|
||||
{
|
||||
if (!_run.exchange(false))
|
||||
return;
|
||||
if (_t.joinable())
|
||||
_t.join();
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// Batcher.h
|
||||
// Worker thread that batches and sends keystroke data through Named Pipe server.
|
||||
#pragma once
|
||||
#include "EventQueue.h"
|
||||
#include "KeystrokeEvent.h"
|
||||
#include "PipeServer.h"
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
|
||||
class Batcher
|
||||
{
|
||||
public:
|
||||
Batcher(SpscRing<KeystrokeEvent, 1024> &q) : _q(q) {}
|
||||
void Start();
|
||||
void Stop();
|
||||
|
||||
private:
|
||||
SpscRing<KeystrokeEvent, 1024> &_q;
|
||||
PipeServer _pipe;
|
||||
std::atomic<bool> _run{false};
|
||||
std::thread _t;
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
// EventQueue.h
|
||||
// Custom SPSC queue to store KeyStrokeEvent objects between producer and consumer threads.
|
||||
#pragma once
|
||||
#include <atomic> // atomic type for head and tail
|
||||
#include <vector>
|
||||
|
||||
template <typename T, size_t N>
|
||||
class SpscRing
|
||||
{
|
||||
public:
|
||||
// Attempts to push an item into the queue. Returns false if the queue is full.
|
||||
bool try_push(const T &v)
|
||||
{
|
||||
auto head = _head.load(std::memory_order_relaxed); // maybe fix pointer types later?
|
||||
auto next = (head + 1) % N;
|
||||
if (next == _tail.load(std::memory_order_acquire))
|
||||
return false; // full case
|
||||
_buf[head] = v;
|
||||
_head.store(next, std::memory_order_release);
|
||||
return true;
|
||||
}
|
||||
// Attempts to pop an item from the queue. Returns false if the queue is empty.
|
||||
bool try_pop(T &out)
|
||||
{
|
||||
auto tail = _tail.load(std::memory_order_relaxed);
|
||||
if (tail == _head.load(std::memory_order_acquire))
|
||||
return false; // empty case
|
||||
out = _buf[tail];
|
||||
_tail.store((tail + 1) % N, std::memory_order_release);
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
std::array<T, N> _buf{};
|
||||
std::atomic<size_t> _head{0}, _tail{0};
|
||||
};
|
||||
@@ -0,0 +1,135 @@
|
||||
#include "pch.h"
|
||||
#include <iostream>
|
||||
#include <windows.h>
|
||||
#include <map>
|
||||
#include <deque>
|
||||
#include <cstdint>
|
||||
#include <array>
|
||||
|
||||
#include "KeystrokeEvent.h"
|
||||
#include "EventQueue.h"
|
||||
#include "Batcher.h"
|
||||
|
||||
// Old compilation command saved for reference
|
||||
// Compilation: x86_64-w64-mingw32-g++ KeyboardListener.cpp -o KeyboardHookTest.exe -static -luser32 -lgdi32 (need to compile with "static" because of lack of DLL's)
|
||||
|
||||
// Global handle for the hook
|
||||
static HHOOK g_hook = nullptr;
|
||||
|
||||
static SpscRing<KeystrokeEvent, 1024> g_q;
|
||||
|
||||
// Batcher worker (drains g_q -> JSON -> named pipe)
|
||||
static Batcher g_batcher(g_q);
|
||||
|
||||
// Timestamping
|
||||
static inline uint64_t now_micros() {
|
||||
static LARGE_INTEGER freq = []{ LARGE_INTEGER f; QueryPerformanceFrequency(&f); return f; }();
|
||||
LARGE_INTEGER c; QueryPerformanceCounter(&c);
|
||||
return static_cast<uint64_t>((c.QuadPart * 1'000'000) / freq.QuadPart);
|
||||
}
|
||||
|
||||
// Modifier snapshot (Ctrl, Alt, Shift, Win)
|
||||
static inline std::array<bool,4> snapshot_mods() {
|
||||
auto down = [](int vk){ return (GetKeyState(vk) & 0x8000) != 0; };
|
||||
return std::array<bool,4>{
|
||||
down(VK_CONTROL), // Ctrl
|
||||
down(VK_MENU), // Alt
|
||||
down(VK_SHIFT), // Shift
|
||||
(down(VK_LWIN) || down(VK_RWIN)) // Win
|
||||
};
|
||||
}
|
||||
|
||||
// Translate vk to printable character
|
||||
static inline char32_t vk_to_char(UINT vk, UINT sc) {
|
||||
BYTE keystate[256];
|
||||
if (!GetKeyboardState(keystate)) return 0;
|
||||
|
||||
WCHAR buf[4] = {0};
|
||||
HKL layout = GetKeyboardLayout(0);
|
||||
|
||||
// Note: ToUnicodeEx can have side-effects for dead keys, this is "good enough" for an overlay.
|
||||
int rc = ToUnicodeEx(vk, sc, keystate, buf, 4, 0, layout);
|
||||
if (rc <= 0) return 0;
|
||||
if (!iswprint(buf[0])) return 0;
|
||||
return static_cast<char32_t>(buf[0]);
|
||||
}
|
||||
|
||||
// Push helpers
|
||||
static inline void emit_down(UINT vk, UINT sc) {
|
||||
KeystrokeEvent e{};
|
||||
e.type = KeystrokeEvent::Type::Down;
|
||||
e.vk = static_cast<uint16_t>(vk);
|
||||
e.ch = 0;
|
||||
e.mods = snapshot_mods();
|
||||
e.ts_micros = now_micros();
|
||||
g_q.try_push(e);
|
||||
|
||||
// Also emit a Char if there is a printable character for this Down
|
||||
if (char32_t ch = vk_to_char(vk, sc)) {
|
||||
KeystrokeEvent c = e;
|
||||
c.type = KeystrokeEvent::Type::Char;
|
||||
c.ch = ch;
|
||||
g_q.try_push(c);
|
||||
}
|
||||
}
|
||||
|
||||
static inline void emit_up(UINT vk) {
|
||||
KeystrokeEvent e{};
|
||||
e.type = KeystrokeEvent::Type::Up;
|
||||
e.vk = static_cast<uint16_t>(vk);
|
||||
e.ch = 0;
|
||||
e.mods = snapshot_mods();
|
||||
e.ts_micros = now_micros();
|
||||
g_q.try_push(e);
|
||||
}
|
||||
|
||||
// LL keyboard hook callback
|
||||
static LRESULT CALLBACK LowLevelKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) {
|
||||
if (nCode == HC_ACTION) {
|
||||
const KBDLLHOOKSTRUCT* p = reinterpret_cast<KBDLLHOOKSTRUCT*>(lParam);
|
||||
switch (wParam) {
|
||||
case WM_KEYDOWN:
|
||||
case WM_SYSKEYDOWN:
|
||||
emit_down(p->vkCode, p->scanCode);
|
||||
break;
|
||||
case WM_KEYUP:
|
||||
case WM_SYSKEYUP:
|
||||
emit_up(p->vkCode);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return CallNextHookEx(nullptr, nCode, wParam, lParam);
|
||||
}
|
||||
|
||||
// Install/uninstall hook
|
||||
static void SetGlobalHook() {
|
||||
g_hook = SetWindowsHookExW(WH_KEYBOARD_LL, LowLevelKeyboardProc, GetModuleHandleW(nullptr), 0);
|
||||
}
|
||||
|
||||
static void UnsetGlobalHook() {
|
||||
if (g_hook) { UnhookWindowsHookEx(g_hook); g_hook = nullptr; }
|
||||
}
|
||||
|
||||
// Main
|
||||
int main() {
|
||||
// Start the batcher FIRST so frames have somewhere to go
|
||||
g_batcher.Start();
|
||||
|
||||
SetGlobalHook();
|
||||
if (!g_hook) {
|
||||
// If hook fails, stop batcher and exit
|
||||
g_batcher.Stop();
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Required message loop for WH_KEYBOARD_LL owner thread
|
||||
MSG msg;
|
||||
while (GetMessageW(&msg, nullptr, 0, 0)) {
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessageW(&msg);
|
||||
}
|
||||
|
||||
UnsetGlobalHook();
|
||||
g_batcher.Stop();
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// KeystrokeEvent.h
|
||||
// Contains the definition of the KeystrokeEvent struct used to represent keyboard events.
|
||||
#pragma once // compiled once per build
|
||||
#include <cstdint>
|
||||
#include <array>
|
||||
#include <windows.h>
|
||||
|
||||
struct KeystrokeEvent
|
||||
{
|
||||
enum class Type : uint8_t
|
||||
{
|
||||
Down,
|
||||
Up,
|
||||
Char
|
||||
} type;
|
||||
DWORD vk; // virtual key code (DWORD type)
|
||||
char32_t ch; // Stores actual character being pressed. Skip for now. 0 if non-printable
|
||||
std::array<bool, 4> mods; // Ctrl, Alt, Shift, Win. Read from getkeyboard state and one other method (in hook).
|
||||
uint64_t ts_micros; // Timestamp in microseconds. Monotonic or UTC micros
|
||||
};
|
||||
|
||||
// Note, VKCodes don't distinguish between key cases (A vs a), therefore mod keys
|
||||
// need to be packaged in KeystrokeEvent in order to aid interpretation.
|
||||
@@ -0,0 +1,102 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
|
||||
<!-- Import CppWinRT props if available -->
|
||||
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props"
|
||||
Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" />
|
||||
|
||||
<!-- Global project settings -->
|
||||
<PropertyGroup Label="Globals">
|
||||
<VCProjectVersion>15.0</VCProjectVersion>
|
||||
<ProjectGuid>{A1B2C3D4-E5F6-4321-8765-123456789ABC}</ProjectGuid>
|
||||
<Keyword>Win32Proj</Keyword>
|
||||
<RootNamespace>KeystrokeOverlay</RootNamespace>
|
||||
<ProjectName>KeystrokeOverlayKeyboardService</ProjectName>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Load VS default props -->
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
|
||||
<!-- THIS IS THE IMPORTANT CHANGE -->
|
||||
<PropertyGroup Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType> <!-- NOW BUILDS AN EXE -->
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
|
||||
<!-- User props -->
|
||||
<ImportGroup Label="PropertySheets">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props"
|
||||
Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')"
|
||||
Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
|
||||
<PropertyGroup Label="UserMacros" />
|
||||
|
||||
<!-- Output directory — EXACTLY where your UI exe lives -->
|
||||
<PropertyGroup>
|
||||
<OutDir>$(SolutionDir)x64\$(Configuration)\WinUI3Apps\</OutDir>
|
||||
<IntDir>$(ProjectDir)Intermediate\$(Platform)\$(Configuration)\</IntDir>
|
||||
<TargetName>PowerToys.KeystrokeOverlayKeystrokeServer</TargetName>
|
||||
<TargetExt>.exe</TargetExt>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Compiler configuration -->
|
||||
<ItemDefinitionGroup>
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<Optimization>MaxSpeed</Optimization>
|
||||
<PreprocessorDefinitions>
|
||||
WIN32;_WINDOWS;%(PreprocessorDefinitions)
|
||||
</PreprocessorDefinitions>
|
||||
<AdditionalIncludeDirectories>
|
||||
$(ProjectDir);
|
||||
$(ProjectDir)..\..\..\common\inc;
|
||||
$(ProjectDir)..\..\..\common\Telemetry;
|
||||
$(ProjectDir)..\..\..;
|
||||
%(AdditionalIncludeDirectories)
|
||||
</AdditionalIncludeDirectories>
|
||||
</ClCompile>
|
||||
|
||||
<Link>
|
||||
<SubSystem>Console</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<AdditionalDependencies>
|
||||
user32.lib;
|
||||
gdi32.lib;
|
||||
kernel32.lib;
|
||||
advapi32.lib;
|
||||
%(AdditionalDependencies)
|
||||
</AdditionalDependencies>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
|
||||
<!-- Header files -->
|
||||
<ItemGroup>
|
||||
<ClInclude Include="pch.h" />
|
||||
<ClInclude Include="Batcher.h" />
|
||||
<ClInclude Include="EventQueue.h" />
|
||||
<ClInclude Include="KeystrokeEvent.h" />
|
||||
<ClInclude Include="Pipeserver.h" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Source files -->
|
||||
<ItemGroup>
|
||||
<ClCompile Include="Batcher.cpp" />
|
||||
<ClCompile Include="Pipeserver.cpp" />
|
||||
<ClCompile Include="KeyboardListener.cpp" /> <!-- ⭐ MAIN() HERE -->
|
||||
<ClCompile Include="pch.cpp">
|
||||
<PrecompiledHeader>Create</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Standard VS C++ targets -->
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
|
||||
<!-- spdlog integration -->
|
||||
<Import Project="..\..\..\..\deps\spdlog.props" />
|
||||
|
||||
<!-- No copy target needed — EXE is built directly into WinUI3Apps -->
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,71 @@
|
||||
// PipeServer.cpp
|
||||
// Administrates named pipes server for IPC communications.
|
||||
#include "pch.h"
|
||||
#include "PipeServer.h" // relies on named pipes api through windows.h
|
||||
|
||||
PipeServer::PipeServer(const wchar_t *name) : _name(name) {} // Constructor
|
||||
PipeServer::~PipeServer() { Close(); } // Destructor, calls Close()
|
||||
|
||||
// Check PipeServer.md for docs
|
||||
bool PipeServer::CreateAndListen()
|
||||
{
|
||||
Close(); // close instance if already open
|
||||
_hPipe = CreateNamedPipeW(
|
||||
_name.c_str(),
|
||||
PIPE_ACCESS_OUTBOUND | FILE_FLAG_FIRST_PIPE_INSTANCE,
|
||||
PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT,
|
||||
1, 1 << 16, 1 << 16, 0, nullptr);
|
||||
if (_hPipe == INVALID_HANDLE_VALUE)
|
||||
return false;
|
||||
BOOL ok = ConnectNamedPipe(_hPipe, nullptr) ? TRUE : (GetLastError() == ERROR_PIPE_CONNECTED);
|
||||
return ok == TRUE;
|
||||
}
|
||||
|
||||
// If no client is connected, wait for one.
|
||||
// Allows for worker thread to ensure a connection is present before sending stuff.
|
||||
bool PipeServer::EnsureClient()
|
||||
{
|
||||
if (_hPipe != INVALID_HANDLE_VALUE)
|
||||
return true;
|
||||
return CreateAndListen();
|
||||
}
|
||||
|
||||
// Sends a length-prefixed JSON frame through the pipe.
|
||||
bool PipeServer::SendFrame(const std::string &json)
|
||||
{
|
||||
// if no client, try to create and listen
|
||||
if (_hPipe == INVALID_HANDLE_VALUE && !CreateAndListen())
|
||||
return false;
|
||||
|
||||
DWORD len = static_cast<DWORD>(json.size()); // gets size of payload (DWORD type for this case)
|
||||
|
||||
// prevent giant payloads (over 8 MiB this case)
|
||||
if (len > 8 * 1024 * 1024)
|
||||
return false;
|
||||
|
||||
DWORD wrote = 0;
|
||||
|
||||
if (!WriteFile(_hPipe, &len, sizeof(len), &wrote, nullptr) || wrote != sizeof(len)) // write length prefix
|
||||
{
|
||||
Close(); // on failed write, close (and reset) pipe
|
||||
return false;
|
||||
}
|
||||
if (!WriteFile(_hPipe, json.data(), len, &wrote, nullptr) || wrote != len) // write payload
|
||||
{
|
||||
Close(); // on failed write, close (and reset) pipe
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Ensures any open handle is properly closed
|
||||
void PipeServer::Close()
|
||||
{
|
||||
if (_hPipe != INVALID_HANDLE_VALUE)
|
||||
{
|
||||
FlushFileBuffers(_hPipe);
|
||||
DisconnectNamedPipe(_hPipe);
|
||||
CloseHandle(_hPipe);
|
||||
_hPipe = INVALID_HANDLE_VALUE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// Pipeserver.h
|
||||
// Administrates named pipes server for IPC communications.
|
||||
#pragma once
|
||||
#include <windows.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class PipeServer
|
||||
{
|
||||
public:
|
||||
explicit PipeServer(const wchar_t *name = LR"(\\.\pipe\KeystrokeOverlayPipe)");
|
||||
~PipeServer(); // Destructor
|
||||
bool EnsureClient(); // Accept client if none
|
||||
bool SendFrame(const std::string &json); // [uint32 length][utf8]
|
||||
|
||||
private:
|
||||
std::wstring _name;
|
||||
HANDLE _hPipe = INVALID_HANDLE_VALUE;
|
||||
bool CreateAndListen();
|
||||
void Close();
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
#include "pch.h"
|
||||
@@ -0,0 +1,13 @@
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
|
||||
// C++ Standard Library Header Files used in your project
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <atomic>
|
||||
#include <thread>
|
||||
#include <mutex>
|
||||
#include <sstream>
|
||||
#include <array>
|
||||
#include <algorithm>
|
||||
#include <iostream>
|
||||
@@ -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
|
||||
@@ -0,0 +1,89 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" />
|
||||
<PropertyGroup Label="Globals">
|
||||
<VCProjectVersion>15.0</VCProjectVersion>
|
||||
<ProjectGuid>{D66DAAF0-84FC-477D-A980-43370F4499C2}</ProjectGuid>
|
||||
<Keyword>Win32Proj</Keyword>
|
||||
<RootNamespace>KeystrokeOverlay</RootNamespace>
|
||||
<ProjectName>KeystrokeOverlayModuleInterface</ProjectName>
|
||||
<WindowsTargetPlatformVersion>10.0.26100.0</WindowsTargetPlatformVersion>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Label="Configuration">
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Label="Configuration" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<ImportGroup Label="ExtensionSettings">
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="Shared">
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Label="UserMacros" />
|
||||
<PropertyGroup>
|
||||
<OutDir>..\..\..\..\$(Platform)\$(Configuration)\WinUI3Apps</OutDir>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<TargetName>PowerToys.KeystrokeOverlayModuleInterface</TargetName>
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup>
|
||||
<ClCompile>
|
||||
<PreprocessorDefinitions>KEYSTROKEOVERLAY_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<!-- Includes 'KeystrokeOverlayKeyboardService' so we can include "Batcher.h" in dllmain.cpp -->
|
||||
<AdditionalIncludeDirectories>..\KeystrokeOverlayKeyboardService;..\..\..\common\inc;..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<OutputFile>$(OutDir)$(TargetName)$(TargetExt)</OutputFile>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="pch.h" />
|
||||
<ClInclude Include="resource.h" />
|
||||
<ClInclude Include="trace.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="dllmain.cpp" />
|
||||
<ClCompile Include="pch.cpp">
|
||||
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="trace.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Image Include="..\KeystrokeOverlayXAML\Assets\Icon.ico" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<!-- Link dependencies -->
|
||||
<ProjectReference Include="..\..\..\common\logger\logger.vcxproj">
|
||||
<Project>{d9b8fc84-322a-4f9f-bbb9-20915c47ddfd}</Project>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\..\common\SettingsAPI\SettingsAPI.vcxproj">
|
||||
<Project>{6955446d-23f7-4023-9bb3-8657f904af99}</Project>
|
||||
</ProjectReference>
|
||||
<!-- Reference the Service Static Library created above -->
|
||||
<ProjectReference Include="..\KeystrokeOverlayKeyboardService\KeystrokeOverlayKeyboardService.vcxproj">
|
||||
<Project>{A1B2C3D4-E5F6-4321-8765-123456789ABC}</Project>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ResourceCompile Include="KeystrokeOverlay.rc" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<Import Project="..\..\..\..\deps\spdlog.props" />
|
||||
<ImportGroup Label="ExtensionTargets">
|
||||
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" />
|
||||
<Import Project="..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" />
|
||||
</ImportGroup>
|
||||
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
|
||||
<PropertyGroup>
|
||||
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
|
||||
</PropertyGroup>
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props'))" />
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets'))" />
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets'))" />
|
||||
</Target>
|
||||
</Project>
|
||||
@@ -0,0 +1,438 @@
|
||||
#include "pch.h"
|
||||
|
||||
#include <interface/powertoy_module_interface.h>
|
||||
#include <common/SettingsAPI/settings_helpers.h>
|
||||
#include <common/hooks/LowlevelKeyboardEvent.h>
|
||||
#include <common/hooks/WinHookEvent.h>
|
||||
#include <common/SettingsAPI/settings_objects.h>
|
||||
#include <common/utils/gpo.h>
|
||||
#include <common/logger/logger.h>
|
||||
#include <common/utils/process_path.h>
|
||||
#include <common/utils/logger_helper.h>
|
||||
|
||||
#include "trace.h"
|
||||
#include <iostream>
|
||||
|
||||
extern "C" IMAGE_DOS_HEADER __ImageBase;
|
||||
|
||||
BOOL APIENTRY DllMain(HMODULE /*hModule*/, DWORD ul_reason_for_call, LPVOID /*lpReserved*/)
|
||||
{
|
||||
switch (ul_reason_for_call)
|
||||
{
|
||||
case DLL_PROCESS_ATTACH:
|
||||
Trace::RegisterProvider();
|
||||
break;
|
||||
case DLL_THREAD_ATTACH:
|
||||
case DLL_THREAD_DETACH:
|
||||
break;
|
||||
case DLL_PROCESS_DETACH:
|
||||
Trace::UnregisterProvider();
|
||||
break;
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
// The PowerToy name that will be shown in the settings.
|
||||
const static wchar_t* MODULE_NAME = L"Keystroke Overlay";
|
||||
const static wchar_t* MODULE_KEY = L"KeystrokeOverlay";
|
||||
// Add a description that will we shown in the module settings page.
|
||||
const static wchar_t* MODULE_DESC = L"<no description>";
|
||||
|
||||
const static wchar_t* win_hook_event = L"win_hook_event";
|
||||
// Note: low-level keyboard hook and batching are handled by the external
|
||||
// KeystrokeServer.exe. The module no longer declares an internal event queue
|
||||
// or batcher to avoid duplicate IPC servers.
|
||||
|
||||
// These are the properties shown in the Settings page.
|
||||
struct ModuleSettings
|
||||
{
|
||||
// Add the PowerToy module properties with default values.
|
||||
|
||||
bool is_draggable = true;
|
||||
int overlay_timeout = 3000;
|
||||
int display_mode = 0; // NEW: 0=Standard, 1=Compact, etc. (Define your modes)
|
||||
|
||||
int text_size = 24;
|
||||
int text_opacity = 100;
|
||||
|
||||
int bg_opacity = 50;
|
||||
|
||||
std::wstring text_color = L"#FFFFFF";
|
||||
std::wstring bg_color = L"#000000";
|
||||
|
||||
PowerToysSettings::HotkeyObject switch_monitor_hotkey =
|
||||
PowerToysSettings::HotkeyObject::from_settings(true, false, false, true, 0xBF); // Default: Win+Ctrl+/
|
||||
|
||||
} g_settings;
|
||||
|
||||
class KeystrokeOverlay : public PowertoyModuleIface
|
||||
{
|
||||
std::wstring app_name;
|
||||
std::wstring app_key;
|
||||
|
||||
private:
|
||||
// The PowerToy state.
|
||||
bool m_enabled = false;
|
||||
HANDLE m_hProcess = nullptr;
|
||||
// Handle for the hidden keystroke server process started by this module
|
||||
HANDLE m_hServerProcess = nullptr;
|
||||
|
||||
// Load initial settings from the persisted values.
|
||||
void init_settings();
|
||||
|
||||
// Launch and terminate the Keystroke Overlay process.
|
||||
// Launch and terminate the Keystroke Overlay process.
|
||||
void launch_process()
|
||||
{
|
||||
Logger::trace(L"Launching Keystroke Overlay process");
|
||||
unsigned long powertoys_pid = GetCurrentProcessId();
|
||||
|
||||
//
|
||||
// Determine the folder where this module's DLL lives
|
||||
//
|
||||
WCHAR modulePath[MAX_PATH] = { 0 };
|
||||
if (GetModuleFileNameW(reinterpret_cast<HMODULE>(&__ImageBase), modulePath, ARRAYSIZE(modulePath)) == 0)
|
||||
{
|
||||
Logger::error(L"GetModuleFileNameW failed — cannot launch KeystrokeOverlay");
|
||||
return;
|
||||
}
|
||||
|
||||
std::wstring folder = modulePath;
|
||||
size_t pos = folder.find_last_of(L"\\/");
|
||||
if (pos != std::wstring::npos)
|
||||
folder.resize(pos + 1);
|
||||
|
||||
//
|
||||
// Build absolute path to KeystrokeOverlay.exe
|
||||
//
|
||||
std::wstring exePath = folder + L"PowerToys.KeystrokeOverlay.exe";
|
||||
std::wstring cmdLine = exePath + L" " + std::to_wstring(powertoys_pid);
|
||||
|
||||
Logger::trace(L"Launching KeystrokeOverlay UI from: " + exePath);
|
||||
|
||||
//
|
||||
// Launch the UI application
|
||||
//
|
||||
STARTUPINFOW si{};
|
||||
si.cb = sizeof(si);
|
||||
PROCESS_INFORMATION pi{};
|
||||
|
||||
if (!CreateProcessW(
|
||||
exePath.c_str(), // lpApplicationName
|
||||
&cmdLine[0], // lpCommandLine (modifiable buffer)
|
||||
nullptr, nullptr,
|
||||
FALSE, // inherit handles
|
||||
0, // flags
|
||||
nullptr, nullptr,
|
||||
&si, &pi))
|
||||
{
|
||||
DWORD err = GetLastError();
|
||||
Logger::error(L"KeystrokeOverlay.exe failed to start with error: " + std::to_wstring(err));
|
||||
return;
|
||||
}
|
||||
|
||||
m_hProcess = pi.hProcess;
|
||||
CloseHandle(pi.hThread);
|
||||
|
||||
Logger::trace(L"KeystrokeOverlay.exe started successfully");
|
||||
}
|
||||
|
||||
|
||||
void terminate_process()
|
||||
{
|
||||
if (m_hProcess)
|
||||
{
|
||||
Logger::trace(L"Terminating Keystroke Overlay process");
|
||||
|
||||
TerminateProcess(m_hProcess, 0);
|
||||
CloseHandle(m_hProcess);
|
||||
m_hProcess = nullptr;
|
||||
}
|
||||
if (m_hServerProcess)
|
||||
{
|
||||
Logger::trace(L"Terminating KeystrokeServer process");
|
||||
TerminateProcess(m_hServerProcess, 0);
|
||||
CloseHandle(m_hServerProcess);
|
||||
m_hServerProcess = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
// Constructor
|
||||
KeystrokeOverlay()
|
||||
{
|
||||
init_settings();
|
||||
app_name = MODULE_NAME;
|
||||
app_key = MODULE_KEY;
|
||||
LoggerHelpers::init_logger(app_key, L"ModuleInterface", "KeystrokeOverlay");
|
||||
Logger::info("Keystroke Overlay ModuleInterface object is constructing");
|
||||
};
|
||||
|
||||
// Destroy the powertoy and free memory
|
||||
virtual void destroy() override
|
||||
{
|
||||
delete this;
|
||||
}
|
||||
|
||||
// Return the display name of the powertoy, this will be cached by the runner
|
||||
virtual const wchar_t* get_name() override
|
||||
{
|
||||
return MODULE_NAME;
|
||||
}
|
||||
|
||||
virtual const wchar_t* get_key() override
|
||||
{
|
||||
return MODULE_KEY;
|
||||
}
|
||||
|
||||
// Return array of the names of all events that this powertoy listens for, with
|
||||
// nullptr as the last element of the array. We no longer request ll_keyboard
|
||||
// events from the runner because the external KeystrokeServer.exe owns the
|
||||
// low-level keyboard hook and IPC delivery.
|
||||
virtual const wchar_t** get_events()
|
||||
{
|
||||
static const wchar_t* events[] = { win_hook_event, nullptr };
|
||||
return events;
|
||||
}
|
||||
|
||||
// Return JSON with the configuration options.
|
||||
virtual bool get_config(wchar_t* buffer, int* buffer_size) override
|
||||
{
|
||||
HINSTANCE hinstance = reinterpret_cast<HINSTANCE>(&__ImageBase);
|
||||
|
||||
// Create a Settings object.
|
||||
PowerToysSettings::Settings settings(hinstance, get_name());
|
||||
settings.set_description(MODULE_DESC);
|
||||
|
||||
// Show an overview link in the Settings page
|
||||
// commented out for now
|
||||
//settings.set_overview_link(L"https://");
|
||||
|
||||
// Show a video link in the Settings page.
|
||||
//settings.set_video_link(L"https://");
|
||||
|
||||
// enable_draggable_overlay Toggle
|
||||
settings.add_bool_toggle(
|
||||
L"enable_draggable_overlay", // property name.
|
||||
L"Enable Draggable Overlay", // description or resource id of the localized string.
|
||||
g_settings.is_draggable // property value.
|
||||
);
|
||||
|
||||
settings.add_int_spinner(
|
||||
L"display_mode", // property name
|
||||
L"Display Mode", // description
|
||||
g_settings.display_mode, // current value
|
||||
0, // Min value (e.g., LastFiveKeystrokes)
|
||||
2, // Max value (e.g., ShortcutsOnly)
|
||||
1 // Step
|
||||
);
|
||||
|
||||
settings.add_hotkey(
|
||||
L"switch_monitor_hotkey",
|
||||
L"Move to Next Monitor",
|
||||
g_settings.switch_monitor_hotkey
|
||||
);
|
||||
|
||||
// Overlay Timeout Spinner
|
||||
settings.add_int_spinner(
|
||||
L"overlay_timeout", // property name
|
||||
L"Overlay Timeout (ms)", // description or resource id of the localized string.
|
||||
g_settings.overlay_timeout, // property value.
|
||||
500, // Min
|
||||
10000, // Max
|
||||
100 // Step
|
||||
);
|
||||
|
||||
// 3. Text Size Spinner
|
||||
settings.add_int_spinner(
|
||||
L"text_size", // property name
|
||||
L"Text Font Size", // description or resource id of the localized string.
|
||||
g_settings.text_size, // property value.
|
||||
10, // Min
|
||||
72, // Max
|
||||
2 // Step
|
||||
);
|
||||
|
||||
// 4. Text Opacity
|
||||
settings.add_int_spinner(
|
||||
L"text_opacity", // property name
|
||||
L"Text Opacity (%)", // description or resource id of the localized string.
|
||||
g_settings.text_opacity, // property value.
|
||||
0, // Min
|
||||
100, // Max
|
||||
5 // Step
|
||||
);
|
||||
|
||||
// 5. Background Opacity
|
||||
settings.add_int_spinner(
|
||||
L"background_opacity", // property name
|
||||
L"Background Opacity (%)", // description or resource id of the localized string.
|
||||
g_settings.bg_opacity, // property value.
|
||||
0, // Min
|
||||
100, // Max
|
||||
5 // Step
|
||||
);
|
||||
|
||||
// 6. Text Color
|
||||
settings.add_color_picker(
|
||||
L"text_color", // property name.
|
||||
L"Text Color", // description or resource id of the localized string.
|
||||
g_settings.text_color // property value.
|
||||
);
|
||||
|
||||
// 7. Background Color
|
||||
settings.add_color_picker(
|
||||
L"background_color", // property name.
|
||||
L"Background Color", // description or resource id of the localized string.
|
||||
g_settings.bg_color // property value.
|
||||
);
|
||||
|
||||
return settings.serialize_to_buffer(buffer, buffer_size);
|
||||
}
|
||||
|
||||
// Return the configured status for the gpo policy for the module
|
||||
virtual powertoys_gpo::gpo_rule_configured_t gpo_policy_enabled_configuration() override
|
||||
{
|
||||
return powertoys_gpo::getConfiguredKeystrokeOverlayEnabledValue();
|
||||
}
|
||||
|
||||
// Called by the runner to pass the updated settings values as a serialized JSON.
|
||||
virtual void set_config(const wchar_t* config) override
|
||||
{
|
||||
try
|
||||
{
|
||||
// Parse the input JSON string.
|
||||
PowerToysSettings::PowerToyValues values =
|
||||
PowerToysSettings::PowerToyValues::from_json_string(config, get_key());
|
||||
|
||||
if (auto v = values.get_bool_value(L"enable_draggable_overlay")) {
|
||||
g_settings.is_draggable = *v;
|
||||
}
|
||||
if (auto v = values.get_int_value(L"overlay_timeout")) {
|
||||
g_settings.overlay_timeout = *v;
|
||||
}
|
||||
if (auto v = values.get_int_value(L"text_size")) {
|
||||
g_settings.text_size = *v;
|
||||
}
|
||||
if (auto v = values.get_int_value(L"text_opacity")) {
|
||||
g_settings.text_opacity = *v;
|
||||
}
|
||||
if (auto v = values.get_int_value(L"background_opacity")) {
|
||||
g_settings.bg_opacity = *v;
|
||||
}
|
||||
if (auto v = values.get_string_value(L"text_color")) {
|
||||
g_settings.text_color = *v;
|
||||
}
|
||||
if (auto v = values.get_string_value(L"background_color")) {
|
||||
g_settings.bg_color = *v;
|
||||
}
|
||||
if (auto v = values.get_int_value(L"display_mode"))
|
||||
{
|
||||
g_settings.display_mode = *v;
|
||||
}
|
||||
if (auto v = values.get_json(L"switch_monitor_hotkey"))
|
||||
{
|
||||
g_settings.switch_monitor_hotkey = PowerToysSettings::HotkeyObject::from_json(*v);
|
||||
}
|
||||
|
||||
// Save to disk so the C# App can read the updated settings.json
|
||||
// If you don't need to do any custom processing of the settings, proceed
|
||||
// to persists the values calling:
|
||||
values.save_to_settings_file();
|
||||
|
||||
// Otherwise call a custom function to process the settings before saving them to disk:
|
||||
// save_settings();
|
||||
}
|
||||
catch (std::exception&)
|
||||
{
|
||||
// Improper JSON.
|
||||
}
|
||||
}
|
||||
|
||||
// Enable the powertoy
|
||||
|
||||
// where the keystrokeoverlay.exe is/should be launched
|
||||
virtual void enable()
|
||||
{
|
||||
launch_process();
|
||||
m_enabled = true;
|
||||
Trace::EnableKeystrokeOverlay(true);
|
||||
}
|
||||
|
||||
// Disable the powertoy
|
||||
virtual void disable()
|
||||
{
|
||||
terminate_process();
|
||||
m_enabled = false;
|
||||
// External server handles batching; nothing else to stop here.
|
||||
Trace::EnableKeystrokeOverlay(false);
|
||||
}
|
||||
|
||||
// Returns if the powertoys is enabled
|
||||
virtual bool is_enabled() override
|
||||
{
|
||||
return m_enabled;
|
||||
}
|
||||
|
||||
// Handle incoming event, data is event-specific
|
||||
// virtual intptr_t signal_event(const wchar_t* name, intptr_t data)
|
||||
// {
|
||||
// if (wcscmp(name, win_hook_event) == 0)
|
||||
// {
|
||||
// /* auto& event = *(reinterpret_cast<WinHookEvent*>(data)); */
|
||||
// // Return value is ignored
|
||||
// return 0;
|
||||
// }
|
||||
// return 0;
|
||||
// }
|
||||
};
|
||||
|
||||
// Load the settings file.
|
||||
void KeystrokeOverlay::init_settings()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Load and parse the settings file for this PowerToy.
|
||||
PowerToysSettings::PowerToyValues settings =
|
||||
PowerToysSettings::PowerToyValues::load_from_settings_file(KeystrokeOverlay::get_name());
|
||||
|
||||
if (auto v = settings.get_bool_value(L"enable_draggable_overlay")) {
|
||||
g_settings.is_draggable = *v;
|
||||
}
|
||||
if (auto v = settings.get_int_value(L"overlay_timeout")) {
|
||||
g_settings.overlay_timeout = *v;
|
||||
}
|
||||
if (auto v = settings.get_int_value(L"text_size")) {
|
||||
g_settings.text_size = *v;
|
||||
}
|
||||
if (auto v = settings.get_int_value(L"text_opacity")) {
|
||||
g_settings.text_opacity = *v;
|
||||
}
|
||||
if (auto v = settings.get_int_value(L"background_opacity")) {
|
||||
g_settings.bg_opacity = *v;
|
||||
}
|
||||
if (auto v = settings.get_string_value(L"text_color")) {
|
||||
g_settings.text_color = *v;
|
||||
}
|
||||
if (auto v = settings.get_string_value(L"background_color")) {
|
||||
g_settings.bg_color = *v;
|
||||
}
|
||||
if (auto v = settings.get_int_value(L"display_mode"))
|
||||
{
|
||||
g_settings.display_mode = *v;
|
||||
}
|
||||
if (auto v = settings.get_json(L"switch_monitor_hotkey"))
|
||||
{
|
||||
g_settings.switch_monitor_hotkey = PowerToysSettings::HotkeyObject::from_json(*v);
|
||||
}
|
||||
}
|
||||
catch (std::exception&)
|
||||
{
|
||||
// Error while loading from the settings file. Let default values stay as they are.
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()
|
||||
{
|
||||
return new KeystrokeOverlay();
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
#include "pch.h"
|
||||
#pragma comment(lib, "windowsapp")
|
||||
@@ -0,0 +1,6 @@
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
#include <winrt/Windows.Foundation.h>
|
||||
#include <winrt/Windows.Foundation.Collections.h>
|
||||
#include <shellapi.h>
|
||||
#include <Shlwapi.h>
|
||||
@@ -0,0 +1,31 @@
|
||||
#include "pch.h"
|
||||
#include "trace.h"
|
||||
|
||||
#include <common/Telemetry/TraceBase.h>
|
||||
|
||||
TRACELOGGING_DEFINE_PROVIDER(
|
||||
g_hProvider,
|
||||
"Microsoft.PowerToys",
|
||||
// {38e8889b-9731-53f5-e901-e8a7c1753074}
|
||||
(0x38e8889b, 0x9731, 0x53f5, 0xe9, 0x01, 0xe8, 0xa7, 0xc1, 0x75, 0x30, 0x74),
|
||||
TraceLoggingOptionProjectTelemetry());
|
||||
|
||||
void Trace::RegisterProvider()
|
||||
{
|
||||
TraceLoggingRegister(g_hProvider);
|
||||
}
|
||||
|
||||
void Trace::UnregisterProvider()
|
||||
{
|
||||
TraceLoggingUnregister(g_hProvider);
|
||||
}
|
||||
|
||||
void Trace::EnableKeystrokeOverlay(const bool enabled) noexcept
|
||||
{
|
||||
TraceLoggingWriteWrapper(
|
||||
g_hProvider,
|
||||
"KeystrokeOverlay_EnableKeystrokeOverlay",
|
||||
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
|
||||
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE),
|
||||
TraceLoggingBoolean(enabled, "Enabled"));
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include <common/Telemetry/TraceBase.h>
|
||||
|
||||
class Trace : public telemetry::TraceBase
|
||||
{
|
||||
public:
|
||||
static void RegisterProvider();
|
||||
static void UnregisterProvider();
|
||||
|
||||
// Log if the user has Keystroke Overlay enabled or disabled
|
||||
static void EnableKeystrokeOverlay(const bool enabled) noexcept;
|
||||
|
||||
};
|
||||
20
src/modules/KeystrokeOverlay/KeystrokeOverlayXAML/App.xaml
Normal file
20
src/modules/KeystrokeOverlay/KeystrokeOverlayXAML/App.xaml
Normal file
@@ -0,0 +1,20 @@
|
||||
<!-- Copyright (c) Microsoft Corporation and Contributors. -->
|
||||
<!-- Licensed under the MIT License. -->
|
||||
|
||||
<Application
|
||||
x:Class="KeystrokeOverlayUI.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="using:KeystrokeOverlayUI">
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
|
||||
<!-- Other merged dictionaries here -->
|
||||
<ResourceDictionary Source="/Controls/KeyVisual/KeyCharPresenter.xaml"/>
|
||||
<ResourceDictionary Source="/Controls/KeyVisual/KeyVisual.xaml"/>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
<!-- Other app resources here -->
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
100
src/modules/KeystrokeOverlay/KeystrokeOverlayXAML/App.xaml.cs
Normal file
100
src/modules/KeystrokeOverlay/KeystrokeOverlayXAML/App.xaml.cs
Normal file
@@ -0,0 +1,100 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using KeystrokeOverlayUI.Services;
|
||||
using ManagedCommon;
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
namespace KeystrokeOverlayUI
|
||||
{
|
||||
// Application entry point for the Keystroke Overlay UI.
|
||||
public sealed partial class App : Application, IDisposable
|
||||
{
|
||||
private readonly ProcessJob _job = new();
|
||||
private Window _window;
|
||||
private bool _disposed;
|
||||
|
||||
// Gets the running native keystroke server process.
|
||||
public Process KeystrokeServerProcess { get; private set; }
|
||||
|
||||
public App()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
string appLanguage = LanguageHelper.LoadLanguage();
|
||||
if (!string.IsNullOrEmpty(appLanguage))
|
||||
{
|
||||
Microsoft.Windows.Globalization.ApplicationLanguages.PrimaryLanguageOverride = appLanguage;
|
||||
}
|
||||
|
||||
Logger.InitializeLogger("\\KeystrokeOverlay\\Logs");
|
||||
|
||||
// Explicitly use Microsoft.UI.Xaml.UnhandledExceptionEventArgs
|
||||
UnhandledException += App_UnhandledException;
|
||||
}
|
||||
|
||||
protected override void OnLaunched(LaunchActivatedEventArgs args)
|
||||
{
|
||||
StartKeystrokeServer();
|
||||
|
||||
_window = new MainWindow();
|
||||
_window.Activate();
|
||||
}
|
||||
|
||||
private void StartKeystrokeServer()
|
||||
{
|
||||
try
|
||||
{
|
||||
string exePath = Path.Combine(
|
||||
AppDomain.CurrentDomain.BaseDirectory,
|
||||
"PowerToys.KeystrokeOverlayKeystrokeServer.exe");
|
||||
|
||||
if (!File.Exists(exePath))
|
||||
{
|
||||
Logger.LogError($"Keystroke server missing: {exePath}");
|
||||
return;
|
||||
}
|
||||
|
||||
KeystrokeServerProcess = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = exePath,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
},
|
||||
};
|
||||
|
||||
KeystrokeServerProcess.Start();
|
||||
|
||||
// Add process to job so it dies with UI
|
||||
_job.AddProcess(KeystrokeServerProcess.Handle);
|
||||
|
||||
Logger.LogInfo("Keystroke server started and assigned to job object.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to launch keystroke server.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
// Explicit namespace to resolve ambiguity
|
||||
private void App_UnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
|
||||
{
|
||||
Logger.LogError("Unhandled UI exception.", e.Exception);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
_job.Dispose();
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
@@ -0,0 +1,106 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ResourceDictionary
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="using:Microsoft.PowerToys.Settings.UI.Controls">
|
||||
|
||||
<Style BasedOn="{StaticResource DefaultKeyCharPresenterStyle}" TargetType="local:KeyCharPresenter" />
|
||||
|
||||
<Style x:Key="DefaultKeyCharPresenterStyle" TargetType="local:KeyCharPresenter">
|
||||
<Setter Property="FontWeight" Value="Normal" />
|
||||
<Setter Property="HorizontalAlignment" Value="Left" />
|
||||
<Setter Property="IsTabStop" Value="False" />
|
||||
<Setter Property="AutomationProperties.AccessibilityView" Value="Raw" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="local:KeyCharPresenter">
|
||||
<Grid Height="{TemplateBinding FontSize}">
|
||||
<TextBlock
|
||||
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||
FontFamily="{TemplateBinding FontFamily}"
|
||||
FontSize="{TemplateBinding FontSize}"
|
||||
FontWeight="{TemplateBinding FontWeight}"
|
||||
Text="{TemplateBinding Content}"
|
||||
TextLineBounds="Tight" />
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
<Style
|
||||
x:Key="WindowsKeyCharPresenterStyle"
|
||||
BasedOn="{StaticResource DefaultKeyCharPresenterStyle}"
|
||||
TargetType="local:KeyCharPresenter">
|
||||
<!-- Scale to visually align the height of the Windows logo and text -->
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="local:KeyCharPresenter">
|
||||
<Grid Height="{TemplateBinding FontSize}">
|
||||
<Viewbox>
|
||||
<PathIcon Data="M9 20H0V11H9V20ZM20 20H11V11H20V20ZM9 9H0V0H9V9ZM20 9H11V0H20V9Z" />
|
||||
</Viewbox>
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
<Style
|
||||
x:Key="OfficeKeyCharPresenterStyle"
|
||||
BasedOn="{StaticResource DefaultKeyCharPresenterStyle}"
|
||||
TargetType="local:KeyCharPresenter">
|
||||
<!-- Scale to visually align the height of the Office logo and text -->
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="local:KeyCharPresenter">
|
||||
<Grid Height="{TemplateBinding FontSize}">
|
||||
<Viewbox>
|
||||
<PathIcon Data="M1792 405v1238q0 33-10 62t-28 54-44 41-57 27l-555 159q-23 6-47 6-31 0-58-8t-53-24l-363-205q-20-11-31-29t-12-42q0-35 24-59t60-25h470V458L735 584q-43 15-69 53t-26 83v651q0 41-20 73t-55 53l-167 91q-23 12-46 12-40 0-68-28t-28-68V587q0-51 26-96t71-71L949 81q41-23 89-23 17 0 30 2t30 8l555 153q31 9 56 27t44 42 29 54 10 61zm-128 1238V405q0-22-13-38t-34-23l-273-75-64-18-64-18v1586l401-115q21-6 34-22t13-39z" />
|
||||
</Viewbox>
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
<Style
|
||||
x:Key="CopilotKeyCharPresenterStyle"
|
||||
BasedOn="{StaticResource DefaultKeyCharPresenterStyle}"
|
||||
TargetType="local:KeyCharPresenter">
|
||||
<!-- Scale to visually align the height of the Copilot logo and text -->
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="local:KeyCharPresenter">
|
||||
<Grid Height="{TemplateBinding FontSize}">
|
||||
<Viewbox>
|
||||
<PathIcon Data="M0 1213q0-60 10-124t27-130 35-129 38-121q18-55 41-119t54-129 70-125 87-106 106-74 129-28h661q59 0 114 17t96 64q30 34 46 72t33 81l22 58q11 29 34 52 23 25 56 31t65 9h4q157 0 238 83t82 240q0 60-10 125t-27 130-35 128-38 121q-18 55-41 119t-54 129-70 125-87 106-106 74-129 28H790q-61 0-107-15t-82-44-61-72-46-98q-11-29-24-60t-35-55q-23-25-51-31t-60-9h-4q-157 0-238-83T0 1213zm598-957q-50 0-93 25t-79 68-67 94-54 108-42 106-31 91q-17 51-35 110t-33 119-26 121-10 114q0 102 43 149t147 47h163q39 0 74-12t64-35 50-53 34-67q19-58 35-115t35-117q35-117 70-232t72-233q23-73 47-147t63-141H598zm452 285q69-29 143-29h281q-18-29-29-59t-21-58-21-54-30-44-46-30-69-11q-32 0-60 9t-48 35q-17 23-31 53t-27 63-23 65-19 60zm-296 867h101q39 0 74-12t66-34 52-52 33-68l58-191 42-140q21-70 43-140 11-36 28-69t43-62h-101q-39 0-74 12t-66 34-52 52-33 68q-15 48-29 96t-29 96q-21 70-41 140t-44 140q-11 36-28 68t-43 62zm814-768q-39 0-74 12t-64 35-50 53-34 68q-56 174-107 347t-106 349q-23 74-47 147t-63 141h427q50 0 93-25t79-68 67-94 54-108 42-106 31-91q16-51 34-110t34-119 26-121 10-114q0-102-43-149t-147-47h-162zm-570 867q-69 29-143 29H564q17 28 29 58t22 58 24 54 32 45 48 30 71 11q31 0 60-8t49-35q15-19 29-50t28-65 24-69 18-58z" />
|
||||
</Viewbox>
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
<Style
|
||||
x:Key="GlyphKeyCharPresenterStyle"
|
||||
BasedOn="{StaticResource DefaultKeyCharPresenterStyle}"
|
||||
TargetType="local:KeyCharPresenter">
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="local:KeyCharPresenter">
|
||||
<Grid>
|
||||
<Viewbox>
|
||||
<FontIcon
|
||||
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||
FontSize="{TemplateBinding FontSize}"
|
||||
FontWeight="{TemplateBinding FontWeight}"
|
||||
Glyph="{TemplateBinding Content}" />
|
||||
</Viewbox>
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
@@ -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)));
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
<ResourceDictionary
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="using:Microsoft.PowerToys.Settings.UI.Controls">
|
||||
|
||||
<Style x:Key="DefaultKeyVisualStyle" TargetType="local:KeyVisual">
|
||||
<Setter Property="MinWidth" Value="16" />
|
||||
<Setter Property="AutomationProperties.AccessibilityView" Value="Raw" />
|
||||
<Setter Property="IsTabStop" Value="False" />
|
||||
<Setter Property="MinHeight" Value="16" />
|
||||
<Setter Property="Background" Value="{ThemeResource CardBackgroundFillColorSecondaryBrush}" />
|
||||
<Setter Property="Foreground" Value="{ThemeResource TextFillColorPrimaryBrush}" />
|
||||
<Setter Property="BorderBrush" Value="{ThemeResource CardStrokeColorDefaultBrush}" />
|
||||
<Setter Property="BorderThickness" Value="{ThemeResource ButtonBorderThemeThickness}" />
|
||||
<Setter Property="Padding" Value="4,4,4,4" />
|
||||
<Setter Property="FontWeight" Value="Normal" />
|
||||
<Setter Property="FontSize" Value="14" />
|
||||
<Setter Property="CornerRadius" Value="2" />
|
||||
<Setter Property="BackgroundSizing" Value="InnerBorderEdge" />
|
||||
<Setter Property="HorizontalAlignment" Value="Left" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="local:KeyVisual">
|
||||
<Grid
|
||||
x:Name="KeyHolder"
|
||||
MinWidth="{TemplateBinding MinWidth}"
|
||||
MinHeight="{TemplateBinding MinHeight}"
|
||||
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalAlignment}"
|
||||
Background="{TemplateBinding Background}"
|
||||
BackgroundSizing="{TemplateBinding BackgroundSizing}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}">
|
||||
<Grid.BackgroundTransition>
|
||||
<BrushTransition Duration="0:0:0.083" />
|
||||
</Grid.BackgroundTransition>
|
||||
<local:KeyCharPresenter
|
||||
x:Name="KeyPresenter"
|
||||
Margin="{TemplateBinding Padding}"
|
||||
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
Content="{TemplateBinding Content}"
|
||||
FontSize="{TemplateBinding FontSize}"
|
||||
FontWeight="{TemplateBinding FontWeight}"
|
||||
Foreground="{TemplateBinding Foreground}" />
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="Disabled">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="KeyHolder.Background" Value="{ThemeResource SubtleFillColorTransparentBrush}" />
|
||||
<Setter Target="KeyHolder.BorderBrush" Value="{ThemeResource CardStrokeColorDefaultSolidBrush}" />
|
||||
<Setter Target="KeyPresenter.Foreground" Value="{ThemeResource ControlStrokeColorDefaultBrush}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="Invalid">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="KeyHolder.Background" Value="{ThemeResource SystemFillColorCriticalBackgroundBrush}" />
|
||||
<Setter Target="KeyHolder.BorderBrush" Value="{ThemeResource SystemFillColorCriticalBrush}" />
|
||||
<Setter Target="KeyHolder.BorderThickness" Value="1" />
|
||||
<Setter Target="KeyPresenter.Foreground" Value="{ThemeResource SystemFillColorCriticalBrush}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="Warning">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="KeyHolder.Background" Value="{ThemeResource SystemFillColorCautionBackgroundBrush}" />
|
||||
<Setter Target="KeyHolder.BorderBrush" Value="{ThemeResource SystemFillColorCautionBrush}" />
|
||||
<Setter Target="KeyHolder.BorderThickness" Value="1" />
|
||||
<Setter Target="KeyPresenter.Foreground" Value="{ThemeResource SystemFillColorCautionBrush}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style
|
||||
x:Key="SubtleKeyVisualStyle"
|
||||
BasedOn="{StaticResource DefaultKeyVisualStyle}"
|
||||
TargetType="local:KeyVisual">
|
||||
<Setter Property="Background" Value="{ThemeResource SubtleFillColorTransparentBrush}" />
|
||||
<Setter Property="BorderBrush" Value="{ThemeResource SubtleFillColorTransparentBrush}" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="local:KeyVisual">
|
||||
<Grid
|
||||
x:Name="KeyHolder"
|
||||
MinWidth="{TemplateBinding MinWidth}"
|
||||
MinHeight="{TemplateBinding MinHeight}"
|
||||
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalAlignment}"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}">
|
||||
<Grid.BackgroundTransition>
|
||||
<BrushTransition Duration="0:0:0.083" />
|
||||
</Grid.BackgroundTransition>
|
||||
<local:KeyCharPresenter
|
||||
x:Name="KeyPresenter"
|
||||
Margin="{TemplateBinding Padding}"
|
||||
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
Content="{TemplateBinding Content}"
|
||||
FontSize="{TemplateBinding FontSize}"
|
||||
FontWeight="{TemplateBinding FontWeight}"
|
||||
Foreground="{TemplateBinding Foreground}" />
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="Disabled">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="KeyPresenter.Foreground" Value="{ThemeResource TextFillColorDisabledBrush}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="Invalid">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="KeyPresenter.Foreground" Value="{ThemeResource SystemFillColorCriticalBrush}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="Warning">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="KeyPresenter.Foreground" Value="{ThemeResource SystemFillColorCautionBrush}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style
|
||||
x:Key="AccentKeyVisualStyle"
|
||||
BasedOn="{StaticResource DefaultKeyVisualStyle}"
|
||||
TargetType="local:KeyVisual">
|
||||
<Setter Property="Background" Value="{ThemeResource AccentFillColorDefaultBrush}" />
|
||||
<Setter Property="Foreground" Value="{ThemeResource TextOnAccentFillColorPrimaryBrush}" />
|
||||
<Setter Property="BorderBrush" Value="{ThemeResource AccentControlElevationBorderBrush}" />
|
||||
<Setter Property="BackgroundSizing" Value="OuterBorderEdge" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="local:KeyVisual">
|
||||
<Grid
|
||||
x:Name="KeyHolder"
|
||||
MinWidth="{TemplateBinding MinWidth}"
|
||||
MinHeight="{TemplateBinding MinHeight}"
|
||||
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalAlignment}"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
Background="{TemplateBinding Background}"
|
||||
BackgroundSizing="{TemplateBinding BackgroundSizing}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}">
|
||||
<Grid.BackgroundTransition>
|
||||
<BrushTransition Duration="0:0:0.083" />
|
||||
</Grid.BackgroundTransition>
|
||||
<local:KeyCharPresenter
|
||||
x:Name="KeyPresenter"
|
||||
Margin="{TemplateBinding Padding}"
|
||||
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||
Content="{TemplateBinding Content}"
|
||||
FontSize="{TemplateBinding FontSize}"
|
||||
FontWeight="{TemplateBinding FontWeight}"
|
||||
Foreground="{TemplateBinding Foreground}" />
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="Disabled">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="KeyHolder.Background" Value="{ThemeResource AccentButtonBackgroundDisabled}" />
|
||||
<Setter Target="KeyHolder.BorderBrush" Value="{ThemeResource AccentButtonBorderBrushDisabled}" />
|
||||
<Setter Target="KeyPresenter.Foreground" Value="{ThemeResource AccentButtonForegroundDisabled}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="Invalid">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="KeyHolder.Background" Value="{ThemeResource SystemFillColorCriticalBackgroundBrush}" />
|
||||
<Setter Target="KeyHolder.BorderBrush" Value="{ThemeResource SystemFillColorCriticalBrush}" />
|
||||
<Setter Target="KeyHolder.BorderThickness" Value="1" />
|
||||
<Setter Target="KeyPresenter.Foreground" Value="{ThemeResource SystemFillColorCriticalBrush}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="Warning">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="KeyHolder.Background" Value="{ThemeResource SystemFillColorCautionBackgroundBrush}" />
|
||||
<Setter Target="KeyHolder.BorderBrush" Value="{ThemeResource SystemFillColorCautionBrush}" />
|
||||
<Setter Target="KeyHolder.BorderThickness" Value="1" />
|
||||
<Setter Target="KeyPresenter.Foreground" Value="{ThemeResource SystemFillColorCautionBrush}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style BasedOn="{StaticResource DefaultKeyVisualStyle}" TargetType="local:KeyVisual" />
|
||||
</ResourceDictionary>
|
||||
@@ -0,0 +1,193 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Windows.System;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.Controls
|
||||
{
|
||||
[TemplatePart(Name = KeyPresenter, Type = typeof(KeyCharPresenter))]
|
||||
[TemplateVisualState(Name = NormalState, GroupName = "CommonStates")]
|
||||
[TemplateVisualState(Name = DisabledState, GroupName = "CommonStates")]
|
||||
[TemplateVisualState(Name = InvalidState, GroupName = "CommonStates")]
|
||||
[TemplateVisualState(Name = WarningState, GroupName = "CommonStates")]
|
||||
public sealed partial class KeyVisual : Control
|
||||
{
|
||||
private const string KeyPresenter = "KeyPresenter";
|
||||
private const string NormalState = "Normal";
|
||||
private const string DisabledState = "Disabled";
|
||||
private const string InvalidState = "Invalid";
|
||||
private const string WarningState = "Warning";
|
||||
private KeyCharPresenter _keyPresenter;
|
||||
|
||||
public object Content
|
||||
{
|
||||
get => (object)GetValue(ContentProperty);
|
||||
set => SetValue(ContentProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty ContentProperty = DependencyProperty.Register(nameof(Content), typeof(object), typeof(KeyVisual), new PropertyMetadata(default(string), OnContentChanged));
|
||||
|
||||
public State State
|
||||
{
|
||||
get => (State)GetValue(StateProperty);
|
||||
set => SetValue(StateProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty StateProperty = DependencyProperty.Register(nameof(State), typeof(State), typeof(KeyVisual), new PropertyMetadata(State.Normal, OnStateChanged));
|
||||
|
||||
public bool RenderKeyAsGlyph
|
||||
{
|
||||
get => (bool)GetValue(RenderKeyAsGlyphProperty);
|
||||
set => SetValue(RenderKeyAsGlyphProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty RenderKeyAsGlyphProperty = DependencyProperty.Register(nameof(RenderKeyAsGlyph), typeof(bool), typeof(KeyVisual), new PropertyMetadata(false, OnContentChanged));
|
||||
|
||||
public KeyVisual()
|
||||
{
|
||||
this.DefaultStyleKey = typeof(KeyVisual);
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate()
|
||||
{
|
||||
IsEnabledChanged -= KeyVisual_IsEnabledChanged;
|
||||
_keyPresenter = (KeyCharPresenter)this.GetTemplateChild(KeyPresenter);
|
||||
Update();
|
||||
SetVisualStates();
|
||||
IsEnabledChanged += KeyVisual_IsEnabledChanged;
|
||||
base.OnApplyTemplate();
|
||||
}
|
||||
|
||||
private static void OnContentChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
((KeyVisual)d).SetVisualStates();
|
||||
}
|
||||
|
||||
private static void OnStateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
((KeyVisual)d).SetVisualStates();
|
||||
}
|
||||
|
||||
private void SetVisualStates()
|
||||
{
|
||||
if (this != null)
|
||||
{
|
||||
if (State == State.Error)
|
||||
{
|
||||
VisualStateManager.GoToState(this, InvalidState, true);
|
||||
}
|
||||
else if (State == State.Warning)
|
||||
{
|
||||
VisualStateManager.GoToState(this, WarningState, true);
|
||||
}
|
||||
else if (!IsEnabled)
|
||||
{
|
||||
VisualStateManager.GoToState(this, DisabledState, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
VisualStateManager.GoToState(this, NormalState, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (Content == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Content is string key)
|
||||
{
|
||||
switch (key)
|
||||
{
|
||||
case "Copilot":
|
||||
_keyPresenter.Style = (Style)Application.Current.Resources["CopilotKeyCharPresenterStyle"];
|
||||
break;
|
||||
|
||||
case "Office":
|
||||
_keyPresenter.Style = (Style)Application.Current.Resources["OfficeKeyCharPresenterStyle"];
|
||||
break;
|
||||
|
||||
default:
|
||||
_keyPresenter.Style = (Style)Application.Current.Resources["DefaultKeyCharPresenterStyle"];
|
||||
break;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (Content is int keyCode)
|
||||
{
|
||||
VirtualKey virtualKey = (VirtualKey)keyCode;
|
||||
switch (virtualKey)
|
||||
{
|
||||
case VirtualKey.Enter:
|
||||
SetGlyphOrText("\uE751", virtualKey);
|
||||
break;
|
||||
|
||||
case VirtualKey.Back:
|
||||
SetGlyphOrText("\uE750", virtualKey);
|
||||
break;
|
||||
|
||||
case VirtualKey.Shift:
|
||||
case (VirtualKey)160: // Left Shift
|
||||
case (VirtualKey)161: // Right Shift
|
||||
SetGlyphOrText("\uE752", virtualKey);
|
||||
break;
|
||||
|
||||
case VirtualKey.Up:
|
||||
_keyPresenter.Content = "\uE0E4";
|
||||
break;
|
||||
|
||||
case VirtualKey.Down:
|
||||
_keyPresenter.Content = "\uE0E5";
|
||||
break;
|
||||
|
||||
case VirtualKey.Left:
|
||||
_keyPresenter.Content = "\uE0E2";
|
||||
break;
|
||||
|
||||
case VirtualKey.Right:
|
||||
_keyPresenter.Content = "\uE0E3";
|
||||
break;
|
||||
|
||||
case VirtualKey.LeftWindows:
|
||||
case VirtualKey.RightWindows:
|
||||
_keyPresenter.Style = (Style)Application.Current.Resources["WindowsKeyCharPresenterStyle"];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SetGlyphOrText(string glyph, VirtualKey key)
|
||||
{
|
||||
if (RenderKeyAsGlyph)
|
||||
{
|
||||
_keyPresenter.Content = glyph;
|
||||
_keyPresenter.Style = (Style)Application.Current.Resources["GlyphKeyCharPresenterStyle"];
|
||||
}
|
||||
else
|
||||
{
|
||||
_keyPresenter.Content = key.ToString();
|
||||
_keyPresenter.Style = (Style)Application.Current.Resources["DefaultKeyCharPresenterStyle"];
|
||||
}
|
||||
}
|
||||
|
||||
private void KeyVisual_IsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
SetVisualStates();
|
||||
}
|
||||
}
|
||||
|
||||
public enum State
|
||||
{
|
||||
Normal,
|
||||
Error,
|
||||
Warning,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using KeystrokeOverlayUI.Models;
|
||||
using Microsoft.UI;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Windows.UI;
|
||||
|
||||
namespace KeystrokeOverlayUI.Controls
|
||||
{
|
||||
public partial class KeyVisualItem : ObservableObject
|
||||
{
|
||||
[ObservableProperty]
|
||||
private string _text;
|
||||
|
||||
[ObservableProperty]
|
||||
private int _textSize;
|
||||
|
||||
[ObservableProperty]
|
||||
private double _opacity = 1.0;
|
||||
|
||||
public bool IsExiting { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace KeystrokeOverlayUI.Controls
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct KeystrokeEvent
|
||||
{
|
||||
public uint VirtualKey;
|
||||
public bool IsPressed;
|
||||
public List<string> Modifiers;
|
||||
public string Text;
|
||||
public string EventType;
|
||||
|
||||
// Convert the KeystrokeEvent to a human-readable string for display
|
||||
public override string ToString()
|
||||
{
|
||||
if (!IsPressed)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
bool isCharEvent = string.Equals(EventType, "char", StringComparison.OrdinalIgnoreCase);
|
||||
string keyName = null;
|
||||
|
||||
// check for modifiers
|
||||
bool hasCtrl = Modifiers != null && Modifiers.Contains("Ctrl");
|
||||
bool hasAlt = Modifiers != null && Modifiers.Contains("Alt");
|
||||
bool hasWin = Modifiers != null && Modifiers.Contains("Win");
|
||||
|
||||
if (isCharEvent && !hasWin)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Text))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(Text))
|
||||
{
|
||||
keyName = Text;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (IsCommandKey(VirtualKey) || hasCtrl || hasAlt || hasWin)
|
||||
{
|
||||
keyName = GetKeyName(VirtualKey);
|
||||
}
|
||||
}
|
||||
|
||||
// only register valid key combinations
|
||||
if (keyName != null)
|
||||
{
|
||||
var displayParts = new List<string>();
|
||||
|
||||
if (Modifiers != null)
|
||||
{
|
||||
foreach (var mod in Modifiers)
|
||||
{
|
||||
// Don't show "Shift" if we are displaying a Char
|
||||
// (because the Char "!" already implies Shift was pressed)
|
||||
if (isCharEvent && !hasWin && mod == "Shift" && !hasCtrl && !hasAlt)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string symbol = GetModifierSymbol(mod);
|
||||
|
||||
if (!displayParts.Contains(symbol))
|
||||
{
|
||||
displayParts.Add(symbol);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Avoid duplicates (e.g. Ctrl + Ctrl)
|
||||
string modSym = GetModifierSymbol(keyName);
|
||||
if (!displayParts.Contains(keyName) && !displayParts.Contains(modSym))
|
||||
{
|
||||
displayParts.Add(keyName);
|
||||
}
|
||||
|
||||
return string.Join(" + ", displayParts);
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public bool IsShortcut
|
||||
{
|
||||
get
|
||||
{
|
||||
// 1. If it has modifiers (Ctrl, Alt, Win), it is definitely a shortcut
|
||||
if (Modifiers != null && Modifiers.Count > 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// 2. If it is a "Command Key" (Enter, Esc, F1), treat it as a shortcut
|
||||
if (IsCommandKey(VirtualKey))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// 3. Otherwise, it is just a standard character (A, 1, !)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsCommandKey(uint virtualKey)
|
||||
{
|
||||
var key = (Windows.System.VirtualKey)virtualKey;
|
||||
|
||||
switch (key)
|
||||
{
|
||||
case Windows.System.VirtualKey.Space:
|
||||
case Windows.System.VirtualKey.Enter:
|
||||
case Windows.System.VirtualKey.Tab:
|
||||
case Windows.System.VirtualKey.Back:
|
||||
case Windows.System.VirtualKey.Escape:
|
||||
case Windows.System.VirtualKey.Delete:
|
||||
case Windows.System.VirtualKey.Insert:
|
||||
case Windows.System.VirtualKey.Home:
|
||||
case Windows.System.VirtualKey.End:
|
||||
case Windows.System.VirtualKey.PageUp:
|
||||
case Windows.System.VirtualKey.PageDown:
|
||||
case Windows.System.VirtualKey.Left:
|
||||
case Windows.System.VirtualKey.Right:
|
||||
case Windows.System.VirtualKey.Up:
|
||||
case Windows.System.VirtualKey.Down:
|
||||
case Windows.System.VirtualKey.Snapshot: // Print Screen
|
||||
case Windows.System.VirtualKey.Pause:
|
||||
case Windows.System.VirtualKey.CapitalLock:
|
||||
case Windows.System.VirtualKey.LeftWindows:
|
||||
case Windows.System.VirtualKey.RightWindows:
|
||||
return true;
|
||||
}
|
||||
|
||||
if (virtualKey >= 112 && virtualKey <= 135)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// BLOCK everything else (A-Z, 0-9, Punctuation)
|
||||
// These will be handled by the "Char" event instead
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string GetModifierSymbol(string modifier)
|
||||
{
|
||||
return modifier switch
|
||||
{
|
||||
"Ctrl" => "Ctrl",
|
||||
"Alt" => "Alt",
|
||||
"Shift" => "⇧",
|
||||
"Win" => "⊞",
|
||||
_ => modifier,
|
||||
};
|
||||
}
|
||||
|
||||
private static string GetKeyName(uint virtualKey)
|
||||
{
|
||||
var key = (Windows.System.VirtualKey)virtualKey;
|
||||
int intKey = (int)virtualKey;
|
||||
|
||||
switch (key)
|
||||
{
|
||||
case Windows.System.VirtualKey.LeftShift:
|
||||
case Windows.System.VirtualKey.RightShift: return "⇧";
|
||||
case Windows.System.VirtualKey.Control:
|
||||
case Windows.System.VirtualKey.LeftControl:
|
||||
case Windows.System.VirtualKey.RightControl: return "Ctrl";
|
||||
case Windows.System.VirtualKey.Menu:
|
||||
case Windows.System.VirtualKey.LeftMenu:
|
||||
case Windows.System.VirtualKey.RightMenu: return "Alt";
|
||||
case Windows.System.VirtualKey.LeftWindows:
|
||||
case Windows.System.VirtualKey.RightWindows: return "⊞";
|
||||
|
||||
case Windows.System.VirtualKey.Space: return "Space";
|
||||
case Windows.System.VirtualKey.Enter: return "Enter";
|
||||
case Windows.System.VirtualKey.Back: return "Backspace";
|
||||
case Windows.System.VirtualKey.Tab: return "Tab";
|
||||
case Windows.System.VirtualKey.Escape: return "Esc";
|
||||
case Windows.System.VirtualKey.Delete: return "Del";
|
||||
case Windows.System.VirtualKey.Insert: return "Ins";
|
||||
case Windows.System.VirtualKey.Left: return "←";
|
||||
case Windows.System.VirtualKey.Right: return "→";
|
||||
case Windows.System.VirtualKey.Up: return "↑";
|
||||
case Windows.System.VirtualKey.Down: return "↓";
|
||||
}
|
||||
|
||||
// Handle Letters (A-Z)
|
||||
if (key >= Windows.System.VirtualKey.A && key <= Windows.System.VirtualKey.Z)
|
||||
{
|
||||
return key.ToString();
|
||||
}
|
||||
|
||||
// Handle Numbers (0-9) - strip the word "Number"
|
||||
if (key >= Windows.System.VirtualKey.Number0 && key <= Windows.System.VirtualKey.Number9)
|
||||
{
|
||||
return key.ToString().Replace("Number", string.Empty);
|
||||
}
|
||||
|
||||
// Handle Numpad
|
||||
if (key >= Windows.System.VirtualKey.NumberPad0 && key <= Windows.System.VirtualKey.NumberPad9)
|
||||
{
|
||||
return "Num " + key.ToString().Replace("NumberPad", string.Empty);
|
||||
}
|
||||
|
||||
// Handle Punctuation (The "Numbers" you are seeing)
|
||||
switch (intKey)
|
||||
{
|
||||
case 186: return ";";
|
||||
case 187: return "=";
|
||||
case 188: return ",";
|
||||
case 189: return "-";
|
||||
case 190: return ".";
|
||||
case 191: return "/"; // Forward Slash
|
||||
case 192: return "`"; // Backtick
|
||||
case 219: return "[";
|
||||
case 220: return "\\"; // Backslash
|
||||
case 221: return "]";
|
||||
case 222: return "'"; // Single Quote
|
||||
case 173: return "Mute";
|
||||
case 174: return "Vol -";
|
||||
case 175: return "Vol +";
|
||||
case 176: return "Next";
|
||||
case 177: return "Prev";
|
||||
case 179: return "Play/Pause";
|
||||
}
|
||||
|
||||
return key.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.UI;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Windows.UI;
|
||||
|
||||
namespace KeystrokeOverlayUI.Helpers
|
||||
{
|
||||
public static class CustomColorHelper
|
||||
{
|
||||
public static SolidColorBrush GetBrushFromHex(string hex)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(hex))
|
||||
{
|
||||
return new SolidColorBrush(Colors.Transparent);
|
||||
}
|
||||
|
||||
// Handles #RRGGBB or #AARRGGBB
|
||||
hex = hex.Replace("#", string.Empty);
|
||||
byte a = 255;
|
||||
byte r = 0, g = 0, b = 0;
|
||||
|
||||
var provider = CultureInfo.InvariantCulture;
|
||||
|
||||
if (hex.Length == 6)
|
||||
{
|
||||
r = byte.Parse(hex.AsSpan(0, 2), NumberStyles.HexNumber, provider);
|
||||
g = byte.Parse(hex.AsSpan(2, 2), NumberStyles.HexNumber, provider);
|
||||
b = byte.Parse(hex.AsSpan(4, 2), NumberStyles.HexNumber, provider);
|
||||
}
|
||||
else if (hex.Length == 8)
|
||||
{
|
||||
a = byte.Parse(hex.AsSpan(0, 2), NumberStyles.HexNumber, provider);
|
||||
r = byte.Parse(hex.AsSpan(2, 2), NumberStyles.HexNumber, provider);
|
||||
g = byte.Parse(hex.AsSpan(4, 2), NumberStyles.HexNumber, provider);
|
||||
b = byte.Parse(hex.AsSpan(6, 2), NumberStyles.HexNumber, provider);
|
||||
}
|
||||
|
||||
return new SolidColorBrush(Color.FromArgb(a, r, g, b));
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Error fallback
|
||||
return new SolidColorBrush(Colors.Magenta);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace KeystrokeOverlayUI.Helpers
|
||||
{
|
||||
public static class NativeWindowHelper
|
||||
{
|
||||
// ---------------------------------------------------------
|
||||
// Public Structs
|
||||
// ---------------------------------------------------------
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct POINT
|
||||
{
|
||||
public int X;
|
||||
public int Y;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// Public Methods
|
||||
// ---------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the current cursor position in screen coordinates.
|
||||
/// </summary>
|
||||
public static POINT GetCursorPosition()
|
||||
{
|
||||
GetCursorPos(out POINT lpPoint);
|
||||
return lpPoint;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the window style to be "ToolWindow" (hidden from Alt-Tab) and
|
||||
/// "NoActivate" (doesn't steal focus).
|
||||
/// </summary>
|
||||
public static void SetOverlayWindowStyles(IntPtr hWnd)
|
||||
{
|
||||
int exStyle = GetWindowLong(hWnd, GwlExStyle);
|
||||
_ = SetWindowLong(hWnd, GwlExStyle, exStyle | WsExNoActivate | WsExToolWindow);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Forces the window to stay on top of all other windows without stealing focus.
|
||||
/// </summary>
|
||||
public static void EnforceTopMost(IntPtr hWnd)
|
||||
{
|
||||
// SwpNoActivate is critical to ensure we don't steal focus while the user types
|
||||
SetWindowPos(hWnd, HwndTopmost, 0, 0, 0, 0, SwpNoMove | SwpNoSize | SwpShowWindow | SwpNoActivate);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies Windows 11 rounded corners preference to the window.
|
||||
/// </summary>
|
||||
public static void SetRoundedCorners(IntPtr hWnd)
|
||||
{
|
||||
int cornerPreference = DwmwcpRound;
|
||||
_ = DwmSetWindowAttribute(hWnd, DwmwaWindowCornerPreference, ref cornerPreference, sizeof(int));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// Native Constants & Private Imports
|
||||
// ---------------------------------------------------------
|
||||
|
||||
// Window Pos Flags
|
||||
private static readonly IntPtr HwndTopmost = new IntPtr(-1);
|
||||
private const uint SwpNoSize = 0x0001;
|
||||
private const uint SwpNoMove = 0x0002;
|
||||
private const uint SwpNoActivate = 0x0010;
|
||||
private const uint SwpShowWindow = 0x0040;
|
||||
|
||||
// Window Styles
|
||||
private const int GwlExStyle = -20;
|
||||
private const int WsExNoActivate = 0x08000000;
|
||||
private const int WsExToolWindow = 0x00000080;
|
||||
|
||||
// DWM Constants
|
||||
private const int DwmwaWindowCornerPreference = 33;
|
||||
private const int DwmwcpRound = 2;
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool GetCursorPos(out POINT lpPoint);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int x, int y, int cx, int cy, uint uFlags);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern int GetWindowLong(IntPtr hWnd, int nIndex);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
|
||||
|
||||
[DllImport("dwmapi.dll")]
|
||||
private static extern int DwmSetWindowAttribute(IntPtr hwnd, int attribute, ref int value, int size);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<!-- Standard PowerToys Imports -->
|
||||
<Import Project="..\..\..\Common.Dotnet.CsWinRT.props" />
|
||||
<Import Project="..\..\..\Common.SelfContained.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
|
||||
<!-- IMPORTANT: Rename UI EXE so that Runner can launch it -->
|
||||
<AssemblyName>PowerToys.KeystrokeOverlay</AssemblyName>
|
||||
<RootNamespace>PowerToys.KeystrokeOverlayUI</RootNamespace>
|
||||
|
||||
<!-- Output folder must match where Runner searches -->
|
||||
<OutputPath>..\..\..\..\$(Platform)\$(Configuration)\WinUI3Apps\</OutputPath>
|
||||
|
||||
<UseWinUI>true</UseWinUI>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
|
||||
<!-- PowerToys WinAppSDK settings (must stay enabled) -->
|
||||
<WindowsPackageType>None</WindowsPackageType>
|
||||
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
|
||||
<SelfContained>true</SelfContained>
|
||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||
|
||||
<!-- Platform settings -->
|
||||
<Platforms>x64;ARM64</Platforms>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<RuntimeHostConfigurationOption Include="MVVMTOOLKIT_ENABLE_INOTIFYPROPERTYCHANGING_SUPPORT" Value="true" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- DO NOT specify version numbers — PowerToys manages versions repo-wide -->
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" />
|
||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" />
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" />
|
||||
<PackageReference Include="System.Text.Json" />
|
||||
<Manifest Include="$(ApplicationManifest)" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Required PowerToys common library references -->
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\common\Common.UI\Common.UI.csproj" />
|
||||
<ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
|
||||
<ProjectReference Include="..\..\..\common\LanguageModelProvider\LanguageModelProvider.csproj" />
|
||||
<ProjectReference Include="..\..\..\common\GPOWrapper\GPOWrapper.vcxproj" />
|
||||
<ProjectReference Include="..\..\..\common\interop\PowerToys.Interop.vcxproj" />
|
||||
<ProjectReference Include="..\..\..\settings-ui/Settings.UI.Library/Settings.UI.Library.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Ensure WinUI pages compile -->
|
||||
<ItemGroup>
|
||||
<Page Update="Controls\KeyVisual\KeyVisual.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Publish Properties (required for flattening like other PT modules) -->
|
||||
<PropertyGroup>
|
||||
<PublishReadyToRun Condition="'$(Configuration)' == 'Debug'">False</PublishReadyToRun>
|
||||
<PublishReadyToRun Condition="'$(Configuration)' != 'Debug'">True</PublishReadyToRun>
|
||||
<PublishTrimmed Condition="'$(Configuration)' == 'Debug'">False</PublishTrimmed>
|
||||
<PublishTrimmed Condition="'$(Configuration)' != 'Debug'">True</PublishTrimmed>
|
||||
<ApplicationIcon>Assets\KeystrokeOverlay\Icon.ico</ApplicationIcon>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,335 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using KeystrokeOverlayUI.Controls;
|
||||
using KeystrokeOverlayUI.Helpers;
|
||||
using KeystrokeOverlayUI.Models;
|
||||
using KeystrokeOverlayUI.Services;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.UI;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Windows.UI;
|
||||
|
||||
namespace KeystrokeOverlayUI
|
||||
{
|
||||
public partial class MainViewModel : ObservableObject
|
||||
{
|
||||
// Changed from string to KeyVisualItem to support individual properties
|
||||
public ObservableCollection<KeyVisualItem> PressedKeys { get; } = new();
|
||||
|
||||
[ObservableProperty]
|
||||
private int _timeoutMs = 3000;
|
||||
|
||||
[ObservableProperty]
|
||||
private int _textSize = 24;
|
||||
|
||||
[ObservableProperty]
|
||||
private SolidColorBrush _textColor = new(Colors.White);
|
||||
|
||||
[ObservableProperty]
|
||||
private SolidColorBrush _backgroundColor = new(Colors.Transparent);
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isDraggable = true;
|
||||
|
||||
[ObservableProperty]
|
||||
private DisplayMode _displayMode = DisplayMode.Last5;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isActive = true;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _monitorLabelText = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isMonitorLabelVisible = false;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _activationLabelText = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isActivationLabelVisible = false;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _displayModeText = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isDisplayModeVisible = false;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isVisibleHotkey = false;
|
||||
|
||||
private int _maxKeystrokesShown = 5;
|
||||
|
||||
public HotkeySettings ActivationShortcut { get; set; }
|
||||
= new HotkeySettings(true, false, false, true, 0x4B);
|
||||
|
||||
public HotkeySettings SwitchMonitorHotkey { get; set; }
|
||||
= new HotkeySettings(true, true, false, false, 0x4B);
|
||||
|
||||
public HotkeySettings SwitchDisplayModeHotkey { get; set; }
|
||||
= new HotkeySettings(true, false, false, true, 0x44);
|
||||
|
||||
public event EventHandler<HotkeyAction> HotkeyActionTriggered;
|
||||
|
||||
public async void ShowLabel(HotkeyAction action, string text, int durationMs = 2000)
|
||||
{
|
||||
switch (action)
|
||||
{
|
||||
case HotkeyAction.Monitor:
|
||||
MonitorLabelText = text;
|
||||
IsMonitorLabelVisible = true;
|
||||
break;
|
||||
case HotkeyAction.Activation:
|
||||
ActivationLabelText = text;
|
||||
IsActivationLabelVisible = true;
|
||||
break;
|
||||
case HotkeyAction.DisplayMode:
|
||||
DisplayModeText = text;
|
||||
IsDisplayModeVisible = true;
|
||||
break;
|
||||
}
|
||||
|
||||
IsVisibleHotkey = IsMonitorLabelVisible || IsActivationLabelVisible || IsDisplayModeVisible;
|
||||
|
||||
try
|
||||
{
|
||||
await Task.Delay(durationMs);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// write to logs
|
||||
Logger.LogError("KeystrokeOverlay: Error showing label delay.");
|
||||
}
|
||||
|
||||
switch (action)
|
||||
{
|
||||
case HotkeyAction.Monitor:
|
||||
IsMonitorLabelVisible = false;
|
||||
break;
|
||||
case HotkeyAction.Activation:
|
||||
IsActivationLabelVisible = false;
|
||||
break;
|
||||
case HotkeyAction.DisplayMode:
|
||||
IsDisplayModeVisible = false;
|
||||
break;
|
||||
}
|
||||
|
||||
IsVisibleHotkey = IsMonitorLabelVisible || IsActivationLabelVisible || IsDisplayModeVisible;
|
||||
}
|
||||
|
||||
public void ApplySettings(ModuleProperties props)
|
||||
{
|
||||
TimeoutMs = props.OverlayTimeout.Value;
|
||||
TextSize = props.TextSize.Value;
|
||||
|
||||
TextColor = CustomColorHelper.GetBrushFromHex(props.TextColor.Value);
|
||||
BackgroundColor = CustomColorHelper.GetBrushFromHex(props.BackgroundColor.Value);
|
||||
|
||||
IsDraggable = props.IsDraggable.Value;
|
||||
DisplayMode = (DisplayMode)props.DisplayMode.Value;
|
||||
|
||||
_maxKeystrokesShown = DisplayMode == DisplayMode.SingleCharactersOnly ? 1 : 5;
|
||||
|
||||
ActivationShortcut = props.ActivationShortcut;
|
||||
SwitchMonitorHotkey = props.SwitchMonitorHotkey;
|
||||
SwitchDisplayModeHotkey = props.SwitchDisplayModeHotkey;
|
||||
}
|
||||
|
||||
private readonly KeystrokeProcessor _keystrokeProcessor = new();
|
||||
|
||||
public void HandleKeystrokeEvent(KeystrokeEvent keystroke)
|
||||
{
|
||||
bool isDown = string.Equals(keystroke.EventType, "down", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (isDown && keystroke.IsPressed)
|
||||
{
|
||||
if (CheckGlobalHotkeys(keystroke))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!IsActive)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// update UI
|
||||
var result = _keystrokeProcessor.Process(keystroke, DisplayMode);
|
||||
switch (result.Action)
|
||||
{
|
||||
case KeystrokeAction.Add:
|
||||
RegisterKey(result.Text, TimeoutMs);
|
||||
break;
|
||||
|
||||
case KeystrokeAction.ReplaceLast:
|
||||
if (PressedKeys.Count > 0)
|
||||
{
|
||||
PressedKeys.RemoveAt(PressedKeys.Count - 1);
|
||||
}
|
||||
|
||||
RegisterKey(result.Text, TimeoutMs);
|
||||
break;
|
||||
|
||||
case KeystrokeAction.RemoveLast:
|
||||
if (PressedKeys.Count > 0)
|
||||
{
|
||||
PressedKeys.RemoveAt(PressedKeys.Count - 1);
|
||||
UpdateOpacities();
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case KeystrokeAction.None:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private bool CheckGlobalHotkeys(KeystrokeEvent keystroke)
|
||||
{
|
||||
if (IsHotkeyMatch(keystroke, ActivationShortcut))
|
||||
{
|
||||
IsActive = !IsActive;
|
||||
ShowLabel(HotkeyAction.Activation, IsActive ? "Overlay On" : "Overlay Off");
|
||||
if (!IsActive)
|
||||
{
|
||||
ClearKeys();
|
||||
_keystrokeProcessor.ResetBuffer();
|
||||
}
|
||||
|
||||
HotkeyActionTriggered?.Invoke(this, HotkeyAction.Activation);
|
||||
return true;
|
||||
}
|
||||
else if (IsHotkeyMatch(keystroke, SwitchMonitorHotkey))
|
||||
{
|
||||
HotkeyActionTriggered?.Invoke(this, HotkeyAction.Monitor);
|
||||
return true;
|
||||
}
|
||||
else if (IsHotkeyMatch(keystroke, SwitchDisplayModeHotkey))
|
||||
{
|
||||
int current = (int)DisplayMode;
|
||||
DisplayMode = (DisplayMode)((current + 1) % 4);
|
||||
|
||||
_maxKeystrokesShown = DisplayMode == DisplayMode.SingleCharactersOnly ? 1 : 5;
|
||||
|
||||
string modeText = DisplayMode switch
|
||||
{
|
||||
DisplayMode.Last5 => "Last Five Keystrokes",
|
||||
DisplayMode.SingleCharactersOnly => "Single Characters Only",
|
||||
DisplayMode.ShortcutsOnly => "Shortcuts Only",
|
||||
DisplayMode.Stream => "Stream",
|
||||
_ => "Unknown",
|
||||
};
|
||||
|
||||
_keystrokeProcessor.ResetBuffer();
|
||||
ShowLabel(HotkeyAction.DisplayMode, modeText);
|
||||
HotkeyActionTriggered?.Invoke(this, HotkeyAction.DisplayMode);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool IsHotkeyMatch(KeystrokeEvent kEvent, HotkeySettings settings)
|
||||
{
|
||||
if (settings == null || !settings.IsValid())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Compare the Main Key Code
|
||||
if (kEvent.VirtualKey != settings.Code)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Compare Modifiers
|
||||
bool hasWin = kEvent.Modifiers?.Contains("Win") ?? false;
|
||||
bool hasCtrl = kEvent.Modifiers?.Contains("Ctrl") ?? false;
|
||||
bool hasAlt = kEvent.Modifiers?.Contains("Alt") ?? false;
|
||||
bool hasShift = kEvent.Modifiers?.Contains("Shift") ?? false;
|
||||
|
||||
return hasWin == settings.Win &&
|
||||
hasCtrl == settings.Ctrl &&
|
||||
hasAlt == settings.Alt &&
|
||||
hasShift == settings.Shift;
|
||||
}
|
||||
|
||||
public void RegisterKey(string key, int durationMs, int textSize = -1)
|
||||
{
|
||||
if (textSize == -1)
|
||||
{
|
||||
textSize = TextSize;
|
||||
}
|
||||
|
||||
var newItem = new KeyVisualItem { Text = key, Opacity = 1.0, TextSize = textSize };
|
||||
PressedKeys.Add(newItem);
|
||||
|
||||
UpdateOpacities();
|
||||
|
||||
if (PressedKeys.Count > _maxKeystrokesShown)
|
||||
{
|
||||
PressedKeys.RemoveAt(0);
|
||||
UpdateOpacities();
|
||||
}
|
||||
|
||||
// Pass the duration to the removal logic
|
||||
_ = RemoveKeyAfterDelayAsync(newItem, durationMs);
|
||||
}
|
||||
|
||||
// 2. Add a helper to clear keys immediately (for switching phases)
|
||||
public void ClearKeys()
|
||||
{
|
||||
PressedKeys.Clear();
|
||||
}
|
||||
|
||||
private void UpdateOpacities()
|
||||
{
|
||||
// Iterate through all keys
|
||||
for (int i = 0; i < PressedKeys.Count; i++)
|
||||
{
|
||||
var item = PressedKeys[i];
|
||||
|
||||
// If the item is currently running its "death animation", skip it
|
||||
if (item.IsExiting)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Calculate index from the end (Newest = 0)
|
||||
int indexFromEnd = PressedKeys.Count - 1 - i;
|
||||
|
||||
// Decrease 15% for every step back
|
||||
double targetOpacity = 1.0 - (0.15 * indexFromEnd);
|
||||
|
||||
// Clamp to valid range (e.g. don't go below 0.1 visible)
|
||||
item.Opacity = Math.Max(0.1, targetOpacity);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RemoveKeyAfterDelayAsync(KeyVisualItem item, int durationMs)
|
||||
{
|
||||
// Wait the defined lifetime
|
||||
await Task.Delay(durationMs);
|
||||
|
||||
// Mark as exiting so UpdateOpacities doesn't fight us
|
||||
item.IsExiting = true;
|
||||
|
||||
PressedKeys.Remove(item);
|
||||
|
||||
// Re-adjust remaining keys
|
||||
UpdateOpacities();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<Window
|
||||
x:Class="KeystrokeOverlayUI.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="using:KeystrokeOverlayUI"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls"
|
||||
mc:Ignorable="d"
|
||||
Title="KeystrokeOverlayUI">
|
||||
|
||||
<Window.SystemBackdrop>
|
||||
<MicaBackdrop Kind="BaseAlt"/>
|
||||
</Window.SystemBackdrop>
|
||||
|
||||
<Border x:Name="RootGrid"
|
||||
Loaded="RootGrid_Loaded"
|
||||
PointerPressed="RootGrid_PointerPressed"
|
||||
PointerMoved="RootGrid_PointerMoved"
|
||||
PointerReleased="RootGrid_PointerReleased"
|
||||
PointerEntered="RootGrid_PointerEntered"
|
||||
PointerExited="RootGrid_PointerExited"
|
||||
Padding="10,7"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Background="Transparent">
|
||||
|
||||
<StackPanel Orientation="Vertical" Spacing="5">
|
||||
|
||||
<StackPanel Orientation="Horizontal" Spacing="5">
|
||||
<Border CornerRadius="4"
|
||||
Padding="8,4"
|
||||
HorizontalAlignment="Left"
|
||||
Visibility="{x:Bind ViewModel.IsMonitorLabelVisible, Mode=OneWay}">
|
||||
|
||||
<TextBlock Text="{x:Bind ViewModel.MonitorLabelText, Mode=OneWay}"
|
||||
Foreground="Black"
|
||||
FontSize="18"
|
||||
FontWeight="SemiBold"/>
|
||||
</Border>
|
||||
|
||||
<Border CornerRadius="4"
|
||||
Padding="8,4"
|
||||
HorizontalAlignment="Left"
|
||||
Visibility="{x:Bind ViewModel.IsActivationLabelVisible, Mode=OneWay}">
|
||||
|
||||
<TextBlock Text="{x:Bind ViewModel.ActivationLabelText, Mode=OneWay}"
|
||||
Foreground="Black"
|
||||
FontSize="18"
|
||||
FontWeight="SemiBold"/>
|
||||
</Border>
|
||||
|
||||
<Border CornerRadius="4"
|
||||
Padding="8,4"
|
||||
HorizontalAlignment="Left"
|
||||
Visibility="{x:Bind ViewModel.IsDisplayModeVisible, Mode=OneWay}">
|
||||
|
||||
<TextBlock Text="{x:Bind ViewModel.DisplayModeText, Mode=OneWay}"
|
||||
Foreground="Black"
|
||||
FontSize="18"
|
||||
FontWeight="SemiBold"/>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<ItemsControl ItemsSource="{x:Bind ViewModel.PressedKeys, Mode=OneWay}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Horizontal" Spacing="10" SizeChanged="StackPanel_SizeChanged"/>
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<controls:KeyVisual
|
||||
Content="{Binding Text}"
|
||||
VerticalAlignment="Center"
|
||||
IsTabStop="False"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
Padding="12,6"
|
||||
CornerRadius="6"
|
||||
Opacity="{Binding Opacity}"
|
||||
FontSize="{Binding TextSize}"
|
||||
Foreground="{Binding DataContext.TextColor, ElementName=RootGrid}"
|
||||
Background="{Binding DataContext.BackgroundColor, ElementName=RootGrid}">
|
||||
|
||||
<controls:KeyVisual.OpacityTransition>
|
||||
<ScalarTransition />
|
||||
</controls:KeyVisual.OpacityTransition>
|
||||
|
||||
</controls:KeyVisual>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Window>
|
||||
@@ -0,0 +1,465 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.InteropServices.WindowsRuntime;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using KeystrokeOverlayUI.Controls;
|
||||
using KeystrokeOverlayUI.Helpers;
|
||||
using KeystrokeOverlayUI.Models;
|
||||
using KeystrokeOverlayUI.Services;
|
||||
|
||||
using Microsoft.UI;
|
||||
using Microsoft.UI.Composition.SystemBackdrops;
|
||||
using Microsoft.UI.Input;
|
||||
using Microsoft.UI.Windowing;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Controls.Primitives;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Microsoft.UI.Xaml.Navigation;
|
||||
using Windows.Foundation;
|
||||
using Windows.Foundation.Collections;
|
||||
using WinRT.Interop;
|
||||
|
||||
namespace KeystrokeOverlayUI
|
||||
{
|
||||
// Main overlay window.
|
||||
public sealed partial class MainWindow : Window, IDisposable
|
||||
{
|
||||
public MainViewModel ViewModel { get; set; } = new();
|
||||
|
||||
// readonly constants
|
||||
private readonly InputCursor _dragCursor = InputSystemCursor.Create(InputSystemCursorShape.SizeAll);
|
||||
private readonly DispatcherTimer _zOrderEnforcer = new();
|
||||
|
||||
// core components
|
||||
private readonly KeystrokeListener _keystrokeListener = new();
|
||||
private readonly OverlaySettings _overlaySettings = new();
|
||||
private CancellationTokenSource _startupCancellationSource;
|
||||
private bool _disposed;
|
||||
|
||||
// draggable overlay
|
||||
private bool _isDragging;
|
||||
private NativeWindowHelper.POINT _lastCursorPos;
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
SystemBackdrop = new MicaBackdrop() { Kind = MicaKind.BaseAlt };
|
||||
ForceWindowOnTop();
|
||||
|
||||
Activated += (s, e) => ApplyOverlayStyles();
|
||||
_zOrderEnforcer.Interval = TimeSpan.FromMilliseconds(500);
|
||||
_zOrderEnforcer.Tick += (s, e) => ForceWindowOnTop();
|
||||
|
||||
RootGrid.DataContext = ViewModel;
|
||||
|
||||
_overlaySettings.SettingsUpdated += (props) =>
|
||||
{
|
||||
DispatcherQueue.TryEnqueue(() => ViewModel.ApplySettings(props));
|
||||
};
|
||||
|
||||
_overlaySettings.Initialize();
|
||||
|
||||
_keystrokeListener.OnBatchReceived += OnKeyReceived;
|
||||
_keystrokeListener.Start();
|
||||
|
||||
ConfigureOverlayWindow();
|
||||
RunStartupSequence(isDraggable: ViewModel.IsDraggable);
|
||||
|
||||
ViewModel.HotkeyActionTriggered += OnHotkeyActionTriggered;
|
||||
|
||||
ViewModel.PropertyChanged += (s, e) =>
|
||||
{
|
||||
if (e.PropertyName == nameof(MainViewModel.IsDraggable))
|
||||
{
|
||||
RunStartupSequence(isDraggable: ViewModel.IsDraggable);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void ApplyOverlayStyles()
|
||||
{
|
||||
IntPtr hWnd = WindowNative.GetWindowHandle(this);
|
||||
NativeWindowHelper.SetOverlayWindowStyles(hWnd);
|
||||
NativeWindowHelper.SetRoundedCorners(hWnd);
|
||||
|
||||
ForceWindowOnTop();
|
||||
}
|
||||
|
||||
private void ConfigureOverlayWindow()
|
||||
{
|
||||
var appWindow = GetAppWindow();
|
||||
|
||||
if (appWindow != null)
|
||||
{
|
||||
var presenter = appWindow.Presenter as OverlappedPresenter
|
||||
?? OverlappedPresenter.Create();
|
||||
|
||||
appWindow.SetPresenter(presenter);
|
||||
|
||||
presenter.IsAlwaysOnTop = true;
|
||||
presenter.IsResizable = false;
|
||||
presenter.IsMinimizable = false;
|
||||
presenter.IsMaximizable = false;
|
||||
presenter.SetBorderAndTitleBar(false, false);
|
||||
}
|
||||
}
|
||||
|
||||
private async void RunStartupSequence(bool isDraggable)
|
||||
{
|
||||
_startupCancellationSource?.Cancel();
|
||||
_startupCancellationSource?.Dispose();
|
||||
_startupCancellationSource = new CancellationTokenSource();
|
||||
|
||||
var token = _startupCancellationSource.Token;
|
||||
|
||||
ForceWindowOnTop();
|
||||
if (!_zOrderEnforcer.IsEnabled)
|
||||
{
|
||||
_zOrderEnforcer.Start();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (isDraggable)
|
||||
{
|
||||
// Loop with cancellation check
|
||||
for (int index = 10; index > 0; index--)
|
||||
{
|
||||
ViewModel.RegisterKey($"Drag to Position ({index})", durationMs: 1000, textSize: 30);
|
||||
await Task.Delay(1000, token);
|
||||
}
|
||||
}
|
||||
|
||||
// cleanup
|
||||
ViewModel.ClearKeys();
|
||||
await Task.Delay(500, token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
ViewModel.ClearKeys();
|
||||
}
|
||||
finally
|
||||
{
|
||||
// cleanup
|
||||
if (_startupCancellationSource != null)
|
||||
{
|
||||
_startupCancellationSource.Dispose();
|
||||
_startupCancellationSource = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnKeyReceived(KeystrokeEvent kEvent)
|
||||
{
|
||||
if (_startupCancellationSource != null && !_startupCancellationSource.IsCancellationRequested)
|
||||
{
|
||||
_startupCancellationSource.Cancel();
|
||||
}
|
||||
|
||||
DispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
ViewModel.HandleKeystrokeEvent(kEvent);
|
||||
|
||||
if (!_zOrderEnforcer.IsEnabled)
|
||||
{
|
||||
_zOrderEnforcer.Start();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------
|
||||
// Hotkey Methods
|
||||
// ----------------------
|
||||
private void OnHotkeyActionTriggered(object sender, HotkeyAction action)
|
||||
{
|
||||
switch (action)
|
||||
{
|
||||
case HotkeyAction.Monitor:
|
||||
MoveToNextMonitor();
|
||||
break;
|
||||
case HotkeyAction.DisplayMode:
|
||||
case HotkeyAction.Activation:
|
||||
HandleActivation();
|
||||
break;
|
||||
}
|
||||
|
||||
ForceWindowOnTop();
|
||||
|
||||
// resize to show labels
|
||||
RootGrid.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
|
||||
var desiredSize = RootGrid.DesiredSize;
|
||||
ResizeAppWindow(desiredSize.Width + 5, desiredSize.Height + 15);
|
||||
}
|
||||
|
||||
private void HandleActivation()
|
||||
{
|
||||
ShowAppWindow();
|
||||
}
|
||||
|
||||
private void MoveToNextMonitor()
|
||||
{
|
||||
IntPtr hWnd = WindowNative.GetWindowHandle(this);
|
||||
WindowId wndId = Win32Interop.GetWindowIdFromWindow(hWnd);
|
||||
AppWindow appWindow = AppWindow.GetFromWindowId(wndId);
|
||||
|
||||
if (appWindow == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var displayAreas = DisplayArea.FindAll();
|
||||
if (displayAreas.Count <= 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Find current display index
|
||||
DisplayArea currentDisplay = DisplayArea.GetFromWindowId(wndId, DisplayAreaFallback.Primary);
|
||||
|
||||
int currentIndex = -1;
|
||||
for (int i = 0; i < displayAreas.Count; i++)
|
||||
{
|
||||
if (displayAreas[i].DisplayId.Value == currentDisplay.DisplayId.Value)
|
||||
{
|
||||
currentIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate Next Index
|
||||
int nextIndex = (currentIndex + 1) % displayAreas.Count;
|
||||
DisplayArea nextDisplay = displayAreas[nextIndex];
|
||||
|
||||
// move to Top-Left of new monitor
|
||||
int newX = nextDisplay.WorkArea.X + 15;
|
||||
int newY = nextDisplay.WorkArea.Y + 12;
|
||||
|
||||
appWindow.Move(new Windows.Graphics.PointInt32(newX, newY));
|
||||
ViewModel.ShowLabel(HotkeyAction.Monitor, $"Monitor {nextIndex + 1}");
|
||||
}
|
||||
|
||||
// ----------------------
|
||||
// Draggable Overlay
|
||||
// ----------------------
|
||||
private void SetRootGridCursor(InputCursor cursor)
|
||||
{
|
||||
// Use Reflection to access the protected "ProtectedCursor" property on the Border (RootGrid)
|
||||
typeof(UIElement)
|
||||
.GetProperty("ProtectedCursor", BindingFlags.NonPublic | BindingFlags.Instance)
|
||||
?.SetValue(RootGrid, cursor);
|
||||
}
|
||||
|
||||
private void RootGrid_PointerPressed(object sender, PointerRoutedEventArgs e)
|
||||
{
|
||||
var properties = e.GetCurrentPoint(RootGrid).Properties;
|
||||
|
||||
if (ViewModel.IsDraggable && properties.IsLeftButtonPressed)
|
||||
{
|
||||
_isDragging = true;
|
||||
RootGrid.CapturePointer(e.Pointer);
|
||||
_lastCursorPos = NativeWindowHelper.GetCursorPosition();
|
||||
|
||||
SetRootGridCursor(_dragCursor);
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void RootGrid_PointerMoved(object sender, PointerRoutedEventArgs e)
|
||||
{
|
||||
if (_isDragging)
|
||||
{
|
||||
var currentPos = NativeWindowHelper.GetCursorPosition();
|
||||
|
||||
int deltaX = currentPos.X - _lastCursorPos.X;
|
||||
int deltaY = currentPos.Y - _lastCursorPos.Y;
|
||||
|
||||
var appWindow = GetAppWindow();
|
||||
if (appWindow != null)
|
||||
{
|
||||
var newPos = new Windows.Graphics.PointInt32(
|
||||
appWindow.Position.X + deltaX,
|
||||
appWindow.Position.Y + deltaY );
|
||||
|
||||
appWindow.Move(newPos);
|
||||
}
|
||||
|
||||
_lastCursorPos = currentPos;
|
||||
}
|
||||
}
|
||||
|
||||
private void RootGrid_PointerReleased(object sender, PointerRoutedEventArgs e)
|
||||
{
|
||||
if (_isDragging)
|
||||
{
|
||||
_isDragging = false;
|
||||
RootGrid.ReleasePointerCapture(e.Pointer);
|
||||
UpdateCursorState();
|
||||
}
|
||||
}
|
||||
|
||||
private void RootGrid_PointerEntered(object sender, PointerRoutedEventArgs e)
|
||||
{
|
||||
UpdateCursorState();
|
||||
}
|
||||
|
||||
private void RootGrid_PointerExited(object sender, PointerRoutedEventArgs e)
|
||||
{
|
||||
SetRootGridCursor(null);
|
||||
}
|
||||
|
||||
private void UpdateCursorState()
|
||||
{
|
||||
if (ViewModel.IsDraggable)
|
||||
{
|
||||
SetRootGridCursor(_dragCursor);
|
||||
}
|
||||
else
|
||||
{
|
||||
SetRootGridCursor(null);
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------
|
||||
// WinUI Event Handlers
|
||||
// ----------------------
|
||||
private void RootGrid_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
RootGrid.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
|
||||
var desiredSize = RootGrid.DesiredSize;
|
||||
ResizeAppWindow(desiredSize.Width + 5, desiredSize.Height + 15);
|
||||
}
|
||||
|
||||
private void StackPanel_SizeChanged(object sender, SizeChangedEventArgs e)
|
||||
{
|
||||
if (sender is not StackPanel stackPanel)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
stackPanel.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
|
||||
var desiredSize = stackPanel.DesiredSize;
|
||||
|
||||
if (desiredSize.Width == 0 ||
|
||||
desiredSize.Height == 0 ||
|
||||
ViewModel.PressedKeys.Count == 0)
|
||||
{
|
||||
if (!ViewModel.IsVisibleHotkey)
|
||||
{
|
||||
HideAppWindow();
|
||||
_zOrderEnforcer.Stop();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
ShowAppWindow();
|
||||
|
||||
double totalWidth =
|
||||
desiredSize.Width + RootGrid.Padding.Left + RootGrid.Padding.Right + 5;
|
||||
|
||||
double totalHeight =
|
||||
desiredSize.Height + RootGrid.Padding.Top + RootGrid.Padding.Bottom + 15;
|
||||
|
||||
if (ViewModel.IsVisibleHotkey)
|
||||
{
|
||||
totalHeight = totalHeight + 30;
|
||||
}
|
||||
|
||||
ResizeAppWindow(totalWidth, totalHeight);
|
||||
ForceWindowOnTop();
|
||||
}
|
||||
|
||||
// ----------------------
|
||||
// Window Helper Methods
|
||||
// ----------------------
|
||||
private AppWindow GetAppWindow()
|
||||
{
|
||||
IntPtr hWnd = WindowNative.GetWindowHandle(this);
|
||||
WindowId wndId = Win32Interop.GetWindowIdFromWindow(hWnd);
|
||||
return AppWindow.GetFromWindowId(wndId);
|
||||
}
|
||||
|
||||
private void HideAppWindow()
|
||||
{
|
||||
var appWindow = GetAppWindow();
|
||||
appWindow?.Hide();
|
||||
}
|
||||
|
||||
private void ShowAppWindow()
|
||||
{
|
||||
var appWindow = GetAppWindow();
|
||||
appWindow?.Show();
|
||||
}
|
||||
|
||||
private void ResizeAppWindow(double widthDIPs, double heightDIPs)
|
||||
{
|
||||
var appWindow = GetAppWindow();
|
||||
|
||||
if (appWindow != null)
|
||||
{
|
||||
double scale = RootGrid.XamlRoot.RasterizationScale;
|
||||
|
||||
int windowWidth = (int)Math.Ceiling(widthDIPs * scale);
|
||||
int windowHeight = (int)Math.Ceiling(heightDIPs * scale);
|
||||
|
||||
if (appWindow.Size.Width != windowWidth ||
|
||||
appWindow.Size.Height != windowHeight)
|
||||
{
|
||||
appWindow.Resize(new Windows.Graphics.SizeInt32(windowWidth, windowHeight));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ForceWindowOnTop()
|
||||
{
|
||||
if (ViewModel.PressedKeys.Count == 0)
|
||||
{
|
||||
_zOrderEnforcer.Stop();
|
||||
return;
|
||||
}
|
||||
|
||||
IntPtr hWnd = WindowNative.GetWindowHandle(this);
|
||||
NativeWindowHelper.EnforceTopMost(hWnd);
|
||||
}
|
||||
|
||||
// -------------------
|
||||
// Other Methods
|
||||
// -------------------
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_keystrokeListener?.Dispose();
|
||||
_overlaySettings?.Dispose();
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
~MainWindow()
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace KeystrokeOverlayUI.Models
|
||||
{
|
||||
public class BoolProperty
|
||||
{
|
||||
[JsonPropertyName("value")]
|
||||
public bool Value { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace KeystrokeOverlayUI.Models
|
||||
{
|
||||
public class IntProperty
|
||||
{
|
||||
[JsonPropertyName("value")]
|
||||
public int Value { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace KeystrokeOverlayUI.Models
|
||||
{
|
||||
public class StringProperty
|
||||
{
|
||||
[JsonPropertyName("value")]
|
||||
public string Value { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace KeystrokeOverlayUI.Models
|
||||
{
|
||||
public enum DisplayMode
|
||||
{
|
||||
Last5 = 0,
|
||||
SingleCharactersOnly = 1,
|
||||
ShortcutsOnly = 2,
|
||||
Stream = 3,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace KeystrokeOverlayUI.Models
|
||||
{
|
||||
public enum HotkeyAction
|
||||
{
|
||||
Monitor,
|
||||
Activation,
|
||||
DisplayMode,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace KeystrokeOverlayUI.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Single keystroke event emitted by the native Batcher.
|
||||
/// Matches native JSON fields: t, vk, text, mods, ts.
|
||||
/// </summary>
|
||||
public sealed class KeystrokeBatchEvent
|
||||
{
|
||||
[JsonPropertyName("t")]
|
||||
public string Type { get; set; }
|
||||
|
||||
[JsonPropertyName("vk")]
|
||||
public int VirtualKey { get; set; }
|
||||
|
||||
[JsonPropertyName("text")]
|
||||
public string Text { get; set; }
|
||||
|
||||
[JsonPropertyName("mods")]
|
||||
public string[] Modifiers { get; set; }
|
||||
|
||||
[JsonPropertyName("ts")]
|
||||
public double Timestamp { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace KeystrokeOverlayUI.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Root object wrapping an array of keystroke events.
|
||||
/// Matches native JSON: { "schema": 1, "events": [ ... ] }.
|
||||
/// </summary>
|
||||
public sealed class KeystrokeBatchRoot
|
||||
{
|
||||
[JsonPropertyName("schema")]
|
||||
public int Schema { get; set; }
|
||||
|
||||
[JsonPropertyName("events")]
|
||||
public KeystrokeBatchEvent[] Events { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace KeystrokeOverlayUI.Models
|
||||
{
|
||||
public enum KeystrokeAction
|
||||
{
|
||||
None, // Do nothing
|
||||
Add, // Create a new visual bubble (e.g., new word or shortcut)
|
||||
ReplaceLast, // Update the current bubble (e.g., typing "Hell" -> "Hello")
|
||||
RemoveLast, // Backspace a full bubble
|
||||
}
|
||||
|
||||
public struct KeystrokeResult
|
||||
{
|
||||
public KeystrokeAction Action { get; set; }
|
||||
|
||||
public string Text { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
using KeystrokeOverlayUI.Controls;
|
||||
using KeystrokeOverlayUI.Models;
|
||||
|
||||
namespace KeystrokeOverlayUI
|
||||
{
|
||||
[JsonSourceGenerationOptions(PropertyNameCaseInsensitive = true)]
|
||||
[JsonSerializable(typeof(KeystrokeEvent))]
|
||||
[JsonSerializable(typeof(KeystrokeBatchRoot))]
|
||||
[JsonSerializable(typeof(KeystrokeBatchEvent))]
|
||||
internal sealed partial class KeystrokeEventJsonContext : JsonSerializerContext
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
using KeystrokeOverlayUI.Models;
|
||||
|
||||
namespace KeystrokeOverlayUI;
|
||||
|
||||
/// Trimming-safe JSON metadata for loading module settings.
|
||||
[JsonSourceGenerationOptions(PropertyNameCaseInsensitive = true)]
|
||||
[JsonSerializable(typeof(ModuleSettingsRoot))]
|
||||
internal sealed partial class ModuleSettingsJsonContext : JsonSerializerContext
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Settings.UI.Library.Attributes;
|
||||
|
||||
namespace KeystrokeOverlayUI.Models
|
||||
{
|
||||
public class ModuleProperties
|
||||
{
|
||||
[CmdConfigureIgnore]
|
||||
public HotkeySettings DefaultSwitchMonitorHotkey => new HotkeySettings(true, true, false, false, 0x4B);
|
||||
|
||||
[CmdConfigureIgnore]
|
||||
public HotkeySettings DefaultActivationShortcut => new HotkeySettings(true, false, false, true, 0x4B);
|
||||
|
||||
[CmdConfigureIgnore]
|
||||
public HotkeySettings DefaultSwitchDisplayModeHotkey => new HotkeySettings(true, false, false, true, 0x44);
|
||||
|
||||
[JsonPropertyName("enable_draggable_overlay")]
|
||||
public BoolProperty IsDraggable { get; set; } = new() { Value = true };
|
||||
|
||||
[JsonPropertyName("activation_shortcut")]
|
||||
public HotkeySettings ActivationShortcut { get; set; }
|
||||
|
||||
[JsonPropertyName("switch_monitor_hotkey")]
|
||||
public HotkeySettings SwitchMonitorHotkey { get; set; }
|
||||
|
||||
[JsonPropertyName("switch_display_mode_hotkey")]
|
||||
public HotkeySettings SwitchDisplayModeHotkey { get; set; }
|
||||
|
||||
[JsonPropertyName("display_mode")]
|
||||
public IntProperty DisplayMode { get; set; } = new() { Value = 0 };
|
||||
|
||||
[JsonPropertyName("overlay_timeout")]
|
||||
public IntProperty OverlayTimeout { get; set; } = new() { Value = 3000 };
|
||||
|
||||
[JsonPropertyName("text_size")]
|
||||
public IntProperty TextSize { get; set; } = new() { Value = 24 };
|
||||
|
||||
[JsonPropertyName("text_color")]
|
||||
public StringProperty TextColor { get; set; } = new() { Value = "#FF000000" };
|
||||
|
||||
[JsonPropertyName("background_color")]
|
||||
public StringProperty BackgroundColor { get; set; } = new() { Value = "#00000000" };
|
||||
|
||||
public ModuleProperties()
|
||||
{
|
||||
ActivationShortcut = DefaultActivationShortcut;
|
||||
SwitchMonitorHotkey = DefaultSwitchMonitorHotkey;
|
||||
SwitchDisplayModeHotkey = DefaultSwitchDisplayModeHotkey;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace KeystrokeOverlayUI.Models
|
||||
{
|
||||
public class ModuleSettingsRoot
|
||||
{
|
||||
[JsonPropertyName("properties")]
|
||||
public ModuleProperties Properties { get; set; } = new();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.IO.Pipes;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using KeystrokeOverlayUI;
|
||||
using KeystrokeOverlayUI.Controls;
|
||||
using KeystrokeOverlayUI.Models;
|
||||
|
||||
namespace KeystrokeOverlayUI.Services
|
||||
{
|
||||
// Connects to the native KeystrokeOverlayPipe and converts native JSON
|
||||
// batches into KeystrokeEvent objects for the overlay UI.
|
||||
public class KeystrokeListener : IDisposable
|
||||
{
|
||||
private const string PipeName = "KeystrokeOverlayPipe";
|
||||
|
||||
private CancellationTokenSource _cts;
|
||||
|
||||
public event Action<KeystrokeEvent> OnBatchReceived;
|
||||
|
||||
public void Start()
|
||||
{
|
||||
_cts = new CancellationTokenSource();
|
||||
Task.Run(() => ListenLoop(_cts.Token));
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
_cts?.Cancel();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Stop();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private async Task ListenLoop(CancellationToken token)
|
||||
{
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var client = new NamedPipeClientStream(
|
||||
serverName: ".",
|
||||
pipeName: PipeName,
|
||||
direction: PipeDirection.In,
|
||||
options: PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly);
|
||||
|
||||
await client.ConnectAsync(token).ConfigureAwait(false);
|
||||
|
||||
using var reader = new BinaryReader(client, Encoding.UTF8, leaveOpen: false);
|
||||
|
||||
while (client.IsConnected && !token.IsCancellationRequested)
|
||||
{
|
||||
// Length-prefixed frame
|
||||
int length = reader.ReadInt32();
|
||||
const int MaxFrameSize = 8 * 1024 * 1024;
|
||||
|
||||
if (length <= 0 || length > MaxFrameSize)
|
||||
{
|
||||
Debug.WriteLine($"KeystrokeListener: invalid frame length {length}, reconnecting");
|
||||
break;
|
||||
}
|
||||
|
||||
// Read JSON payload
|
||||
byte[] buffer = reader.ReadBytes(length);
|
||||
if (buffer.Length != length)
|
||||
{
|
||||
Debug.WriteLine($"KeystrokeListener: short read {buffer.Length}/{length}, reconnecting");
|
||||
break;
|
||||
}
|
||||
|
||||
string json = Encoding.UTF8.GetString(buffer);
|
||||
|
||||
try
|
||||
{
|
||||
// Deserialize batch
|
||||
var root = JsonSerializer.Deserialize(
|
||||
json,
|
||||
KeystrokeEventJsonContext.Default.KeystrokeBatchRoot);
|
||||
|
||||
if (root?.Events == null || root.Events.Length == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Process only DOWN events to avoid duplicates
|
||||
foreach (var e in root.Events)
|
||||
{
|
||||
bool isDown = string.Equals(e.Type, "down", StringComparison.OrdinalIgnoreCase);
|
||||
bool isChar = string.Equals(e.Type, "char", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (!isDown && !isChar)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var uiEvent = new KeystrokeEvent
|
||||
{
|
||||
VirtualKey = (uint)e.VirtualKey,
|
||||
IsPressed = true, // Both down and char imply a press for UI purposes
|
||||
Modifiers = e.Modifiers != null ? new List<string>(e.Modifiers) : new List<string>(),
|
||||
Text = e.Text, // Capture the text from backend
|
||||
EventType = e.Type, // Capture the type
|
||||
};
|
||||
|
||||
OnBatchReceived?.Invoke(uiEvent);
|
||||
}
|
||||
}
|
||||
catch (JsonException je)
|
||||
{
|
||||
Debug.WriteLine($"KeystrokeListener: JSON parse error: {je.Message}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"KeystrokeListener: error processing frame: {ex}");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"KeystrokeListener: connect error: {ex}");
|
||||
await Task.Delay(1000, token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using KeystrokeOverlayUI.Controls;
|
||||
using KeystrokeOverlayUI.Models;
|
||||
|
||||
namespace KeystrokeOverlayUI.Services
|
||||
{
|
||||
public class KeystrokeProcessor
|
||||
{
|
||||
private string _streamBuffer = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Determines what visual action to take based on the incoming key and current mode.
|
||||
/// </summary>
|
||||
public KeystrokeResult Process(KeystrokeEvent kEvent, DisplayMode displayMode)
|
||||
{
|
||||
string formattedText = kEvent.ToString();
|
||||
|
||||
// Early out for no text
|
||||
if (string.IsNullOrEmpty(formattedText))
|
||||
{
|
||||
return new KeystrokeResult { Action = KeystrokeAction.None };
|
||||
}
|
||||
|
||||
bool isShortcut = kEvent.IsShortcut;
|
||||
|
||||
// Mode-specific logic
|
||||
switch (displayMode)
|
||||
{
|
||||
case DisplayMode.Last5:
|
||||
return new KeystrokeResult { Action = KeystrokeAction.Add, Text = formattedText };
|
||||
|
||||
case DisplayMode.SingleCharactersOnly:
|
||||
if (isShortcut)
|
||||
{
|
||||
return new KeystrokeResult { Action = KeystrokeAction.None };
|
||||
}
|
||||
|
||||
return new KeystrokeResult { Action = KeystrokeAction.Add, Text = formattedText };
|
||||
|
||||
case DisplayMode.ShortcutsOnly:
|
||||
if (!isShortcut)
|
||||
{
|
||||
return new KeystrokeResult { Action = KeystrokeAction.None };
|
||||
}
|
||||
|
||||
return new KeystrokeResult { Action = KeystrokeAction.Add, Text = formattedText };
|
||||
|
||||
case DisplayMode.Stream:
|
||||
return ProcessStreamMode(kEvent, isShortcut, formattedText);
|
||||
|
||||
default:
|
||||
return new KeystrokeResult { Action = KeystrokeAction.None };
|
||||
}
|
||||
}
|
||||
|
||||
private KeystrokeResult ProcessStreamMode(KeystrokeEvent kEvent, bool isShortcut, string formattedText)
|
||||
{
|
||||
// handle Backspace
|
||||
if (kEvent.VirtualKey == (uint)Windows.System.VirtualKey.Back)
|
||||
{
|
||||
if (_streamBuffer.Length > 0)
|
||||
{
|
||||
_streamBuffer = _streamBuffer.Substring(0, _streamBuffer.Length - 1);
|
||||
|
||||
if (string.IsNullOrEmpty(_streamBuffer))
|
||||
{
|
||||
return new KeystrokeResult { Action = KeystrokeAction.RemoveLast };
|
||||
}
|
||||
else
|
||||
{
|
||||
return new KeystrokeResult { Action = KeystrokeAction.ReplaceLast, Text = _streamBuffer };
|
||||
}
|
||||
}
|
||||
|
||||
return new KeystrokeResult { Action = KeystrokeAction.None };
|
||||
}
|
||||
|
||||
// If a shortcut is pressed (and it's not Space), we reset the stream.
|
||||
if (isShortcut && kEvent.VirtualKey != (uint)Windows.System.VirtualKey.Space)
|
||||
{
|
||||
ResetBuffer();
|
||||
return new KeystrokeResult { Action = KeystrokeAction.Add, Text = formattedText };
|
||||
}
|
||||
|
||||
// If it's a space/tab, we clear the buffer so the next character starts a new bubble.
|
||||
if (string.IsNullOrWhiteSpace(kEvent.Text))
|
||||
{
|
||||
ResetBuffer();
|
||||
return new KeystrokeResult { Action = KeystrokeAction.None };
|
||||
}
|
||||
|
||||
_streamBuffer += kEvent.Text;
|
||||
|
||||
// If this is the start of a word (Length 1), Add it.
|
||||
// If we are appending to a word (Length > 1), Replace the last bubble.
|
||||
if (_streamBuffer.Length == 1)
|
||||
{
|
||||
return new KeystrokeResult { Action = KeystrokeAction.Add, Text = _streamBuffer };
|
||||
}
|
||||
else
|
||||
{
|
||||
return new KeystrokeResult { Action = KeystrokeAction.ReplaceLast, Text = _streamBuffer };
|
||||
}
|
||||
}
|
||||
|
||||
public void ResetBuffer()
|
||||
{
|
||||
_streamBuffer = string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using KeystrokeOverlayUI;
|
||||
using KeystrokeOverlayUI.Models;
|
||||
using Microsoft.UI;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Windows.UI;
|
||||
|
||||
namespace KeystrokeOverlayUI.Services
|
||||
{
|
||||
public class OverlaySettings : IDisposable
|
||||
{
|
||||
// Event to notify ViewModel when settings change
|
||||
public event Action<ModuleProperties> SettingsUpdated;
|
||||
|
||||
private const string ModuleName = "KeystrokeOverlay";
|
||||
private readonly string _settingsFilePath;
|
||||
private FileSystemWatcher _watcher;
|
||||
|
||||
public OverlaySettings()
|
||||
{
|
||||
// Path: %LOCALAPPDATA%\Microsoft\PowerToys\KeystrokeOverlay\settings.json
|
||||
string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
||||
_settingsFilePath = Path.Combine(localAppData, "Microsoft", "PowerToys", ModuleName, "settings.json");
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
// 1. Load initial settings
|
||||
LoadSettings();
|
||||
|
||||
// 2. Watch for changes
|
||||
SetupWatcher();
|
||||
}
|
||||
|
||||
private void SetupWatcher()
|
||||
{
|
||||
try
|
||||
{
|
||||
string folder = Path.GetDirectoryName(_settingsFilePath);
|
||||
if (string.IsNullOrEmpty(folder) || !Directory.Exists(folder))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_watcher = new FileSystemWatcher(folder, "settings.json")
|
||||
{
|
||||
NotifyFilter = NotifyFilters.LastWrite,
|
||||
EnableRaisingEvents = true,
|
||||
};
|
||||
|
||||
// Debounce logic (FileWatcher often fires twice)
|
||||
DateTime lastRead = DateTime.MinValue;
|
||||
_watcher.Changed += (s, e) =>
|
||||
{
|
||||
if ((DateTime.Now - lastRead).TotalMilliseconds < 500)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lastRead = DateTime.Now;
|
||||
|
||||
// Give the writing process (dllmain) a moment to close the file
|
||||
Task.Delay(100).ContinueWith(_ => LoadSettings());
|
||||
};
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Watcher might fail if permissions are weird, just ignore
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadSettings()
|
||||
{
|
||||
if (!File.Exists(_settingsFilePath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Retry loop in case file is locked by dllmain
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
string json = File.ReadAllText(_settingsFilePath);
|
||||
var root = JsonSerializer.Deserialize(json, ModuleSettingsJsonContext.Default.ModuleSettingsRoot);
|
||||
|
||||
if (root?.Properties != null)
|
||||
{
|
||||
SettingsUpdated?.Invoke(root.Properties);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
System.Threading.Thread.Sleep(50);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Log error or ignore
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_watcher != null)
|
||||
{
|
||||
_watcher.Dispose();
|
||||
_watcher = null;
|
||||
}
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace KeystrokeOverlayUI.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Uses a Windows Job Object to ensure child processes terminate when this
|
||||
/// process exits — even if killed via Task Manager.
|
||||
/// </summary>
|
||||
public sealed class ProcessJob : IDisposable
|
||||
{
|
||||
private IntPtr _hJob;
|
||||
|
||||
public ProcessJob()
|
||||
{
|
||||
_hJob = CreateJobObject(IntPtr.Zero, null);
|
||||
|
||||
var info = new JOBOBJECT_BASIC_LIMIT_INFORMATION
|
||||
{
|
||||
LimitFlags = JOBOBJECTLIMIT.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
|
||||
};
|
||||
|
||||
var extendedInfo = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION
|
||||
{
|
||||
BasicLimitInformation = info,
|
||||
};
|
||||
|
||||
int length = Marshal.SizeOf(typeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION));
|
||||
IntPtr ptr = Marshal.AllocHGlobal(length);
|
||||
Marshal.StructureToPtr(extendedInfo, ptr, false);
|
||||
|
||||
_ = SetInformationJobObject(
|
||||
_hJob,
|
||||
JobObjectInfoType.ExtendedLimitInformation,
|
||||
ptr,
|
||||
(uint)length);
|
||||
|
||||
Marshal.FreeHGlobal(ptr);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a process to the job object.
|
||||
/// </summary>
|
||||
public void AddProcess(IntPtr processHandle)
|
||||
{
|
||||
_ = AssignProcessToJobObject(_hJob, processHandle);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
if (_hJob != IntPtr.Zero)
|
||||
{
|
||||
_ = CloseHandle(_hJob);
|
||||
_hJob = IntPtr.Zero;
|
||||
}
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// P/Invoke
|
||||
// ====================================================================
|
||||
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
|
||||
private static extern IntPtr CreateJobObject(IntPtr jobAttributes, string name);
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
private static extern bool SetInformationJobObject(
|
||||
IntPtr job,
|
||||
JobObjectInfoType infoType,
|
||||
IntPtr jobObjectInfo,
|
||||
uint jobObjectInfoLength);
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
private static extern bool AssignProcessToJobObject(IntPtr job, IntPtr process);
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
private static extern bool CloseHandle(IntPtr handle);
|
||||
|
||||
private enum JobObjectInfoType
|
||||
{
|
||||
ExtendedLimitInformation = 9,
|
||||
}
|
||||
|
||||
[Flags]
|
||||
private enum JOBOBJECTLIMIT : uint
|
||||
{
|
||||
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000,
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct JOBOBJECT_BASIC_LIMIT_INFORMATION
|
||||
{
|
||||
public long PerProcessUserTimeLimit;
|
||||
public long PerJobUserTimeLimit;
|
||||
public JOBOBJECTLIMIT LimitFlags;
|
||||
public UIntPtr MinimumWorkingSetSize;
|
||||
public UIntPtr MaximumWorkingSetSize;
|
||||
public uint ActiveProcessLimit;
|
||||
public long Affinity;
|
||||
public uint PriorityClass;
|
||||
public uint SchedulingClass;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct IO_COUNTERS
|
||||
{
|
||||
public ulong ReadOperationCount;
|
||||
public ulong WriteOperationCount;
|
||||
public ulong OtherOperationCount;
|
||||
public ulong ReadTransferCount;
|
||||
public ulong WriteTransferCount;
|
||||
public ulong OtherTransferCount;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION
|
||||
{
|
||||
public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation;
|
||||
public IO_COUNTERS IoInfo;
|
||||
public UIntPtr ProcessMemoryLimit;
|
||||
public UIntPtr JobMemoryLimit;
|
||||
public UIntPtr PeakProcessMemoryUsed;
|
||||
public UIntPtr PeakJobMemoryUsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
</assembly>
|
||||
@@ -50,6 +50,7 @@ enum class ScheduleMode
|
||||
Off,
|
||||
FixedHours,
|
||||
SunsetToSunrise,
|
||||
FollowNightLight,
|
||||
// add more later
|
||||
};
|
||||
|
||||
@@ -61,6 +62,8 @@ inline std::wstring ToString(ScheduleMode mode)
|
||||
return L"SunsetToSunrise";
|
||||
case ScheduleMode::FixedHours:
|
||||
return L"FixedHours";
|
||||
case ScheduleMode::FollowNightLight:
|
||||
return L"FollowNightLight";
|
||||
default:
|
||||
return L"Off";
|
||||
}
|
||||
@@ -72,6 +75,8 @@ inline ScheduleMode FromString(const std::wstring& str)
|
||||
return ScheduleMode::SunsetToSunrise;
|
||||
if (str == L"FixedHours")
|
||||
return ScheduleMode::FixedHours;
|
||||
if (str == L"FollowNightLight")
|
||||
return ScheduleMode::FollowNightLight;
|
||||
return ScheduleMode::Off;
|
||||
}
|
||||
|
||||
@@ -167,7 +172,9 @@ public:
|
||||
ToString(g_settings.m_scheduleMode),
|
||||
{ { L"Off", L"Disable the schedule" },
|
||||
{ L"FixedHours", L"Set hours manually" },
|
||||
{ L"SunsetToSunrise", L"Use sunrise/sunset times" } });
|
||||
{ L"SunsetToSunrise", L"Use sunrise/sunset times" },
|
||||
{ L"FollowNightLight", L"Follow Windows Night Light state" }
|
||||
});
|
||||
|
||||
// Integer spinners
|
||||
settings.add_int_spinner(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#include <windows.h>
|
||||
#include <windows.h>
|
||||
#include <tchar.h>
|
||||
#include "ThemeScheduler.h"
|
||||
#include "ThemeHelper.h"
|
||||
@@ -13,10 +13,12 @@
|
||||
#include <utils/logger_helper.h>
|
||||
#include "LightSwitchStateManager.h"
|
||||
#include <LightSwitchUtils.h>
|
||||
#include <NightLightRegistryObserver.h>
|
||||
|
||||
SERVICE_STATUS g_ServiceStatus = {};
|
||||
SERVICE_STATUS_HANDLE g_StatusHandle = nullptr;
|
||||
HANDLE g_ServiceStopEvent = nullptr;
|
||||
static LightSwitchStateManager* g_stateManagerPtr = nullptr;
|
||||
|
||||
VOID WINAPI ServiceMain(DWORD argc, LPTSTR* argv);
|
||||
VOID WINAPI ServiceCtrlHandler(DWORD dwCtrl);
|
||||
@@ -168,7 +170,15 @@ static void DetectAndHandleExternalThemeChange(LightSwitchStateManager& stateMan
|
||||
}
|
||||
|
||||
// Use shared helper (handles wraparound logic)
|
||||
bool shouldBeLight = ShouldBeLight(nowMinutes, effectiveLight, effectiveDark);
|
||||
bool shouldBeLight = false;
|
||||
if (s.scheduleMode == ScheduleMode::FollowNightLight)
|
||||
{
|
||||
shouldBeLight = !IsNightLightEnabled();
|
||||
}
|
||||
else
|
||||
{
|
||||
shouldBeLight = ShouldBeLight(nowMinutes, effectiveLight, effectiveDark);
|
||||
}
|
||||
|
||||
// Compare current system/apps theme
|
||||
bool currentSystemLight = GetCurrentSystemTheme();
|
||||
@@ -199,15 +209,40 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
|
||||
// Initialization
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
static LightSwitchStateManager stateManager;
|
||||
g_stateManagerPtr = &stateManager;
|
||||
|
||||
LightSwitchSettings::instance().InitFileWatcher();
|
||||
|
||||
HANDLE hManualOverride = OpenEventW(SYNCHRONIZE | EVENT_MODIFY_STATE, FALSE, L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE");
|
||||
HANDLE hSettingsChanged = LightSwitchSettings::instance().GetSettingsChangedEvent();
|
||||
|
||||
static std::unique_ptr<NightLightRegistryObserver> g_nightLightWatcher;
|
||||
|
||||
LightSwitchSettings::instance().LoadSettings();
|
||||
const auto& settings = LightSwitchSettings::instance().settings();
|
||||
|
||||
// after loading settings:
|
||||
bool nightLightNeeded = (settings.scheduleMode == ScheduleMode::FollowNightLight);
|
||||
|
||||
if (nightLightNeeded && !g_nightLightWatcher)
|
||||
{
|
||||
Logger::info(L"[LightSwitchService] Starting Night Light registry watcher...");
|
||||
|
||||
g_nightLightWatcher = std::make_unique<NightLightRegistryObserver>(
|
||||
HKEY_CURRENT_USER,
|
||||
NIGHT_LIGHT_REGISTRY_PATH,
|
||||
[]() {
|
||||
if (g_stateManagerPtr)
|
||||
g_stateManagerPtr->OnNightLightChange();
|
||||
});
|
||||
}
|
||||
else if (!nightLightNeeded && g_nightLightWatcher)
|
||||
{
|
||||
Logger::info(L"[LightSwitchService] Stopping Night Light registry watcher...");
|
||||
g_nightLightWatcher->Stop();
|
||||
g_nightLightWatcher.reset();
|
||||
}
|
||||
|
||||
SYSTEMTIME st;
|
||||
GetLocalTime(&st);
|
||||
int nowMinutes = st.wHour * 60 + st.wMinute;
|
||||
@@ -274,6 +309,31 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
|
||||
ResetEvent(hSettingsChanged);
|
||||
LightSwitchSettings::instance().LoadSettings();
|
||||
stateManager.OnSettingsChanged();
|
||||
|
||||
const auto& settings = LightSwitchSettings::instance().settings();
|
||||
bool nightLightNeeded = (settings.scheduleMode == ScheduleMode::FollowNightLight);
|
||||
|
||||
if (nightLightNeeded && !g_nightLightWatcher)
|
||||
{
|
||||
Logger::info(L"[LightSwitchService] Starting Night Light registry watcher...");
|
||||
|
||||
g_nightLightWatcher = std::make_unique<NightLightRegistryObserver>(
|
||||
HKEY_CURRENT_USER,
|
||||
NIGHT_LIGHT_REGISTRY_PATH,
|
||||
[]() {
|
||||
if (g_stateManagerPtr)
|
||||
g_stateManagerPtr->OnNightLightChange();
|
||||
});
|
||||
|
||||
stateManager.OnNightLightChange();
|
||||
}
|
||||
else if (!nightLightNeeded && g_nightLightWatcher)
|
||||
{
|
||||
Logger::info(L"[LightSwitchService] Stopping Night Light registry watcher...");
|
||||
g_nightLightWatcher->Stop();
|
||||
g_nightLightWatcher.reset();
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -285,6 +345,11 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
|
||||
CloseHandle(hManualOverride);
|
||||
if (hParent)
|
||||
CloseHandle(hParent);
|
||||
if (g_nightLightWatcher)
|
||||
{
|
||||
g_nightLightWatcher->Stop();
|
||||
g_nightLightWatcher.reset();
|
||||
}
|
||||
|
||||
Logger::info(L"[LightSwitchService] Worker thread exiting cleanly.");
|
||||
return 0;
|
||||
|
||||
@@ -76,6 +76,7 @@
|
||||
<ClCompile Include="LightSwitchService.cpp" />
|
||||
<ClCompile Include="LightSwitchSettings.cpp" />
|
||||
<ClCompile Include="LightSwitchStateManager.cpp" />
|
||||
<ClCompile Include="NightLightRegistryObserver.cpp" />
|
||||
<ClCompile Include="SettingsConstants.cpp" />
|
||||
<ClCompile Include="ThemeHelper.cpp" />
|
||||
<ClCompile Include="ThemeScheduler.cpp" />
|
||||
@@ -88,6 +89,7 @@
|
||||
<ClInclude Include="LightSwitchSettings.h" />
|
||||
<ClInclude Include="LightSwitchStateManager.h" />
|
||||
<ClInclude Include="LightSwitchUtils.h" />
|
||||
<ClInclude Include="NightLightRegistryObserver.h" />
|
||||
<ClInclude Include="SettingsConstants.h" />
|
||||
<ClInclude Include="SettingsObserver.h" />
|
||||
<ClInclude Include="ThemeHelper.h" />
|
||||
|
||||
@@ -36,6 +36,9 @@
|
||||
<ClCompile Include="LightSwitchStateManager.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="NightLightRegistryObserver.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="ThemeScheduler.h">
|
||||
@@ -62,6 +65,9 @@
|
||||
<ClInclude Include="LightSwitchUtils.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="NightLightRegistryObserver.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Natvis Include="$(MSBuildThisFileDirectory)..\..\natvis\wil.natvis" />
|
||||
|
||||
@@ -19,7 +19,8 @@ enum class ScheduleMode
|
||||
{
|
||||
Off,
|
||||
FixedHours,
|
||||
SunsetToSunrise
|
||||
SunsetToSunrise,
|
||||
FollowNightLight,
|
||||
// Add more in the future
|
||||
};
|
||||
|
||||
@@ -31,6 +32,8 @@ inline std::wstring ToString(ScheduleMode mode)
|
||||
return L"FixedHours";
|
||||
case ScheduleMode::SunsetToSunrise:
|
||||
return L"SunsetToSunrise";
|
||||
case ScheduleMode::FollowNightLight:
|
||||
return L"FollowNightLight";
|
||||
default:
|
||||
return L"Off";
|
||||
}
|
||||
@@ -42,6 +45,8 @@ inline ScheduleMode FromString(const std::wstring& str)
|
||||
return ScheduleMode::SunsetToSunrise;
|
||||
if (str == L"FixedHours")
|
||||
return ScheduleMode::FixedHours;
|
||||
if (str == L"FollowNightLight")
|
||||
return ScheduleMode::FollowNightLight;
|
||||
else
|
||||
return ScheduleMode::Off;
|
||||
}
|
||||
|
||||
@@ -31,7 +31,10 @@ void LightSwitchStateManager::OnSettingsChanged()
|
||||
void LightSwitchStateManager::OnTick(int currentMinutes)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_stateMutex);
|
||||
EvaluateAndApplyIfNeeded();
|
||||
if (_state.lastAppliedMode != ScheduleMode::FollowNightLight)
|
||||
{
|
||||
EvaluateAndApplyIfNeeded();
|
||||
}
|
||||
}
|
||||
|
||||
// Called when manual override is triggered
|
||||
@@ -49,8 +52,38 @@ void LightSwitchStateManager::OnManualOverride()
|
||||
_state.isAppsLightActive = GetCurrentAppsTheme();
|
||||
|
||||
Logger::debug(L"[LightSwitchStateManager] Synced internal theme state to current system theme ({}) and apps theme ({}).",
|
||||
(_state.isSystemLightActive ? L"light" : L"dark"),
|
||||
(_state.isAppsLightActive ? L"light" : L"dark"));
|
||||
(_state.isSystemLightActive ? L"light" : L"dark"),
|
||||
(_state.isAppsLightActive ? L"light" : L"dark"));
|
||||
}
|
||||
|
||||
EvaluateAndApplyIfNeeded();
|
||||
}
|
||||
|
||||
// Runs with the registry observer detects a change in Night Light settings.
|
||||
void LightSwitchStateManager::OnNightLightChange()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_stateMutex);
|
||||
|
||||
bool newNightLightState = IsNightLightEnabled();
|
||||
|
||||
// In Follow Night Light mode, treat a Night Light toggle as a boundary
|
||||
if (_state.lastAppliedMode == ScheduleMode::FollowNightLight && _state.isManualOverride)
|
||||
{
|
||||
Logger::info(L"[LightSwitchStateManager] Night Light changed while manual override active; "
|
||||
L"treating as a boundary and clearing manual override.");
|
||||
_state.isManualOverride = false;
|
||||
}
|
||||
|
||||
if (newNightLightState != _state.isNightLightActive)
|
||||
{
|
||||
Logger::info(L"[LightSwitchStateManager] Night Light toggled to {}",
|
||||
newNightLightState ? L"ON" : L"OFF");
|
||||
|
||||
_state.isNightLightActive = newNightLightState;
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::debug(L"[LightSwitchStateManager] Night Light change event fired, but no actual change.");
|
||||
}
|
||||
|
||||
EvaluateAndApplyIfNeeded();
|
||||
@@ -77,9 +110,9 @@ void LightSwitchStateManager::SyncInitialThemeState()
|
||||
_state.isSystemLightActive = GetCurrentSystemTheme();
|
||||
_state.isAppsLightActive = GetCurrentAppsTheme();
|
||||
Logger::debug(L"[LightSwitchStateManager] Synced initial state to current system theme ({})",
|
||||
_state.isSystemLightActive ? L"light" : L"dark");
|
||||
_state.isSystemLightActive ? L"light" : L"dark");
|
||||
Logger::debug(L"[LightSwitchStateManager] Synced initial state to current apps theme ({})",
|
||||
_state.isAppsLightActive ? L"light" : L"dark");
|
||||
_state.isAppsLightActive ? L"light" : L"dark");
|
||||
}
|
||||
|
||||
static std::pair<int, int> update_sun_times(auto& settings)
|
||||
@@ -194,7 +227,15 @@ void LightSwitchStateManager::EvaluateAndApplyIfNeeded()
|
||||
|
||||
_state.lastAppliedMode = _currentSettings.scheduleMode;
|
||||
|
||||
bool shouldBeLight = ShouldBeLight(now, _state.effectiveLightMinutes, _state.effectiveDarkMinutes);
|
||||
bool shouldBeLight = false;
|
||||
if (_currentSettings.scheduleMode == ScheduleMode::FollowNightLight)
|
||||
{
|
||||
shouldBeLight = !_state.isNightLightActive;
|
||||
}
|
||||
else
|
||||
{
|
||||
shouldBeLight = ShouldBeLight(now, _state.effectiveLightMinutes, _state.effectiveDarkMinutes);
|
||||
}
|
||||
|
||||
bool appsNeedsToChange = _currentSettings.changeApps && (_state.isAppsLightActive != shouldBeLight);
|
||||
bool systemNeedsToChange = _currentSettings.changeSystem && (_state.isSystemLightActive != shouldBeLight);
|
||||
@@ -227,6 +268,3 @@ void LightSwitchStateManager::EvaluateAndApplyIfNeeded()
|
||||
|
||||
_state.lastTickMinutes = now;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ struct LightSwitchState
|
||||
bool isManualOverride = false;
|
||||
bool isSystemLightActive = false;
|
||||
bool isAppsLightActive = false;
|
||||
bool isNightLightActive = false;
|
||||
int lastEvaluatedDay = -1;
|
||||
int lastTickMinutes = -1;
|
||||
|
||||
@@ -32,6 +33,9 @@ public:
|
||||
// Called when manual override is toggled (via shortcut or system change).
|
||||
void OnManualOverride();
|
||||
|
||||
// Called when night light changes in windows settings
|
||||
void OnNightLightChange();
|
||||
|
||||
// Initial sync at startup to align internal state with system theme
|
||||
void SyncInitialThemeState();
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
#include "NightLightRegistryObserver.h"
|
||||
@@ -0,0 +1,134 @@
|
||||
#pragma once
|
||||
#include <wtypes.h>
|
||||
#include <string>
|
||||
#include <functional>
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
|
||||
class NightLightRegistryObserver
|
||||
{
|
||||
public:
|
||||
NightLightRegistryObserver(HKEY root, const std::wstring& subkey, std::function<void()> callback) :
|
||||
_root(root), _subkey(subkey), _callback(std::move(callback)), _stop(false)
|
||||
{
|
||||
_thread = std::thread([this]() { this->Run(); });
|
||||
}
|
||||
|
||||
~NightLightRegistryObserver()
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
|
||||
void Stop()
|
||||
{
|
||||
_stop = true;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
if (_event)
|
||||
SetEvent(_event);
|
||||
}
|
||||
|
||||
if (_thread.joinable())
|
||||
_thread.join();
|
||||
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
if (_hKey)
|
||||
{
|
||||
RegCloseKey(_hKey);
|
||||
_hKey = nullptr;
|
||||
}
|
||||
|
||||
if (_event)
|
||||
{
|
||||
CloseHandle(_event);
|
||||
_event = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private:
|
||||
void Run()
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
if (RegOpenKeyExW(_root, _subkey.c_str(), 0, KEY_NOTIFY, &_hKey) != ERROR_SUCCESS)
|
||||
return;
|
||||
|
||||
_event = CreateEventW(nullptr, TRUE, FALSE, nullptr);
|
||||
if (!_event)
|
||||
{
|
||||
RegCloseKey(_hKey);
|
||||
_hKey = nullptr;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
while (!_stop)
|
||||
{
|
||||
HKEY hKeyLocal = nullptr;
|
||||
HANDLE eventLocal = nullptr;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
if (_stop)
|
||||
break;
|
||||
|
||||
hKeyLocal = _hKey;
|
||||
eventLocal = _event;
|
||||
}
|
||||
|
||||
if (!hKeyLocal || !eventLocal)
|
||||
break;
|
||||
|
||||
if (_stop)
|
||||
break;
|
||||
|
||||
if (RegNotifyChangeKeyValue(hKeyLocal, FALSE, REG_NOTIFY_CHANGE_LAST_SET, eventLocal, TRUE) != ERROR_SUCCESS)
|
||||
break;
|
||||
|
||||
DWORD wait = WaitForSingleObject(eventLocal, INFINITE);
|
||||
if (_stop || wait == WAIT_FAILED)
|
||||
break;
|
||||
|
||||
ResetEvent(eventLocal);
|
||||
|
||||
if (!_stop && _callback)
|
||||
{
|
||||
try
|
||||
{
|
||||
_callback();
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
if (_hKey)
|
||||
{
|
||||
RegCloseKey(_hKey);
|
||||
_hKey = nullptr;
|
||||
}
|
||||
|
||||
if (_event)
|
||||
{
|
||||
CloseHandle(_event);
|
||||
_event = nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
HKEY _root;
|
||||
std::wstring _subkey;
|
||||
std::function<void()> _callback;
|
||||
HANDLE _event = nullptr;
|
||||
HKEY _hKey = nullptr;
|
||||
std::thread _thread;
|
||||
std::atomic<bool> _stop;
|
||||
std::mutex _mutex;
|
||||
};
|
||||
@@ -11,4 +11,7 @@ enum class SettingId
|
||||
Sunset_Offset,
|
||||
ChangeSystem,
|
||||
ChangeApps
|
||||
};
|
||||
};
|
||||
|
||||
constexpr wchar_t PERSONALIZATION_REGISTRY_PATH[] = L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize";
|
||||
constexpr wchar_t NIGHT_LIGHT_REGISTRY_PATH[] = L"Software\\Microsoft\\Windows\\CurrentVersion\\CloudStore\\Store\\DefaultAccount\\Current\\default$windows.data.bluelightreduction.bluelightreductionstate\\windows.data.bluelightreduction.bluelightreductionstate";
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <logger/logger.h>
|
||||
#include <utils/logger_helper.h>
|
||||
#include "ThemeHelper.h"
|
||||
#include <SettingsConstants.h>
|
||||
|
||||
// Controls changing the themes.
|
||||
|
||||
@@ -10,7 +11,7 @@ static void ResetColorPrevalence()
|
||||
{
|
||||
HKEY hKey;
|
||||
if (RegOpenKeyEx(HKEY_CURRENT_USER,
|
||||
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
|
||||
PERSONALIZATION_REGISTRY_PATH,
|
||||
0,
|
||||
KEY_SET_VALUE,
|
||||
&hKey) == ERROR_SUCCESS)
|
||||
@@ -31,7 +32,7 @@ void SetAppsTheme(bool mode)
|
||||
{
|
||||
HKEY hKey;
|
||||
if (RegOpenKeyEx(HKEY_CURRENT_USER,
|
||||
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
|
||||
PERSONALIZATION_REGISTRY_PATH,
|
||||
0,
|
||||
KEY_SET_VALUE,
|
||||
&hKey) == ERROR_SUCCESS)
|
||||
@@ -50,7 +51,7 @@ void SetSystemTheme(bool mode)
|
||||
{
|
||||
HKEY hKey;
|
||||
if (RegOpenKeyEx(HKEY_CURRENT_USER,
|
||||
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
|
||||
PERSONALIZATION_REGISTRY_PATH,
|
||||
0,
|
||||
KEY_SET_VALUE,
|
||||
&hKey) == ERROR_SUCCESS)
|
||||
@@ -79,7 +80,7 @@ bool GetCurrentSystemTheme()
|
||||
DWORD size = sizeof(value);
|
||||
|
||||
if (RegOpenKeyEx(HKEY_CURRENT_USER,
|
||||
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
|
||||
PERSONALIZATION_REGISTRY_PATH,
|
||||
0,
|
||||
KEY_READ,
|
||||
&hKey) == ERROR_SUCCESS)
|
||||
@@ -98,7 +99,7 @@ bool GetCurrentAppsTheme()
|
||||
DWORD size = sizeof(value);
|
||||
|
||||
if (RegOpenKeyEx(HKEY_CURRENT_USER,
|
||||
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
|
||||
PERSONALIZATION_REGISTRY_PATH,
|
||||
0,
|
||||
KEY_READ,
|
||||
&hKey) == ERROR_SUCCESS)
|
||||
@@ -109,3 +110,30 @@ bool GetCurrentAppsTheme()
|
||||
|
||||
return value == 1; // true = light, false = dark
|
||||
}
|
||||
|
||||
bool IsNightLightEnabled()
|
||||
{
|
||||
HKEY hKey;
|
||||
const wchar_t* path = NIGHT_LIGHT_REGISTRY_PATH;
|
||||
|
||||
if (RegOpenKeyExW(HKEY_CURRENT_USER, path, 0, KEY_READ, &hKey) != ERROR_SUCCESS)
|
||||
return false;
|
||||
|
||||
// RegGetValueW will set size to the size of the data and we expect that to be at least 25 bytes (we need to access bytes 23 and 24)
|
||||
DWORD size = 0;
|
||||
if (RegGetValueW(hKey, nullptr, L"Data", RRF_RT_REG_BINARY, nullptr, nullptr, &size) != ERROR_SUCCESS || size < 25)
|
||||
{
|
||||
RegCloseKey(hKey);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<BYTE> data(size);
|
||||
if (RegGetValueW(hKey, nullptr, L"Data", RRF_RT_REG_BINARY, nullptr, data.data(), &size) != ERROR_SUCCESS)
|
||||
{
|
||||
RegCloseKey(hKey);
|
||||
return false;
|
||||
}
|
||||
|
||||
RegCloseKey(hKey);
|
||||
return data[23] == 0x10 && data[24] == 0x00;
|
||||
}
|
||||
@@ -3,3 +3,4 @@ void SetSystemTheme(bool dark);
|
||||
void SetAppsTheme(bool dark);
|
||||
bool GetCurrentSystemTheme();
|
||||
bool GetCurrentAppsTheme();
|
||||
bool IsNightLightEnabled();
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Microsoft.Windows.CppWinRT" version="2.0.240111.5" targetFramework="native" />
|
||||
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.231216.1" targetFramework="native" />
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup Label="NuGet">
|
||||
<!-- Tell NuGet this is PackageReference style -->
|
||||
<RestoreProjectStyle>PackageReference</RestoreProjectStyle>
|
||||
|
||||
<!-- Tell NuGet we're a native project -->
|
||||
<NuGetTargetMoniker>native,Version=v0.0</NuGetTargetMoniker>
|
||||
|
||||
<!-- Tell NuGet we target Windows (use your existing WindowsTargetPlatformVersion) -->
|
||||
<NuGetTargetPlatformIdentifier>Windows</NuGetTargetPlatformIdentifier>
|
||||
<NuGetTargetPlatformVersion>$(WindowsTargetPlatformVersion)</NuGetTargetPlatformVersion>
|
||||
</PropertyGroup>
|
||||
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.1.8.250907003\build\native\Microsoft.WindowsAppSDK.props" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.8.250907003\build\native\Microsoft.WindowsAppSDK.props')" />
|
||||
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.Base.1.8.250831001\build\native\Microsoft.WindowsAppSDK.Base.props" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.Base.1.8.250831001\build\native\Microsoft.WindowsAppSDK.Base.props')" />
|
||||
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.Foundation.1.8.250906002\build\native\Microsoft.WindowsAppSDK.Foundation.props" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.Foundation.1.8.250906002\build\native\Microsoft.WindowsAppSDK.Foundation.props')" />
|
||||
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.WinUI.1.8.250906003\build\native\Microsoft.WindowsAppSDK.WinUI.props" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.WinUI.1.8.250906003\build\native\Microsoft.WindowsAppSDK.WinUI.props')" />
|
||||
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.Runtime.1.8.250907003\build\native\Microsoft.WindowsAppSDK.Runtime.props" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.Runtime.1.8.250907003\build\native\Microsoft.WindowsAppSDK.Runtime.props')" />
|
||||
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.DWrite.1.8.25090401\build\Microsoft.WindowsAppSDK.DWrite.props" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.DWrite.1.8.25090401\build\Microsoft.WindowsAppSDK.DWrite.props')" />
|
||||
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.InteractiveExperiences.1.8.250906004\build\native\Microsoft.WindowsAppSDK.InteractiveExperiences.props" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.InteractiveExperiences.1.8.250906004\build\native\Microsoft.WindowsAppSDK.InteractiveExperiences.props')" />
|
||||
<Import Project="..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.26100.4188\build\Microsoft.Windows.SDK.BuildTools.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.26100.4188\build\Microsoft.Windows.SDK.BuildTools.props')" />
|
||||
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" />
|
||||
<PropertyGroup Label="Globals">
|
||||
<CppWinRTOptimized>true</CppWinRTOptimized>
|
||||
<CppWinRTRootNamespaceAutoMerge>true</CppWinRTRootNamespaceAutoMerge>
|
||||
@@ -33,11 +31,6 @@
|
||||
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
|
||||
<EnablePreviewMsixTooling>true</EnablePreviewMsixTooling>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" GeneratePathProperty="true" />
|
||||
<PackageReference Include="Microsoft.Windows.CppWinRT" GeneratePathProperty="true" />
|
||||
<PackageReference Include="Microsoft.Windows.ImplementationLibrary" GeneratePathProperty="true" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Label="Configuration">
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
@@ -45,6 +38,7 @@
|
||||
<DesktopCompatible>true</DesktopCompatible>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<Import Project="..\..\..\..\deps\spdlog.props" />
|
||||
<ImportGroup Label="ExtensionSettings">
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets">
|
||||
@@ -124,6 +118,9 @@
|
||||
<WarnAsError>true</WarnAsError>
|
||||
</Midl>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\common\Display\Display.vcxproj">
|
||||
<Project>{caba8dfb-823b-4bf2-93ac-3f31984150d9}</Project>
|
||||
@@ -145,5 +142,42 @@
|
||||
<ResourceCompile Include="PowerToys.MeasureToolCore.rc" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<Import Project="..\..\..\..\deps\spdlog.props" />
|
||||
<ImportGroup Label="ExtensionTargets">
|
||||
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" />
|
||||
<Import Project="..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" />
|
||||
<Import Project="..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.26100.4188\build\Microsoft.Windows.SDK.BuildTools.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.26100.4188\build\Microsoft.Windows.SDK.BuildTools.targets')" />
|
||||
<Import Project="..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets')" />
|
||||
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.1.8.250907003\build\native\Microsoft.WindowsAppSDK.targets" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.8.250907003\build\native\Microsoft.WindowsAppSDK.targets')" />
|
||||
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.Base.1.8.250831001\build\native\Microsoft.WindowsAppSDK.Base.targets" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.Base.1.8.250831001\build\native\Microsoft.WindowsAppSDK.Base.targets')" />
|
||||
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.Foundation.1.8.250906002\build\native\Microsoft.WindowsAppSDK.Foundation.targets" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.Foundation.1.8.250906002\build\native\Microsoft.WindowsAppSDK.Foundation.targets')" />
|
||||
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.WinUI.1.8.250906003\build\native\Microsoft.WindowsAppSDK.WinUI.targets" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.WinUI.1.8.250906003\build\native\Microsoft.WindowsAppSDK.WinUI.targets')" />
|
||||
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.Runtime.1.8.250907003\build\native\Microsoft.WindowsAppSDK.Runtime.targets" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.Runtime.1.8.250907003\build\native\Microsoft.WindowsAppSDK.Runtime.targets')" />
|
||||
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.DWrite.1.8.25090401\build\Microsoft.WindowsAppSDK.DWrite.targets" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.DWrite.1.8.25090401\build\Microsoft.WindowsAppSDK.DWrite.targets')" />
|
||||
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.InteractiveExperiences.1.8.250906004\build\native\Microsoft.WindowsAppSDK.InteractiveExperiences.targets" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.InteractiveExperiences.1.8.250906004\build\native\Microsoft.WindowsAppSDK.InteractiveExperiences.targets')" />
|
||||
</ImportGroup>
|
||||
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
|
||||
<PropertyGroup>
|
||||
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
|
||||
</PropertyGroup>
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props'))" />
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets'))" />
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets'))" />
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.26100.4188\build\Microsoft.Windows.SDK.BuildTools.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.26100.4188\build\Microsoft.Windows.SDK.BuildTools.props'))" />
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.26100.4188\build\Microsoft.Windows.SDK.BuildTools.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.26100.4188\build\Microsoft.Windows.SDK.BuildTools.targets'))" />
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets'))" />
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.8.250907003\build\native\Microsoft.WindowsAppSDK.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.1.8.250907003\build\native\Microsoft.WindowsAppSDK.props'))" />
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.8.250907003\build\native\Microsoft.WindowsAppSDK.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.1.8.250907003\build\native\Microsoft.WindowsAppSDK.targets'))" />
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.Base.1.8.250831001\build\native\Microsoft.WindowsAppSDK.Base.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.Base.1.8.250831001\build\native\Microsoft.WindowsAppSDK.Base.props'))" />
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.Base.1.8.250831001\build\native\Microsoft.WindowsAppSDK.Base.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.Base.1.8.250831001\build\native\Microsoft.WindowsAppSDK.Base.targets'))" />
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.Foundation.1.8.250906002\build\native\Microsoft.WindowsAppSDK.Foundation.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.Foundation.1.8.250906002\build\native\Microsoft.WindowsAppSDK.Foundation.props'))" />
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.Foundation.1.8.250906002\build\native\Microsoft.WindowsAppSDK.Foundation.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.Foundation.1.8.250906002\build\native\Microsoft.WindowsAppSDK.Foundation.targets'))" />
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.WinUI.1.8.250906003\build\native\Microsoft.WindowsAppSDK.WinUI.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.WinUI.1.8.250906003\build\native\Microsoft.WindowsAppSDK.WinUI.props'))" />
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.WinUI.1.8.250906003\build\native\Microsoft.WindowsAppSDK.WinUI.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.WinUI.1.8.250906003\build\native\Microsoft.WindowsAppSDK.WinUI.targets'))" />
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.Runtime.1.8.250907003\build\native\Microsoft.WindowsAppSDK.Runtime.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.Runtime.1.8.250907003\build\native\Microsoft.WindowsAppSDK.Runtime.props'))" />
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.Runtime.1.8.250907003\build\native\Microsoft.WindowsAppSDK.Runtime.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.Runtime.1.8.250907003\build\native\Microsoft.WindowsAppSDK.Runtime.targets'))" />
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.DWrite.1.8.25090401\build\Microsoft.WindowsAppSDK.DWrite.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.DWrite.1.8.25090401\build\Microsoft.WindowsAppSDK.DWrite.props'))" />
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.DWrite.1.8.25090401\build\Microsoft.WindowsAppSDK.DWrite.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.DWrite.1.8.25090401\build\Microsoft.WindowsAppSDK.DWrite.targets'))" />
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.InteractiveExperiences.1.8.250906004\build\native\Microsoft.WindowsAppSDK.InteractiveExperiences.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.InteractiveExperiences.1.8.250906004\build\native\Microsoft.WindowsAppSDK.InteractiveExperiences.props'))" />
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.InteractiveExperiences.1.8.250906004\build\native\Microsoft.WindowsAppSDK.InteractiveExperiences.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.InteractiveExperiences.1.8.250906004\build\native\Microsoft.WindowsAppSDK.InteractiveExperiences.targets'))" />
|
||||
</Target>
|
||||
</Project>
|
||||
17
src/modules/MeasureTool/MeasureToolCore/packages.config
Normal file
17
src/modules/MeasureTool/MeasureToolCore/packages.config
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Microsoft.Web.WebView2" version="1.0.2903.40" targetFramework="native" />
|
||||
<package id="Microsoft.Windows.CppWinRT" version="2.0.240111.5" targetFramework="native" />
|
||||
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.231216.1" targetFramework="native" />
|
||||
<package id="Microsoft.Windows.SDK.BuildTools" version="10.0.26100.4188" targetFramework="native" />
|
||||
<package id="Microsoft.WindowsAppSDK" version="1.8.250907003" targetFramework="native" />
|
||||
<package id="Microsoft.WindowsAppSDK.Base" version="1.8.250831001" targetFramework="native" />
|
||||
<package id="Microsoft.WindowsAppSDK.Foundation" version="1.8.250906002" targetFramework="native" />
|
||||
<package id="Microsoft.WindowsAppSDK.WinUI" version="1.8.250906003" targetFramework="native" />
|
||||
<package id="Microsoft.WindowsAppSDK.Runtime" version="1.8.250907003" targetFramework="native" />
|
||||
<package id="Microsoft.WindowsAppSDK.DWrite" version="1.8.25090401" targetFramework="native" />
|
||||
<package id="Microsoft.WindowsAppSDK.InteractiveExperiences" version="1.8.250906004" targetFramework="native" />
|
||||
<package id="Microsoft.WindowsAppSDK.Widgets" version="1.8.250904007" targetFramework="native" />
|
||||
<package id="Microsoft.WindowsAppSDK.AI" version="1.8.37" targetFramework="native" />
|
||||
<package id="Microsoft.Windows.SDK.BuildTools.MSIX" version="1.7.20250829.1" targetFramework="native" />
|
||||
</packages>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user