Merge remote-tracking branch 'microsoft/main' into dev/feature/projects

This commit is contained in:
seraphima 2024-07-15 15:43:21 +02:00
commit 12308babc7
103 changed files with 2588 additions and 1729 deletions

View File

@ -9,7 +9,7 @@
]
},
"xamlstyler.console": {
"version": "3.2206.4",
"version": "3.2404.2",
"commands": [
"xstyler"
]

View File

@ -221,3 +221,8 @@ artanh
arsinh
arcosh
# Linux
dbus
anypass
gpg

View File

@ -97,6 +97,7 @@ AUTOUPDATE
AValid
awakeness
AWAYMODE
azcliversion
azman
backtracer
bbwe
@ -121,6 +122,7 @@ BLURREGION
bmi
bms
BNumber
BODGY
BOKMAL
bootstrapper
BOOTSTRAPPERINSTALLFOLDER
@ -167,6 +169,7 @@ CENTERALIGN
ceq
certlm
certmgr
cfp
cguid
CHANGECBCHAIN
changecursor
@ -757,6 +760,7 @@ KEYEVENTF
KEYIMAGE
keynum
keyremaps
keyvault
KILLFOCUS
killrunner
Knownfolders
@ -1347,6 +1351,9 @@ RRF
rrr
rsop
Rsp
rstringalnum
rstringalpha
rstringdigit
Rstrtmgr
RTB
RTLREADING
@ -1361,6 +1368,7 @@ runtimeclass
runtimeobject
runtimepack
runtimes
ruuid
rvm
rwin
rwl

View File

@ -5,56 +5,80 @@ on:
release:
types: [published]
permissions:
id-token: write
jobs:
microsoft_store:
name: Publish Microsoft Store
environment: store
runs-on: ubuntu-latest
steps:
- name: BODGY - Set up Gnome Keyring for future Cert Auth
run: |-
sudo apt-get install -y gnome-keyring
export $(dbus-launch --sh-syntax)
export $(echo 'anypass_just_to_unlock' | gnome-keyring-daemon --unlock)
export $(echo 'anypass_just_to_unlock' | gnome-keyring-daemon --start --components=gpg,pkcs11,secrets,ssh)
- name: Log in to Azure
uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
enable-AzPSSession: true
- name: Get latest URL from public releases
id: releaseVars
run: |
release=$(curl https://api.github.com/repos/Microsoft/PowerToys/releases | jq '[.[]|select(.name | contains("Release"))][0]')
assets=$(jq -n "$release" | jq '.assets')
powerToysSetup=$(jq -n "$assets" | jq '[.[]|select(.name | contains("PowerToysUserSetup"))]')
echo ::set-output name=powerToysInstallerX64Url::$(jq -n "$powerToysSetup" | jq -r '[.[]|select(.name | contains("x64"))][0].browser_download_url')
echo ::set-output name=powerToysInstallerArm64Url::$(jq -n "$powerToysSetup" | jq -r '[.[]|select(.name | contains("arm64"))][0].browser_download_url')
echo powerToysInstallerX64Url=$(jq -n "$powerToysSetup" | jq -r '[.[]|select(.name | contains("x64"))][0].browser_download_url') >> $GITHUB_OUTPUT
echo powerToysInstallerArm64Url=$(jq -n "$powerToysSetup" | jq -r '[.[]|select(.name | contains("arm64"))][0].browser_download_url') >> $GITHUB_OUTPUT
- uses: microsoft/setup-msstore-cli@v1
- name: Fetch Store Credential
uses: azure/cli@v2
with:
azcliversion: latest
inlineScript: |-
az keyvault secret download --vault-name ${{ secrets.AZURE_KEYVAULT_NAME }} -n ${{ secrets.AZURE_AUTH_CERT_NAME }} -f cert.pfx.b64
base64 -d < cert.pfx.b64 > cert.pfx
- name: Configure Store Credentials
uses: microsoft/store-submission@v1
with:
command: configure
type: win32
seller-id: ${{ secrets.SELLER_ID }}
product-id: ${{ secrets.PRODUCT_ID }}
tenant-id: ${{ secrets.TENANT_ID }}
client-id: ${{ secrets.CLIENT_ID }}
client-secret: ${{ secrets.CLIENT_SECRET }}
run: |-
msstore reconfigure -cfp cert.pfx -c ${{ secrets.AZURE_CLIENT_ID }} -t ${{ secrets.AZURE_TENANT_ID }} -s ${{ secrets.SELLER_ID }}
- name: Update draft submission
uses: microsoft/store-submission@v1
with:
command: update
product-update: '{
"packages":[
{
"packageUrl":"${{ steps.releaseVars.outputs.powerToysInstallerX64Url }}",
"languages":["zh-hans", "zh-hant", "en", "cs", "nl", "fr", "pt", "pt-br", "de", "hu", "it", "ja", "ko", "pl", "ru", "es", "tr"],
"architectures":["X64"],
"installerParameters":"/quiet /norestart",
"isSilentInstall":true
},
{
"packageUrl":"${{ steps.releaseVars.outputs.powerToysInstallerArm64Url }}",
"languages":["zh-hans", "zh-hant", "en", "cs", "nl", "fr", "pt", "pt-br", "de", "hu", "it", "ja", "ko", "pl", "ru", "es", "tr"],
"architectures":["Arm64"],
"installerParameters":"/quiet /norestart",
"isSilentInstall":true
}
]
}'
run: |-
msstore submission update ${{ secrets.PRODUCT_ID }} '{
"packages":[
{
"packageUrl":"${{ steps.releaseVars.outputs.powerToysInstallerX64Url }}",
"languages":["zh-hans", "zh-hant", "en", "cs", "nl", "fr", "pt", "pt-br", "de", "hu", "it", "ja", "ko", "pl", "ru", "es", "tr"],
"architectures":["X64"],
"installerParameters":"/quiet /norestart",
"isSilentInstall":true
},
{
"packageUrl":"${{ steps.releaseVars.outputs.powerToysInstallerArm64Url }}",
"languages":["zh-hans", "zh-hant", "en", "cs", "nl", "fr", "pt", "pt-br", "de", "hu", "it", "ja", "ko", "pl", "ru", "es", "tr"],
"architectures":["Arm64"],
"installerParameters":"/quiet /norestart",
"isSilentInstall":true
}
]
}'
- name: Publish Submission
uses: microsoft/store-submission@v1
with:
command: publish
run: |-
msstore submission publish ${{ secrets.PRODUCT_ID }}
- name: Clean up auth certificate
if: always()
run: |-
rm -f cert.pfx cert.pfx.b64

View File

@ -41,6 +41,6 @@ jobs:
platform: arm64
${{ if eq(variables['System.PullRequest.IsFork'], 'False') }}:
enableCaching: true
- template: ./templates/run-ui-tests-ci.yml
parameters:
platform: x64
# - template: ./templates/run-ui-tests-ci.yml
# parameters:
# platform: x64

View File

@ -24,10 +24,10 @@ jobs:
NODE_OPTIONS: --max_old_space_size=16384
pool:
demands: ImageOverride -equals SHINE-VS17-Latest
${{ if eq(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}:
name: SHINE-OSS-L
${{ if ne(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}:
${{ if eq(variables['System.CollectionId'], 'cb55739e-4afe-46a3-970f-1b49d8ee7564') }}:
name: SHINE-INT-L
${{ else }}:
name: SHINE-OSS-L
timeoutInMinutes: 120
strategy:
maxParallel: 10

View File

@ -3,10 +3,10 @@ jobs:
- job: Precheck
pool:
demands: ImageOverride -equals SHINE-VS17-Latest
${{ if eq(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}:
name: SHINE-OSS-L
${{ if ne(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}:
${{ if eq(variables['System.CollectionId'], 'cb55739e-4afe-46a3-970f-1b49d8ee7564') }}:
name: SHINE-INT-L
${{ else }}:
name: SHINE-OSS-L
steps:
- checkout: none

View File

@ -9,10 +9,10 @@ jobs:
variables:
SrcPath: $(Build.Repository.LocalPath)
pool:
${{ if eq(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}:
name: SHINE-OSS-Testing-x64
${{ if ne(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}:
${{ if eq(variables['System.CollectionId'], 'cb55739e-4afe-46a3-970f-1b49d8ee7564') }}:
name: SHINE-INT-Testing-x64
${{ else }}:
name: SHINE-OSS-Testing-x64
steps:
- checkout: self
fetchDepth: 1

View File

@ -43,7 +43,7 @@ steps:
inputs:
solution: "**/installer/PowerToysSetup.sln"
vsVersion: 17.0
msbuildArgs: /p:CIBuild=true /target:PowerToysInstaller /bl:$(Build.SourcesDirectory)\msbuild.binlog /p:RunBuildEvents=false /p:PerUser=${{parameters.perUserArg}}
msbuildArgs: /p:CIBuild=true /p:BuildProjectReferences=false /target:PowerToysInstaller /bl:$(Build.SourcesDirectory)\msbuild.binlog /p:RunBuildEvents=false /p:PerUser=${{parameters.perUserArg}}
platform: $(BuildPlatform)
configuration: $(BuildConfiguration)
clean: false # don't undo our hard work above by deleting the CustomActions dll

View File

@ -76,7 +76,7 @@ extends:
NODE_OPTIONS: --max_old_space_size=16384
IsPipeline: 1 # The installer uses this to detect whether it should pick up localizations
SkipCppCodeAnalysis: 1 # Skip the code analysis to speed up release CI. It runs on PR CI, anyway
IsExperimentationLive: 1 # The build and installer use this to turn on experimentation
# IsExperimentationLive: 1 # The build and installer use this to turn on experimentation
steps:
- checkout: self
clean: true

View File

@ -85,7 +85,7 @@
<UsePrecompiledHeaders Condition="'$(TF_BUILD)' != ''">false</UsePrecompiledHeaders>
<!-- Change this to bust the cache -->
<MSBuildCacheCacheUniverse Condition="'$(MSBuildCacheCacheUniverse)' == ''">202310210737</MSBuildCacheCacheUniverse>
<MSBuildCacheCacheUniverse Condition="'$(MSBuildCacheCacheUniverse)' == ''">202407100737</MSBuildCacheCacheUniverse>
<!--
Visual Studio telemetry reads various ApplicationInsights.config files and other files after the project is finished, likely in a detached process.

View File

@ -86,7 +86,7 @@
<PackageVersion Include="System.ServiceProcess.ServiceController" Version="8.0.0" />
<PackageVersion Include="System.Text.Encoding.CodePages" Version="8.0.0" />
<PackageVersion Include="UnicodeInformation" Version="2.6.0" />
<PackageVersion Include="UnitsNet" Version="4.145.0" />
<PackageVersion Include="UnitsNet" Version="5.50.0" />
<PackageVersion Include="UTF.Unknown" Version="2.5.1" />
<PackageVersion Include="Vanara.PInvoke.User32" Version="3.4.11" />
<PackageVersion Include="Vanara.PInvoke.Shell32" Version="3.4.11" />

View File

@ -1365,7 +1365,7 @@ EXHIBIT A -Mozilla Public License.
- System.ServiceProcess.ServiceController 8.0.0
- System.Text.Encoding.CodePages 8.0.0
- UnicodeInformation 2.6.0
- UnitsNet 4.145.0
- UnitsNet 5.50.0
- UTF.Unknown 2.5.1
- Vanara.PInvoke.Shell32 3.4.11
- Vanara.PInvoke.User32 3.4.11

195
README.md
View File

@ -41,19 +41,19 @@ Microsoft PowerToys is a set of utilities for power users to tune and streamline
Go to the [Microsoft PowerToys GitHub releases page][github-release-link] and click on `Assets` at the bottom to show the files available in the release. Please use the appropriate PowerToys installer that matches your machine's architecture and install scope. For most, it is `x64` and per-user.
<!-- items that need to be updated release to release -->
[github-next-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.82%22
[github-current-release-work]: https://github.com/microsoft/PowerToys/issues?q=project%3Amicrosoft%2FPowerToys%2F54
[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.81.0/PowerToysUserSetup-0.81.0-x64.exe
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.81.0/PowerToysUserSetup-0.81.0-arm64.exe
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.81.0/PowerToysSetup-0.81.0-x64.exe
[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.81.0/PowerToysSetup-0.81.0-arm64.exe
[github-next-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.83%22
[github-current-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.82%22
[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/V0.82.1/PowerToysUserSetup-0.82.1-x64.exe
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/V0.82.1/PowerToysUserSetup-0.82.1-arm64.exe
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/V0.82.1/PowerToysSetup-0.82.1-x64.exe
[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/V0.82.1/PowerToysSetup-0.82.1-arm64.exe
| Description | Filename | sha256 hash |
|----------------|----------|-------------|
| Per user - x64 | [PowerToysUserSetup-0.81.0-x64.exe][ptUserX64] | E62B1EE81954A75355C04E7567B1C9AAD6034AA0C61AD22587F8746D0DC488C8 |
| Per user - ARM64 | [PowerToysUserSetup-0.81.0-arm64.exe][ptUserArm64] | 75330A2DB4F9EF9B548B3B58F8BF3262C8C67E680042639BBBBC87EA244F24E2 |
| Machine wide - x64 | [PowerToysSetup-0.81.0-x64.exe][ptMachineX64] | 29F151B01FE3C94D4FD75F2D6E8F09A6C0F0962385B83A5A733F6717312F639D |
| Machine wide - ARM64 | [PowerToysSetup-0.81.0-arm64.exe][ptMachineArm64] | FCE636220E1FB854771258D9558E07B7532728AD4C722A7920338DEE60DEECF7 |
| Per user - x64 | [PowerToysUserSetup-0.82.1-x64.exe][ptUserX64] | B594C9A32125079186DCE776431E2DC77B896774D2AEE2ACF51BAB8791683485 |
| Per user - ARM64 | [PowerToysUserSetup-0.82.1-arm64.exe][ptUserArm64] | 41C1D9C0E8FA7EFFCE6F605C92C143AE933F8C999A2933A4D9D1115B16F14F67 |
| Machine wide - x64 | [PowerToysSetup-0.82.1-x64.exe][ptMachineX64] | B8FA7E7C8F88B69E070E234F561D32807634E2E9D782EDBB9DC35F3A454F2264 |
| Machine wide - ARM64 | [PowerToysSetup-0.82.1-arm64.exe][ptMachineArm64] | 58F22306F22CF9878C6DDE6AC128388DF4DFF78B76165E38A695490E55B3C8C4 |
This is our preferred method.
@ -99,155 +99,134 @@ For guidance on developing for PowerToys, please read the [developer docs](/doc/
Our [prioritized roadmap][roadmap] of features and utilities that the core team is focusing on.
### 0.81 - Build 2024 Update
### 0.82 - June 2024 Update
In this release, we focused on new features, stability and improvements.
In this release, we focused on stability and improvements.
**Highlights**
- New utility: Advanced Paste - This is an evolution based on feedback of the Paste As Plain Text utility to do more. It can paste as plain text, markdown, or json directly with the new UX or with a direct keystroke invoke. These are fully locally executed. In addition, it now has an AI powered option as well if you wish with the free form text box. The AI feature is 100% opt-in and requires an Open AI key. This new system will allow us to have more freedom in the future to quickly add in new features like pasting an image directly to a file or handle additional meta data types past just text.
- Thanks [@craigloewen-msft](https://github.com/craigloewen-msft) for the core functionality and [@niels9001](https://github.com/niels9001) for the UI/UX design!
- Command Not Found now uses the PowerShell Gallery release and now supports ARM64. Thanks [@carlos-zamora](https://github.com/carlos-zamora)!
- Fixed most accessibility issues opened after the latest accessibility review.
- Refactored, packaged and released the main Environment Variables Editor, Hosts File Editor and Registry Preview utilities functionality as controls to be integrated into DevHome. Thanks [@dabhattimsft](https://github.com/dabhattimsft) for validating and integrating into DevHome!
### General
- Fixed crashes on older CPUS by updating .NET to 8.0.4. (This was a hotfix for 0.80)
- New feature added to PowerRename to allow using sequences of random characters and UUIDs when renaming files. Thanks [@jhirvioja](https://github.com/jhirvioja)!
- Improvements in the Paste As JSON feature to better handle other CSV delimiters and converting from ini files. Thanks [@htcfreek](https://github.com/htcfreek)!
- Fixed UI issues that were reported after upgrading to WPF UI on Color Picker and PowerToys Run.
- Bug fixes and stability.
### Advanced Paste
- New utility: Advanced Paste - This is an evolution based on feedback of the Paste As Plain Text utility to do more. It can paste as plain text, markdown, or json directly with the new UX or with a direct keystroke invoke. These are fully locally executed. In addition, it now has an AI powered option as well if you wish with the free form text box. The AI feature is 100% opt-in and requires an Open AI key. This new system will allow us to have more freedom in the future to quickly add in new features like pasting an image directly to a file or handle additional meta data types past just text.
- Thanks [@craigloewen-msft](https://github.com/craigloewen-msft) for the core functionality and [@niels9001](https://github.com/niels9001) for the UI/UX design!
### AlwaysOnTop
- Enable border anti-aliasing. Thanks [@ewancg](https://github.com/ewancg)!
- Fixed an issue causing external applications triggering Advanced Paste. (This was a hotfix for 0.81)
- Added a GPO rule to disallow using online models in Advanced Paste. (This was a hotfix for 0.81)
- Improved CSV delimiter handling and plain text parsing for the Paste as JSON feature. Thanks [@htcfreek](https://github.com/htcfreek)!
- Added support to convert from ini in the Paste as JSON feature. Thanks [@htcfreek](https://github.com/htcfreek)!
- Fixed a memory leak caused by images not being properly cleaned out from clipboard history.
- Added an option to hide the UI when it loses focus. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
- Improved telemetry to get better data about token usage and if clipboard history is a popular feature. Thanks [@craigloewen-msft](https://github.com/craigloewen-msft)!
### Color Picker
- Improved accessibility by making the Settings and Copy to clipboard buttons focusable.
- Improved accessibility by supporting picking a color using the keyboard.
- Fixed the opaque background corners in the picker that were introduced after the upgrade to WPFUI.
### Command Not Found
### Developer Files Preview (Monaco)
- Upgraded the Command Not Found to use the new PowerShell Gallery release and support ARM64. Thanks [@carlos-zamora](https://github.com/carlos-zamora)!
- Improved the syntax highlight for .gitignore files. Thanks [@PesBandi](https://github.com/PesBandi)!
- Checking for the sticky scroll option in code behind was being done twice. Removed one of the checks. Thanks [@downarowiczd](https://github.com/downarowiczd)!
### Environment Variables Editor
- Refactored, packaged and released the main Environment Variables Editor functionality as a control to be integrated into DevHome. Thanks [@dabhattimsft](https://github.com/dabhattimsft) for validating and integrating into DevHome!
### FancyZones
- Fixed window wrap around behavior when overriding Windows key and arrow shortcuts on single monitor scenarios. Thanks [@DanRosenberry](https://github.com/DanRosenberry)!
- Improved accessibility of the editor by listing the keyboard shortcuts in the Canvas Editor.
- Added clarity to the UI section tooltips. Thanks [@anson-poon](https://github.com/anson-poon)!
### File Explorer add-ons
- Updated Monaco to 0.47 and added the new sticky scroll setting for DevFiles viewer. Thanks [@Aaron-Junker](https://github.com/Aaron-Junker)!
- Added the new font size setting for DevFiles viewer. Thanks [@Aaron-Junker](https://github.com/Aaron-Junker)!
- Added support for .srt (subtitle) file previewing in DevFiles viewer. Thanks [@PesBandi](https://github.com/PesBandi)!
- Fixed a crash when the preview handlers received a 64-bit handle from the OS. Thanks [@z4pf1sh](https://github.com/z4pf1sh)!
- Fixed a crash when trying to update window bounds and File Explorer already disposed the preview.
### Find My Mouse
- Added the option to have to use the Windows + Control keys to activate. Thanks [@Gentoli](https://github.com/Gentoli)!
### Hosts File Editor
- Refactored, packaged and released the main Hosts File Editor functionality as a control to be integrated into DevHome. Thanks [@dabhattimsft](https://github.com/dabhattimsft) for validating and integrating into DevHome!
### Image Resizer
- Supported narrator announcing the checkboxes in the UI and the sizes combobox. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
- Improved accessibility by increasing contrast in the text color of combobox items.
- Improved spacing definitions in the UI so that hosts name are not hidden when resizing and icons are well aligned. Thanks [@htcfreek](https://github.com/htcfreek)!
- Changed the additional lines dialog to show the horizontal scrollbar instead of wrapping contents. Thanks [@htcfreek](https://github.com/htcfreek)!
- Improved the duplication check's logic to improve performance and take into account features that were introduced after it. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
### Installer
- Fixed some install failures when the folders the DSC module is to be installed in isn't accessible by the WiX installer. (This was a hotfix for 0.80)
- Detecting install location for DSC now uses registry instead of WMI to improve performance. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
- Fixed an error causing the machine scope installer to not install correctly in machines where the documents folder is in a UNC network path. We're still working in a fix for the user scope installer.
### Keyboard Manager
- Fixed startup crashes in the editor when the Visual C++ Redistributable wasn't installed. (This was a hotfix for 0.80)
- Fixed an accessibility issue where the first button wasn't focused after adding a new row in the editor.
- Environment Variables are now expanded in arguments of programs started through a shortcut. Thanks [@HydroH](https://github.com/HydroH)!
### Paste as Plain Text
- Paste as Plain Text was removed as a separate utility, since its functionality is now part of the Advanced Paste utility.
- Fixed the remaining install failures when the folders the DSC module is to be installed in isn't accessible by the WiX installer for user scope installations.
- Fixed an issue causing ARM 64 uninstall process to not correctly finding powershell 7 to run uninstall scripts.
### Peek
- Updated icons, tweaked UI and refactored internal code. Thanks [@Jay-o-Way](https://github.com/Jay-o-Way)!
- Updated Monaco to 0.47 and added the new sticky scroll setting for DevFiles viewer. Thanks [@Aaron-Junker](https://github.com/Aaron-Junker)!
- Added the new font size setting for DevFiles viewer. Thanks [@Aaron-Junker](https://github.com/Aaron-Junker)!
- Upgrade the SharpCompress dependency to 0.37.2 and fixed archive parsing. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
- Fixed aliasing in the image viewer.
- Added support for .srt (subtitle) file previewing in DevFiles viewer. Thanks [@PesBandi](https://github.com/PesBandi)!
- Prevent activating Peek when the user is renaming a file. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
- Added support to preview special folders like Recycle Bin and My PC instead of throwing an error.
- Fixed a crash caused by double releasing a COM object from the module interface.
### Power Rename
- Fixed the descriptions that were mixed up in the regex helper (\S and \w).
- Improved apostrophe character handling for the Capitalize and Titlecase renaming flags. Thanks [@anthonymonforte](https://github.com/anthonymonforte)!
- Added a feature to allow using sequences of random characters or UUIDs when renaming files. Thanks [@jhirvioja](https://github.com/jhirvioja)!
### PowerToys Run
- Added support for UNC paths starting with // in the Folder plugin. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
- Fixed the plugin load failed message to list the failed plugins. Thanks [@belkiss](https://github.com/belkiss)!
- Icons for MSIX packages are now updated when a package update is detected. Thanks [@HydroH](https://github.com/HydroH)!
- Use Mica backdrop instead of Acrylic to fix random crashes caused by the Windows composition being momentarily turned off.
- Improved accessibility in the results list action buttons by improving contrast of hovered/focused buttons.
- Improved the plugin descriptions for consistency in the UI. Thanks [@HydroH](https://github.com/HydroH)!
- Fixed UI scaling for different dpi scenarios.
- Fixed crash on a racing condition when updating UWP icon paths in the Program plugin. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
- Fixed PowerToys Run hanging when trying to close an unresponsive window in the WindowWalker plugin. Thanks [@GhostVaibhav](https://github.com/GhostVaibhav)!
- Fixed the example in the UnitConverter description to reduce confusion with the inches abbreviation (now uses "to" instead of "in"). Thanks [@acekirkpatrick](https://github.com/acekirkpatrick)!
- Brought the acrylic background back and applied a proper fix to the titlebar accent showing through transparency.
- Fixed an issue causing the transparency from the UI disappearing after some time.
### Quick Accent
- Added support for the Esperanto character set. Thanks [@salutontalk](https://github.com/salutontalk) and [@ccmywish](https://github.com/ccmywish)!
- Added the ǽ and ϑ characters. Thanks [@PesBandi](https://github.com/PesBandi)!
- Added support for the Crimean Tatar character set. Thanks [@cor-bee](https://github.com/cor-bee)!
- Added the Numero symbol and double acute accent character. Thanks [@PesBandi](https://github.com/PesBandi)!
- Added the International Phonetic Alphabet characters. Thanks [@PesBandi](https://github.com/PesBandi)!
- Fixed the character description center positioning. Thanks [@PesBandi](https://github.com/PesBandi)!
- Added feminine and masculine ordinal indicator characters to the Portuguese character set. Thanks [@octastylos-pseudodipteros](https://github.com/octastylos-pseudodipteros)!
### Registry Preview
### Screen Ruler
- Refactored, packaged and released the main Registry Preview functionality as a control to be integrated into DevHome. Thanks [@dabhattimsft](https://github.com/dabhattimsft) for validating and integrating into DevHome!
### Text Extractor
- Fixed an issue causing the Settings page to not be opened when clicking the Settings button in Text Extractor's overlay. (This was a hotfix for 0.80)
- Updated the default activation hotkey to Win+Control+Shift+M, in order to not conflict with the Windows shortcut that restores minimized windows (Win+Shift+M). Thanks [@nx-frost](https://github.com/nx-frost)!
### Settings
- Improved UI ordering of the File Explorer add-ons. Thanks [@niels9001](https://github.com/niels9001)!
- Applied fixes to theme overriding and cleaned up unneeded code. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
- Fixed misspells in references to the Hosts File Editor utility. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
- Improved accessibility of the Select Folder button in the Settings Backup UI.
- Improved accessibility by improving focus and tab navigation in the ColorPicker page. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
- Added a description to the fallback encoder setting in the Image Resizer page. Thanks [@Kissaki](https://github.com/Kissaki)!
- Refactored and improved performance in the PowerToys Run plugins UI in the Settings page. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
- Fixed a crash when a user cleared the contents of a Number Box in the PowerToys Run plugins additional options. Thanks [@htcfreek](https://github.com/htcfreek)!
- Update the PATH environment variables with the user scope PATH when entering the Command Not Found page to improve PowerShell detection.
- Disabled the UI to enable/disable clipboard history in the Advanced Paste settings page when clipboard history is disabled by GPO in the system. (This was a hotfix for 0.81)
- Updated Advanced Paste's Settings and OOBE page to clarify that the AI use is optional and opt-in. (This was a hotfix for 0.81)
- Corrected a spelling fix in Advanced Paste's settings page. Thanks [@htcfreek](https://github.com/htcfreek)!
- Added localization support for the "Configure OpenAI Key" button in Advanced Paste's settings page. Thanks [@zetaloop](https://github.com/zetaloop)!
- Fixed extra GPO warnings being shown in Advanced Paste's settings page even if the module is disabled. Thanks [@htcfreek](https://github.com/htcfreek)!
- Fixed a crash when a PowerToys Run plugin icon path is badly formed.
- Disabled the experimentation paths in code behind to improve performance, since there's no current experimentation going on.
### Documentation
- Added the WebSearchShortcut plugin to PowerToys Run thirdPartyRunPlugins.md docs. Thanks [@Daydreamer-riri](https://github.com/Daydreamer-riri)!
- Updated COMMUNITY.md with the project managers that are part of the core team.
- Improved the DSC samples.
- Added the 1Password plugin to PowerToys Run thirdPartyRunPlugins.md docs. Thanks [@KairuDeibisu](https://github.com/KairuDeibisu)!
- Added the UnicodeInput plugin to PowerToys Run thirdPartyRunPlugins.md docs. Thanks [@nathancartlidge](https://github.com/nathancartlidge)!
- Adjusted the readme and release notes to clarify use of AI on Advanced Paste. (This was a hotfix for 0.81)
- Added the Edge Workspaces plugin to PowerToys Run thirdPartyRunPlugins.md docs. Thanks [@quachpas](https://github.com/quachpas)!
- Removed the deprecated Guid plugin from PowerToys Run thirdPartyRunPlugins.md docs. Thanks [@abduljawada](https://github.com/abduljawada)!
- Added the PowerHexInspector plugin to PowerToys Run thirdPartyRunPlugins.md docs. Thanks [@NaroZeol](https://github.com/NaroZeol)!
- Fixed a broken link in the communication-with-modules.md file. Thanks [@PesBandi](https://github.com/PesBandi)!
- Updated COMMUNITY.md with missing and former members.
### Development
- Updated System.Drawing.Common to 8.0.5 to fix CI builds after the .NET 8.0.5 upgrade was released.
- Fixed file permissions when doing a build using cache on PR CI. Thanks [@dfederm](https://github.com/dfederm)!
- Removed the Test SDK reference on ARM64 to fix local building for ARM64. Thanks [@dfederm](https://github.com/dfederm)!
- Replaced make_pair with RemapBufferRow in Keyboard Manager internal code. Thanks [@masaru-iritani](https://github.com/masaru-iritani)!
- Added CODEOWNERS file to protect sensitive parts of the repo. Thanks [@htcfreek](https://github.com/htcfreek) for the help in figuring out how to make the spellcheck folder an exception!
- Added comments in code. to make it clear what the error badge in PowerToys Run plugin list in Settings means. Thanks [@Jay-o-Way](https://github.com/Jay-o-Way)!
- Enabled caching by default in the PR CI pipelines. Thanks [@dfederm](https://github.com/dfederm)!
- Disabled caching for PR started from forks, since those were failing. Thanks [@dfederm](https://github.com/dfederm)!
- Removed baseline files for policy checking and turned on the "TSA" process in the release pipelines instead.
- Added caching of nuget packages in the PR CI pipelines. Thanks [@dfederm](https://github.com/dfederm)!
- Updated the release CI pipelines TouchdownBuildTask to v3.
- Moved the release CI pipelines to ESRPv5.
- Added a policy for GitHub Copilot Workspaces for the repo on GitHub. Thanks [@Aaron-Junker](https://github.com/Aaron-Junker)!
- Fixed ci UI tests to point to the correct Visual Studio vstest location after a Visual Studio upgrade. (This was a hotfix for 0.81)
- Updated System.Drawing.Common to 8.0.6 to fix CI builds after the .NET 8.0.6 upgrade was released.
- Removed an incorrect file reference to long removed documentation from the solution file. Thanks [@Kissaki](https://github.com/Kissaki)!
- Upgraded Windows App SDK to 1.5.3.
- Removed use of the BinaryFormatter API from Mouse Without Borders, which is expected to be deprecated in .NET 9.
- The user scope installer is now sent to the Microsoft store instead of the machine scope installer.
- Refactored Mouse Jump's internal code to allow for a future introduction of customizable appearance features. Thanks [@mikeclayton](https://github.com/mikeclayton)!
- Removed a noisy error from spell check ci runs.
- Improved the ci agent pool selection code.
- Updated Xamlstyler.console to 3.2404.2. Thanks [@Jvr2022](https://github.com/Jvr2022)!
- Updated UnitsNet to 5.50.0 Thanks [@Jvr2022](https://github.com/Jvr2022)!
- Replaced LPINPUT with std::vector of INPUT instances in Keyboard Manager internal code. Thanks [@masaru-iritani](https://github.com/masaru-iritani)!
- Improved the Microsoft Store submission ci action to use the proper cli and authentication.
#### What is being planned for version 0.82
#### What is being planned for version 0.83
For [v0.82][github-next-release-work], we'll work on the items below:
For [v0.83][github-next-release-work], we'll work on the items below:
- Stability / bug fixes
- New utility: Dev Projects
- Language selection
- New module: File Actions Menu

View File

@ -47,7 +47,16 @@ registerAdditionalNewLanguage("id", [".fileExtension"], idDefinition(), monaco)
* The id can be anything. Recommended is one of the file extensions. For example "php" or "reg".
4. Execute the steps described in the [monaco_languages.json](#monaco_languagesjson) section.
4. In case you wish to add a custom color for a token, you can do so by adding the following line to [`customTokenColors.js`](/src/common/FilePreviewCommon/Assets/Monaco/customTokenColors.js):
```javascript
{token: 'token-name', foreground: 'ff0000'}
```
> Replace `token-name` with the name of the token and `ff0000` with the hex code of the desired color.
> Note: you can also specify a `background` and a `fontStyle` attribute for your token.
* Keep in mind that these rules apply to all languages. Therefore, you should not change the colors of any default tokens. Instead, create new tokens specific to the language you are adding.
5. Execute the steps described in the [monaco_languages.json](#monaco_languagesjson) section.
### Add a new file extension to an existing language

View File

@ -72,7 +72,7 @@ In order to test the remapping logic, a mocked keyboard input handler had to be
The [`MockedInput`](https://github.com/microsoft/PowerToys/blob/main/src/modules/keyboardmanager/test/MockedInput.h) class uses a 256 size `bool` vector to store the key state for each key code. Identifying the foreground process is mocked by simply setting and getting a string value for the name of the current process.
[To mock the `SendInput` method](https://github.com/microsoft/PowerToys/blob/b80578b1b9a4b24c9945bddac33c771204280107/src/modules/keyboardmanager/test/MockedInput.cpp#L10-L110), the steps for processing the input are as follows. This implementation is based on public documentation for SendInput and the behavior of key messages and keyboard hooks:
- Iterate over all the inputs in the INPUT array argument
- Iterate over all the inputs in the `INPUT` vector argument.
- If the event is a key up event, then it is considered [`WM_SYSKEYUP`](https://learn.microsoft.com/windows/win32/inputdev/wm-syskeyup) if Alt is held down, otherwise it is `WM_KEYUP`.
- If the event is a key down event, then it is considered [`WM_SYSKEYDOWN`](https://learn.microsoft.com/windows/win32/inputdev/wm-syskeydown) if either Alt is held down or if it is F10, otherwise it is `WM_KEYDOWN`.
- An optional function which can be set on the `MockedInput` handler can be used to test for the number of times a key event is received by the system with a particular condition using [`sendVirtualInputCallCondition`](https://github.com/microsoft/PowerToys/blob/b80578b1b9a4b24c9945bddac33c771204280107/src/modules/keyboardmanager/test/MockedInput.cpp#L48-L52).

View File

@ -21,7 +21,7 @@ $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", "monacoSpecialLanguages.js", "*.pri")
$fileInclusionList = @("*.dll", "*.exe", "*.json", "*.msix", "*.png", "*.gif", "*.ico", "*.cur", "*.svg", "index.html", "reg.js", "gitignore.js", "monacoSpecialLanguages.js", "customTokenColors.js", "*.pri")
$dllsToIgnore = @("System.CodeDom.dll", "WindowsBase.dll")

View File

@ -357,7 +357,7 @@ UINT __stdcall InstallDSCModuleCA(MSIHANDLE hInstall)
ExitOnFailure(hr, "Unable to determine Powershell modules path");
}
const auto modulesPath = baseModulesPath / L"Microsoft.PowerToys.Configure" / get_product_version();
const auto modulesPath = baseModulesPath / L"Microsoft.PowerToys.Configure" / get_product_version(false);
std::error_code errorCode;
fs::create_directories(modulesPath, errorCode);
@ -411,7 +411,7 @@ UINT __stdcall UninstallDSCModuleCA(MSIHANDLE hInstall)
}
const auto powerToysModulePath = baseModulesPath / L"Microsoft.PowerToys.Configure";
const auto versionedModulePath = powerToysModulePath / get_product_version();
const auto versionedModulePath = powerToysModulePath / get_product_version(false);
std::error_code errorCode;

View File

@ -12,5 +12,10 @@ namespace Common.UI
{
return Environment.OSVersion.Version.Major >= 10 && Environment.OSVersion.Version.Build >= 22000;
}
public static bool IsGreaterThanWindows11_21H2()
{
return Environment.OSVersion.Version.Major >= 10 && Environment.OSVersion.Version.Build > 22000;
}
}
}

View File

@ -5,8 +5,8 @@
tokenizer: {
root: [
[/^#.*$/, 'comment'],
[/^\s*!.*/, 'invalid'],
[/^\s*[^#]+/, "tag"]
[/.*((?<!(^|\/))\*\*.*|\*\*(?!(\/|$))).*/, 'invalid'],
[/((?:^!\s*(?:\\\s|\S)+)?)((?:^\s*(?:\\\s|\S)+)?)((?:\s+(?:\\\s|\S)+)*)/, ['custom-gitignore.negation', 'tag', 'invalid']]
]
}
};

View File

@ -0,0 +1,3 @@
export const customTokenColors = [
{token: 'custom-gitignore.negation', foreground: 'c00ce0'}
];

View File

@ -6,7 +6,7 @@
// Get URL parameters:
// `code` contains the code of the file in base64 encoded
// `theme` can be "light" or "dark"
// `theme` can be "vs" for light theme or "vs-dark" for dark theme
// `lang` is the language of the file
// `wrap` if the editor is wrapping or not
@ -59,19 +59,29 @@
<script src="http://[[PT_URL]]/monacoSpecialLanguages.js" type="module"></script>
<script type="module">
var editor;
import { registerAdditionalLanguages } from "http://[[PT_URL]]/monacoSpecialLanguages.js"
import { registerAdditionalLanguages } from 'http://[[PT_URL]]/monacoSpecialLanguages.js';
import { customTokenColors } from 'http://[[PT_URL]]/customTokenColors.js';
require.config({ paths: { vs: 'http://[[PT_URL]]/monacoSRC/min/vs' } });
require(['vs/editor/editor.main'], async function () {
await registerAdditionalLanguages(monaco)
// Creates a theme to handle custom tokens
monaco.editor.defineTheme('theme', {
base: theme, // Sets the base theme to "vs" or "vs-dark" depending on the user's preference
inherit: true,
rules: customTokenColors,
colors: {} // `colors` is a required attribute
});
// Creates the editor
// For all parameters: https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.istandaloneeditorconstructionoptions.html
// For all parameters: https://microsoft.github.io/monaco-editor/typedoc/interfaces/editor.IStandaloneEditorConstructionOptions.html
editor = monaco.editor.create(document.getElementById('container'), {
value: code, // Sets content of the editor
language: lang, // Sets language fof the code
language: lang, // Sets language of the code
readOnly: true, // Sets to readonly
theme: theme, // Sets editor theme
theme: 'theme', // Sets editor theme
minimap: {enabled: false}, // Disables minimap
lineNumbersMinChars: "3", //Width of the line numbers
lineNumbersMinChars: '3', // Width of the line numbers
scrollbar: {
// Deactivate shadows
shadows: false,
@ -79,17 +89,16 @@
// Render scrollbar automatically
vertical: 'auto',
horizontal: 'auto',
},
stickyScroll: {enabled: stickyScroll},
fontSize: fontSize,
wordWrap: (wrap?"on":"off") // Word wraps
wordWrap: (wrap ? 'on' : 'off') // Word wraps
});
window.onresize = function (){
window.onresize = () => {
editor.layout();
};
// Add switch wrap button to context menu
// Add toggle wrap button to context menu
editor.addAction({
id: 'text-wrap',
@ -101,17 +110,17 @@
// A rule to evaluate on top of the precondition in order to dispatch the keybindings.
keybindingContext: null,
contextMenuGroupId: 'cutcopypaste',
contextMenuGroupId: 'cutcopypaste',
contextMenuOrder: 100,
contextMenuOrder: 100,
// Method that will be executed when the action is triggered.
// @param editor The editor instance is passed in as a convenience
run: function (ed) {
if(wrap){
editor.updateOptions({ wordWrap: "off" })
}else{
editor.updateOptions({ wordWrap: "on" })
if (wrap) {
editor.updateOptions({ wordWrap: 'off' })
} else {
editor.updateOptions({ wordWrap: 'on' })
}
wrap = !wrap;
}
@ -120,11 +129,11 @@
onContextMenu();
});
function onContextMenu(){
function onContextMenu() {
// Hide context menu items
// Code modified from https://stackoverflow.com/questions/48745208/disable-cut-and-copy-in-context-menu-in-monaco-editor/65413517#65413517
let menus = require('vs/platform/actions/common/actions').MenuRegistry._menuItems
let contextMenuEntry = [...menus].find(entry => entry[0].id == "EditorContext")
let contextMenuEntry = [...menus].find(entry => entry[0].id == 'EditorContext')
let contextMenuLinks = contextMenuEntry[1]
let removableIds = ['editor.action.clipboardCutAction', 'editor.action.formatDocument', 'editor.action.formatSelection', 'editor.action.quickCommand', 'editor.action.quickOutline', 'editor.action.refactor', 'editor.action.sourceAction', 'editor.action.rename', undefined, 'editor.action.revealDefinition', 'editor.action.revealDeclaration', 'editor.action.goToTypeDefinition', 'editor.action.goToImplementation', 'editor.action.goToReferences', 'editor.action.changeAll']

View File

@ -34,6 +34,9 @@
<None Update="Assets\Monaco\monacoSpecialLanguages.js">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Assets\Monaco\customTokenColors.js">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<Content Include="Assets\Monaco\monacoSRC\**">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>

View File

@ -27,18 +27,18 @@ enum class version_architecture
version_architecture get_current_architecture();
const wchar_t* get_architecture_string(const version_architecture);
inline std::wstring get_product_version()
inline std::wstring get_product_version(bool includeV = true)
{
static std::wstring version = L"v" + std::to_wstring(VERSION_MAJOR) +
static std::wstring version = (includeV ? L"v" : L"") + std::to_wstring(VERSION_MAJOR) +
L"." + std::to_wstring(VERSION_MINOR) +
L"." + std::to_wstring(VERSION_REVISION);
return version;
}
inline std::wstring get_std_product_version()
inline std::wstring get_std_product_version(bool includeV = true)
{
static std::wstring version = L"v" + std::to_wstring(VERSION_MAJOR) +
static std::wstring version = (includeV ? L"v" : L"") + std::to_wstring(VERSION_MAJOR) +
L"." + std::to_wstring(VERSION_MINOR) +
L"." + std::to_wstring(VERSION_REVISION) + L".0";

View File

@ -3,15 +3,14 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Threading.Tasks;
using AdvancedPaste.Helpers;
using AdvancedPaste.Settings;
using AdvancedPaste.ViewModels;
using ManagedCommon;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Windows.ApplicationModel.DataTransfer;
using Windows.Graphics;
using WinUIEx;
using static AdvancedPaste.Helpers.NativeMethods;
@ -47,6 +46,7 @@ namespace AdvancedPaste
Host = Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder().UseContentRoot(AppContext.BaseDirectory).ConfigureServices((context, services) =>
{
services.AddSingleton<OptionsViewModel>();
services.AddSingleton<IUserSettings, UserSettings>();
}).Build();
viewModel = GetService<OptionsViewModel>();

View File

@ -2,7 +2,6 @@
// 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.Net;
using System.Threading.Tasks;
using AdvancedPaste.Helpers;
@ -20,14 +19,13 @@ namespace AdvancedPaste.Controls
public sealed partial class PromptBox : Microsoft.UI.Xaml.Controls.UserControl
{
private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread();
private UserSettings _userSettings;
private readonly IUserSettings _userSettings;
public static readonly DependencyProperty PromptProperty = DependencyProperty.Register(
nameof(Prompt),
typeof(string),
typeof(PromptBox),
new PropertyMetadata(defaultValue: string.Empty));
nameof(Prompt),
typeof(string),
typeof(PromptBox),
new PropertyMetadata(defaultValue: string.Empty));
public OptionsViewModel ViewModel { get; private set; }
@ -38,10 +36,10 @@ namespace AdvancedPaste.Controls
}
public static readonly DependencyProperty PlaceholderTextProperty = DependencyProperty.Register(
nameof(PlaceholderText),
typeof(string),
typeof(PromptBox),
new PropertyMetadata(defaultValue: string.Empty));
nameof(PlaceholderText),
typeof(string),
typeof(PromptBox),
new PropertyMetadata(defaultValue: string.Empty));
public string PlaceholderText
{
@ -50,10 +48,10 @@ namespace AdvancedPaste.Controls
}
public static readonly DependencyProperty FooterProperty = DependencyProperty.Register(
nameof(Footer),
typeof(object),
typeof(PromptBox),
new PropertyMetadata(defaultValue: null));
nameof(Footer),
typeof(object),
typeof(PromptBox),
new PropertyMetadata(defaultValue: null));
public object Footer
{
@ -65,7 +63,7 @@ namespace AdvancedPaste.Controls
{
this.InitializeComponent();
_userSettings = new UserSettings();
_userSettings = App.GetService<IUserSettings>();
ViewModel = App.GetService<OptionsViewModel>();
}
@ -80,8 +78,6 @@ namespace AdvancedPaste.Controls
{
Logger.LogTrace();
PowerToysTelemetry.Log.WriteEvent(new Telemetry.AdvancedPasteGenerateCustomFormatEvent());
VisualStateManager.GoToState(this, "LoadingState", true);
string inputInstructions = InputTxtBox.Text;
ViewModel.SaveQuery(inputInstructions);

View File

@ -3,21 +3,20 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Runtime.CompilerServices;
using AdvancedPaste.Helpers;
using AdvancedPaste.Settings;
using ManagedCommon;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Windows.Graphics;
using WinUIEx;
using WinUIEx.Messaging;
using static AdvancedPaste.Helpers.NativeMethods;
namespace AdvancedPaste
{
public sealed partial class MainWindow : WindowEx, IDisposable
{
private WindowMessageMonitor _msgMonitor;
private readonly WindowMessageMonitor _msgMonitor;
private readonly IUserSettings _userSettings;
private bool _disposedValue;
@ -25,6 +24,8 @@ namespace AdvancedPaste
{
this.InitializeComponent();
_userSettings = App.GetService<IUserSettings>();
AppWindow.SetIcon("Assets/AdvancedPaste/AdvancedPaste.ico");
this.ExtendsContentIntoTitleBar = true;
this.SetTitleBar(titleBar);
@ -32,6 +33,8 @@ namespace AdvancedPaste
var loader = ResourceLoaderInstance.ResourceLoader;
Title = loader.GetString("WindowTitle");
Activated += OnActivated;
_msgMonitor = new WindowMessageMonitor(this);
_msgMonitor.WindowMessageReceived += (_, e) =>
{
@ -47,6 +50,14 @@ namespace AdvancedPaste
WindowHelpers.BringToForeground(this.GetWindowHandle());
}
private void OnActivated(object sender, WindowActivatedEventArgs args)
{
if (_userSettings.CloseAfterLosingFocus && args.WindowActivationState == WindowActivationState.Deactivated)
{
Hide();
}
}
private void Dispose(bool disposing)
{
if (!_disposedValue)
@ -66,11 +77,15 @@ namespace AdvancedPaste
private void WindowEx_Closed(object sender, Microsoft.UI.Xaml.WindowEventArgs args)
{
Windows.Win32.PInvoke.ShowWindow((Windows.Win32.Foundation.HWND)this.GetWindowHandle(), Windows.Win32.UI.WindowsAndMessaging.SHOW_WINDOW_CMD.SW_HIDE);
Hide();
args.Handled = true;
}
private void Hide()
{
Windows.Win32.PInvoke.ShowWindow(new Windows.Win32.Foundation.HWND(this.GetWindowHandle()), Windows.Win32.UI.WindowsAndMessaging.SHOW_WINDOW_CMD.SW_HIDE);
}
public void SetFocus()
{
MainPage.CustomFormatTextBox.InputTxtBox.Focus(FocusState.Programmatic);

View File

@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AdvancedPaste.Helpers;
@ -86,6 +87,11 @@ namespace AdvancedPaste.Pages
_dispatcherQueue.TryEnqueue(async () =>
{
// Clear to avoid leaks due to Garbage Collection not clearing the bitmap from memory. Fix for https://github.com/microsoft/PowerToys/issues/33423
clipboardHistory.Where(x => x.Image is not null)
.ToList()
.ForEach(x => x.Image.ClearValue(BitmapImage.UriSourceProperty));
clipboardHistory.Clear();
foreach (var item in items)
@ -225,6 +231,7 @@ namespace AdvancedPaste.Pages
var item = e.ClickedItem as ClipboardItem;
if (item is not null)
{
PowerToysTelemetry.Log.WriteEvent(new Telemetry.AdvancedPasteClipboardItemClicked());
if (!string.IsNullOrEmpty(item.Content))
{
ClipboardHelper.SetClipboardTextContent(item.Content);

View File

@ -33,6 +33,8 @@ namespace AdvancedPaste.Helpers
private string _openAIKey;
private string _modelName = "gpt-3.5-turbo-instruct";
public bool IsAIEnabled => !string.IsNullOrEmpty(this._openAIKey);
public AICompletionsHelper()
@ -69,14 +71,14 @@ namespace AdvancedPaste.Helpers
return string.Empty;
}
public string GetAICompletion(string systemInstructions, string userMessage)
private Response<Completions> GetAICompletion(string systemInstructions, string userMessage)
{
OpenAIClient azureAIClient = new OpenAIClient(_openAIKey);
var response = azureAIClient.GetCompletions(
new CompletionsOptions()
{
DeploymentName = "gpt-3.5-turbo-instruct",
DeploymentName = _modelName,
Prompts =
{
systemInstructions + "\n\n" + userMessage,
@ -90,7 +92,7 @@ namespace AdvancedPaste.Helpers
Console.WriteLine("Cut off due to length constraints");
}
return response.Value.Choices[0].Text;
return response;
}
public AICompletionsResponse AIFormatString(string inputInstructions, string inputString)
@ -109,10 +111,16 @@ Output:
";
string aiResponse = null;
Response<Completions> rawAIResponse = null;
int apiRequestStatus = (int)HttpStatusCode.OK;
try
{
aiResponse = this.GetAICompletion(systemInstructions, userMessage);
rawAIResponse = this.GetAICompletion(systemInstructions, userMessage);
aiResponse = rawAIResponse.Value.Choices[0].Text;
int promptTokens = rawAIResponse.Value.Usage.PromptTokens;
int completionTokens = rawAIResponse.Value.Usage.CompletionTokens;
PowerToysTelemetry.Log.WriteEvent(new Telemetry.AdvancedPasteGenerateCustomFormatEvent(promptTokens, completionTokens, _modelName));
}
catch (Azure.RequestFailedException error)
{

View File

@ -9,5 +9,7 @@ namespace AdvancedPaste.Settings
public bool ShowCustomPreview { get; }
public bool SendPasteKeyCombination { get; }
public bool CloseAfterLosingFocus { get; }
}
}

View File

@ -16,6 +16,10 @@ namespace AdvancedPaste.Helpers
{
internal static class JsonHelper
{
// Ini parts regex
private static readonly Regex IniSectionNameRegex = new Regex(@"^\[(.+)\]");
private static readonly Regex IniValueLineRegex = new Regex(@"(.+?)\s*=\s*(.*)");
// List of supported CSV delimiters and Regex to detect separator property
private static readonly char[] CsvDelimArry = [',', ';', '\t'];
private static readonly Regex CsvSepIdentifierRegex = new Regex(@"^sep=(.)$", RegexOptions.IgnoreCase);
@ -45,6 +49,7 @@ namespace AdvancedPaste.Helpers
{
XmlDocument doc = new XmlDocument();
doc.LoadXml(text);
Logger.LogDebug("Converted from XML.");
jsonText = JsonConvert.SerializeXmlNode(doc, Newtonsoft.Json.Formatting.Indented);
}
catch (Exception ex)
@ -52,6 +57,75 @@ namespace AdvancedPaste.Helpers
Logger.LogError("Failed parsing input as xml", ex);
}
// Try convert Ini
// (Must come before CSV that ini is not false detected as CSV.)
try
{
if (string.IsNullOrEmpty(jsonText))
{
var ini = new Dictionary<string, Dictionary<string, string>>();
var lastSectionName = string.Empty;
string[] lines = text.Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
// Skipp comment lines.
// (Comments are lines that starts with a semicolon.
// This also skips commented key-value-pairs.)
lines = lines.Where(l => !l.StartsWith(';')).ToArray();
// Validate content as ini
// (First line is a section name and second line is a section name or a key-value-pair.
// For the second line we check both, in case the first ini section is empty.)
if (lines.Length >= 2 && IniSectionNameRegex.IsMatch(lines[0]) &&
(IniSectionNameRegex.IsMatch(lines[1]) || IniValueLineRegex.IsMatch(lines[1])))
{
// Parse and convert Ini
foreach (string line in lines)
{
Match lineSectionNameCheck = IniSectionNameRegex.Match(line);
Match lineKeyValuePairCheck = IniValueLineRegex.Match(line);
if (lineSectionNameCheck.Success)
{
// Section name (Group 1)
lastSectionName = lineSectionNameCheck.Groups[1].Value.Trim();
if (string.IsNullOrWhiteSpace(lastSectionName))
{
throw new FormatException("Invalid ini file format: Empty section name.");
}
ini.Add(lastSectionName, new Dictionary<string, string>());
}
else if (!lineKeyValuePairCheck.Success)
{
// Fail if it is not a key-value-pair (and was not detected as section name before).
throw new FormatException("Invalid ini file format: Invalid line.");
}
else
{
// Key-value-pair (Group 1=Key; Group 2=Value)
string iniKeyName = lineKeyValuePairCheck.Groups[1].Value.Trim();
if (string.IsNullOrWhiteSpace(iniKeyName))
{
throw new FormatException("Invalid ini file format: Empty value name (key).");
}
string iniValueData = lineKeyValuePairCheck.Groups[2].Value;
ini[lastSectionName].Add(iniKeyName, iniValueData);
}
}
// Convert to JSON
Logger.LogDebug("Converted from Ini.");
jsonText = JsonConvert.SerializeObject(ini, Newtonsoft.Json.Formatting.Indented);
}
}
}
catch (Exception ex)
{
Logger.LogError("Failed parsing input as ini", ex);
}
// Try convert CSV
try
{

View File

@ -24,12 +24,15 @@ namespace AdvancedPaste.Settings
public bool SendPasteKeyCombination { get; private set; }
public bool CloseAfterLosingFocus { get; private set; }
public UserSettings()
{
_settingsUtils = new SettingsUtils();
ShowCustomPreview = true;
SendPasteKeyCombination = true;
CloseAfterLosingFocus = false;
LoadSettingsFromJson();
@ -61,6 +64,7 @@ namespace AdvancedPaste.Settings
{
ShowCustomPreview = settings.Properties.ShowCustomPreview;
SendPasteKeyCombination = settings.Properties.SendPasteKeyCombination;
CloseAfterLosingFocus = settings.Properties.CloseAfterLosingFocus;
}
retry = false;

View File

@ -0,0 +1,16 @@
// 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.Diagnostics.Tracing;
using Microsoft.PowerToys.Telemetry;
using Microsoft.PowerToys.Telemetry.Events;
namespace AdvancedPaste.Telemetry
{
[EventData]
public class AdvancedPasteClipboardItemClicked : EventBase, IEvent
{
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
}
}

View File

@ -11,6 +11,19 @@ namespace AdvancedPaste.Telemetry
[EventData]
public class AdvancedPasteGenerateCustomFormatEvent : EventBase, IEvent
{
public int PromptTokens { get; set; }
public int CompletionTokens { get; set; }
public string ModelName { get; set; }
public AdvancedPasteGenerateCustomFormatEvent(int promptTokens, int completionTokens, string modelName)
{
this.PromptTokens = promptTokens;
this.CompletionTokens = completionTokens;
ModelName = modelName;
}
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
}
}

View File

@ -25,13 +25,12 @@ namespace AdvancedPaste.ViewModels
public partial class OptionsViewModel : ObservableObject
{
private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread();
private readonly IUserSettings _userSettings;
private App app = App.Current as App;
private AICompletionsHelper aiHelper;
private UserSettings _userSettings;
public DataPackageView ClipboardData { get; set; }
[ObservableProperty]
@ -50,10 +49,10 @@ namespace AdvancedPaste.ViewModels
[NotifyPropertyChangedFor(nameof(InputTxtBoxErrorText))]
private int _apiRequestStatus;
public OptionsViewModel()
public OptionsViewModel(IUserSettings userSettings)
{
aiHelper = new AICompletionsHelper();
_userSettings = new UserSettings();
_userSettings = userSettings;
IsCustomAIEnabled = IsClipboardDataText && aiHelper.IsAIEnabled;

View File

@ -135,6 +135,14 @@ namespace ColorPicker.Helpers
Application.Current.MainWindow.Opacity = 0;
Application.Current.MainWindow.Visibility = Visibility.Visible;
_colorPickerShown = true;
// HACK: WPF UI theme watcher removes the composition target background color, among other weird stuff.
// https://github.com/lepoco/wpfui/blob/303f0aefcd59a142bc681415dc4360a34a15f33d/src/Wpf.Ui/Controls/Window/WindowBackdrop.cs#L280
// So we set it back with https://github.com/lepoco/wpfui/blob/303f0aefcd59a142bc681415dc4360a34a15f33d/src/Wpf.Ui/Controls/Window/WindowBackdrop.cs#L191
// And also reapply the intended backdrop.
// This hack fixes: https://github.com/microsoft/PowerToys/issues/31725
Wpf.Ui.Controls.WindowBackdrop.RemoveBackground(Application.Current.MainWindow);
Wpf.Ui.Controls.WindowBackdrop.ApplyBackdrop(Application.Current.MainWindow, Wpf.Ui.Controls.WindowBackdropType.None);
}
}

View File

@ -30,6 +30,9 @@
<!-- This package is a dependency of System.Management, but we need to set it here so we can exclude the assets, so it doesn't conflict with the 8.0.1 dll coming from .NET SDK. -->
<ExcludeAssets>runtime</ExcludeAssets> <!-- Should already be present on .net sdk runtime, so we avoid the conflicting runtime version from nuget -->
</PackageReference>
<PackageReference Include="System.Drawing.Common">
<ExcludeAssets>runtime</ExcludeAssets> <!-- Should already be present on .net sdk runtime, so we avoid the conflicting runtime version from nuget -->
</PackageReference>
</ItemGroup>
<ItemGroup>

View File

@ -69,7 +69,7 @@ namespace KeyboardEventHandlers
key_count = std::get<Shortcut>(it->second).Size();
}
LPINPUT keyEventList = new INPUT[size_t(key_count)]{};
std::vector<INPUT> keyEventList;
// Handle remaps to VK_WIN_BOTH
DWORD target;
@ -92,35 +92,31 @@ namespace KeyboardEventHandlers
{
if (data->wParam == WM_KEYUP || data->wParam == WM_SYSKEYUP)
{
Helpers::SetKeyEvent(keyEventList, 0, INPUT_KEYBOARD, static_cast<WORD>(target), KEYEVENTF_KEYUP, KeyboardManagerConstants::KEYBOARDMANAGER_SINGLEKEY_FLAG);
Helpers::SetKeyEvent(keyEventList, INPUT_KEYBOARD, static_cast<WORD>(target), KEYEVENTF_KEYUP, KeyboardManagerConstants::KEYBOARDMANAGER_SINGLEKEY_FLAG);
}
else
{
Helpers::SetKeyEvent(keyEventList, 0, INPUT_KEYBOARD, static_cast<WORD>(target), 0, KeyboardManagerConstants::KEYBOARDMANAGER_SINGLEKEY_FLAG);
Helpers::SetKeyEvent(keyEventList, INPUT_KEYBOARD, static_cast<WORD>(target), 0, KeyboardManagerConstants::KEYBOARDMANAGER_SINGLEKEY_FLAG);
}
}
else
{
int i = 0;
Shortcut targetShortcut = std::get<Shortcut>(it->second);
if (data->wParam == WM_KEYUP || data->wParam == WM_SYSKEYUP)
{
Helpers::SetKeyEvent(keyEventList, i, INPUT_KEYBOARD, static_cast<WORD>(targetShortcut.GetActionKey()), KEYEVENTF_KEYUP, KeyboardManagerConstants::KEYBOARDMANAGER_SINGLEKEY_FLAG);
i++;
Helpers::SetModifierKeyEvents(targetShortcut, ModifierKey::Disabled, keyEventList, i, false, KeyboardManagerConstants::KEYBOARDMANAGER_SINGLEKEY_FLAG);
Helpers::SetKeyEvent(keyEventList, INPUT_KEYBOARD, static_cast<WORD>(targetShortcut.GetActionKey()), KEYEVENTF_KEYUP, KeyboardManagerConstants::KEYBOARDMANAGER_SINGLEKEY_FLAG);
Helpers::SetModifierKeyEvents(targetShortcut, ModifierKey::Disabled, keyEventList, false, KeyboardManagerConstants::KEYBOARDMANAGER_SINGLEKEY_FLAG);
// Dummy key is not required here since SetModifierKeyEvents will only add key-up events for the modifiers here, and the action key key-up is already sent before it
}
else
{
// Dummy key is not required here since SetModifierKeyEvents will only add key-down events for the modifiers here, and the action key key-down is already sent after it
Helpers::SetModifierKeyEvents(targetShortcut, ModifierKey::Disabled, keyEventList, i, true, KeyboardManagerConstants::KEYBOARDMANAGER_SINGLEKEY_FLAG);
Helpers::SetKeyEvent(keyEventList, i, INPUT_KEYBOARD, static_cast<WORD>(targetShortcut.GetActionKey()), 0, KeyboardManagerConstants::KEYBOARDMANAGER_SINGLEKEY_FLAG);
i++;
Helpers::SetModifierKeyEvents(targetShortcut, ModifierKey::Disabled, keyEventList, true, KeyboardManagerConstants::KEYBOARDMANAGER_SINGLEKEY_FLAG);
Helpers::SetKeyEvent(keyEventList, INPUT_KEYBOARD, static_cast<WORD>(targetShortcut.GetActionKey()), 0, KeyboardManagerConstants::KEYBOARDMANAGER_SINGLEKEY_FLAG);
}
}
UINT res = ii.SendVirtualInput(key_count, keyEventList, sizeof(INPUT));
delete[] keyEventList;
ii.SendVirtualInput(keyEventList);
if (data->wParam == WM_KEYDOWN || data->wParam == WM_SYSKEYDOWN)
{
@ -194,14 +190,12 @@ namespace KeyboardEventHandlers
return 1;
}
}
int key_count = 2;
LPINPUT keyEventList = new INPUT[size_t(key_count)]();
Helpers::SetKeyEvent(keyEventList, 0, INPUT_KEYBOARD, (WORD)data->lParam->vkCode, 0, KeyboardManagerConstants::KEYBOARDMANAGER_SINGLEKEY_FLAG);
Helpers::SetKeyEvent(keyEventList, 1, INPUT_KEYBOARD, (WORD)data->lParam->vkCode, KEYEVENTF_KEYUP, KeyboardManagerConstants::KEYBOARDMANAGER_SINGLEKEY_FLAG);
std::vector<INPUT> keyEventList;
Helpers::SetKeyEvent(keyEventList, INPUT_KEYBOARD, (WORD)data->lParam->vkCode, 0, KeyboardManagerConstants::KEYBOARDMANAGER_SINGLEKEY_FLAG);
Helpers::SetKeyEvent(keyEventList, INPUT_KEYBOARD, (WORD)data->lParam->vkCode, KEYEVENTF_KEYUP, KeyboardManagerConstants::KEYBOARDMANAGER_SINGLEKEY_FLAG);
lock.unlock();
UINT res = ii.SendVirtualInput(key_count, keyEventList, sizeof(INPUT));
delete[] keyEventList;
ii.SendVirtualInput(keyEventList);
// Reset the long press flag when the key has been lifted.
if (data->wParam == WM_KEYUP || data->wParam == WM_SYSKEYUP)
@ -306,8 +300,7 @@ namespace KeyboardEventHandlers
continue;
}
size_t key_count = 0;
LPINPUT keyEventList = nullptr;
std::vector<INPUT> keyEventList;
// Remember which win key was pressed initially
if (ii.GetVirtualKeyState(VK_RWIN))
@ -331,8 +324,6 @@ namespace KeyboardEventHandlers
myThread.detach();
}
Logger::trace(L"ChordKeyboardHandler:returning..");
return 1;
}
@ -362,7 +353,6 @@ namespace KeyboardEventHandlers
}
}
auto threadFunction = [newUri]() {
HINSTANCE result = ShellExecute(NULL, L"open", newUri.c_str(), NULL, NULL, SW_SHOWNORMAL);
@ -380,7 +370,6 @@ namespace KeyboardEventHandlers
myThread.detach();
}
Logger::trace(L"ChordKeyboardHandler:returning..");
return 1;
}
@ -394,30 +383,22 @@ namespace KeyboardEventHandlers
if (commonKeys == src_size - 1)
{
// key down for all new shortcut keys except the common modifiers
key_count = dest_size - commonKeys;
keyEventList = new INPUT[key_count]{};
int i = 0;
Helpers::SetModifierKeyEvents(std::get<Shortcut>(it->second.targetShortcut), it->second.winKeyInvoked, keyEventList, i, true, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG, it->first);
Helpers::SetKeyEvent(keyEventList, i, INPUT_KEYBOARD, static_cast<WORD>(std::get<Shortcut>(it->second.targetShortcut).GetActionKey()), 0, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
i++;
keyEventList = std::vector<INPUT>{};
Helpers::SetModifierKeyEvents(std::get<Shortcut>(it->second.targetShortcut), it->second.winKeyInvoked, keyEventList, true, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG, it->first);
Helpers::SetKeyEvent(keyEventList, INPUT_KEYBOARD, static_cast<WORD>(std::get<Shortcut>(it->second.targetShortcut).GetActionKey()), 0, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
}
else
{
// Dummy key, key up for all the original shortcut modifier keys and key down for all the new shortcut keys but common keys in each are not repeated
key_count = KeyboardManagerConstants::DUMMY_KEY_EVENT_SIZE + (src_size - 1) + (dest_size) - (2 * static_cast<size_t>(commonKeys));
keyEventList = new INPUT[key_count]{};
// Send a dummy key event to prevent modifier press+release from being triggered. Example: Win+A->Ctrl+V, press Win+A, since Win will be released here we need to send a dummy event before it
int i = 0;
Helpers::SetDummyKeyEvent(keyEventList, i, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
Helpers::SetDummyKeyEvent(keyEventList, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
// Release original shortcut state (release in reverse order of shortcut to be accurate)
Helpers::SetModifierKeyEvents(it->first, it->second.winKeyInvoked, keyEventList, i, false, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG, std::get<Shortcut>(it->second.targetShortcut));
Helpers::SetModifierKeyEvents(it->first, it->second.winKeyInvoked, keyEventList, false, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG, std::get<Shortcut>(it->second.targetShortcut));
// Set new shortcut key down state
Helpers::SetModifierKeyEvents(std::get<Shortcut>(it->second.targetShortcut), it->second.winKeyInvoked, keyEventList, i, true, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG, it->first);
Helpers::SetKeyEvent(keyEventList, i, INPUT_KEYBOARD, static_cast<WORD>(std::get<Shortcut>(it->second.targetShortcut).GetActionKey()), 0, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
i++;
Helpers::SetModifierKeyEvents(std::get<Shortcut>(it->second.targetShortcut), it->second.winKeyInvoked, keyEventList, true, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG, it->first);
Helpers::SetKeyEvent(keyEventList, INPUT_KEYBOARD, static_cast<WORD>(std::get<Shortcut>(it->second.targetShortcut).GetActionKey()), 0, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
}
// Modifier state reset might be required for this key depending on the shortcut's action and target modifiers - ex: Win+Caps -> Ctrl+A
@ -432,31 +413,23 @@ namespace KeyboardEventHandlers
}
else if (remapToKey)
{
// Dummy key, key up for all the original shortcut modifier keys and key down for remapped key
key_count = KeyboardManagerConstants::DUMMY_KEY_EVENT_SIZE + (src_size - 1) + dest_size;
// Do not send Disable key
if (std::get<DWORD>(it->second.targetShortcut) == CommonSharedConstants::VK_DISABLED)
{
key_count--;
// Since the original shortcut's action key is pressed, set it to true
it->second.isOriginalActionKeyPressed = true;
}
keyEventList = new INPUT[key_count]{};
// Send a dummy key event to prevent modifier press+release from being triggered. Example: Win+A->V, press Win+A, since Win will be released here we need to send a dummy event before it
int i = 0;
Helpers::SetDummyKeyEvent(keyEventList, i, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
Helpers::SetDummyKeyEvent(keyEventList, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
// Release original shortcut state (release in reverse order of shortcut to be accurate)
Helpers::SetModifierKeyEvents(it->first, it->second.winKeyInvoked, keyEventList, i, false, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
Helpers::SetModifierKeyEvents(it->first, it->second.winKeyInvoked, keyEventList, false, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
// Set target key down state
if (std::get<DWORD>(it->second.targetShortcut) != CommonSharedConstants::VK_DISABLED)
{
Helpers::SetKeyEvent(keyEventList, i, INPUT_KEYBOARD, static_cast<WORD>(Helpers::FilterArtificialKeys(std::get<DWORD>(it->second.targetShortcut))), 0, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
i++;
Helpers::SetKeyEvent(keyEventList, INPUT_KEYBOARD, static_cast<WORD>(Helpers::FilterArtificialKeys(std::get<DWORD>(it->second.targetShortcut))), 0, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
}
// Modifier state reset might be required for this key depending on the shortcut's action and target modifier - ex: Win+Caps -> Ctrl
@ -468,29 +441,14 @@ namespace KeyboardEventHandlers
// Remapped to text
else
{
key_count = KeyboardManagerConstants::DUMMY_KEY_EVENT_SIZE + src_size;
auto& remapping = std::get<std::wstring>(it->second.targetShortcut);
auto remapping = std::get<std::wstring>(it->second.targetShortcut);
const UINT inputEventCount = static_cast<UINT>(remapping.length() * 2);
key_count += inputEventCount;
keyEventList = new INPUT[key_count]{};
int i = 0;
Helpers::SetDummyKeyEvent(keyEventList, i, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
Helpers::SetDummyKeyEvent(keyEventList, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
// Release original shortcut state (release in reverse order of shortcut to be accurate)
Helpers::SetModifierKeyEvents(it->first, it->second.winKeyInvoked, keyEventList, i, false, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
Helpers::SetModifierKeyEvents(it->first, it->second.winKeyInvoked, keyEventList, false, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
for (size_t idx = 0; idx < inputEventCount; ++idx)
{
auto& input = keyEventList[idx + i];
input.type = INPUT_KEYBOARD;
const bool upEvent = idx & 0x1;
input.ki.dwFlags = KEYEVENTF_UNICODE | (upEvent ? KEYEVENTF_KEYUP : 0);
input.ki.dwExtraInfo = KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG;
input.ki.wScan = remapping[idx >> 1];
}
Helpers::SetTextKeyEvents(keyEventList, remapping);
}
it->second.isShortcutInvoked = true;
@ -500,12 +458,9 @@ namespace KeyboardEventHandlers
state.SetActivatedApp(*activatedApp);
}
Logger::trace(L"ChordKeyboardHandler:key_count:{}", key_count);
Logger::trace(L"ChordKeyboardHandler:keyEventList.size:{}", keyEventList.size());
UINT res = ii.SendVirtualInput(static_cast<UINT>(key_count), keyEventList, sizeof(INPUT));
delete[] keyEventList;
// Send daily telemetry event for Keyboard Manager key activation.
ii.SendVirtualInput(keyEventList);
if (activatedApp.has_value())
{
if (remapToKey)
@ -574,84 +529,41 @@ namespace KeyboardEventHandlers
if ((it->first.CheckWinKey(data->lParam->vkCode) || it->first.CheckCtrlKey(data->lParam->vkCode) || it->first.CheckAltKey(data->lParam->vkCode) || it->first.CheckShiftKey(data->lParam->vkCode)) && (data->wParam == WM_KEYUP || data->wParam == WM_SYSKEYUP))
{
// Release new shortcut, and set original shortcut keys except the one released
size_t key_count = 0;
LPINPUT keyEventList = nullptr;
std::vector<INPUT> keyEventList;
if (remapToShortcut && !isRunProgram)
{
// if the released key is present in both shortcuts' modifiers (i.e part of the common modifiers)
if (std::get<Shortcut>(it->second.targetShortcut).CheckWinKey(data->lParam->vkCode) || std::get<Shortcut>(it->second.targetShortcut).CheckCtrlKey(data->lParam->vkCode) || std::get<Shortcut>(it->second.targetShortcut).CheckAltKey(data->lParam->vkCode) || std::get<Shortcut>(it->second.targetShortcut).CheckShiftKey(data->lParam->vkCode))
{
// release all new shortcut keys and the common released modifier except the other common modifiers, and add all original shortcut modifiers except the common ones, and dummy key
key_count = (dest_size - commonKeys) + (src_size - 1 - commonKeys) + KeyboardManagerConstants::DUMMY_KEY_EVENT_SIZE;
}
else
{
// release all new shortcut keys except the common modifiers and add all original shortcut modifiers except the common ones, and dummy key
key_count = (dest_size - 1) + (src_size - 2) - (2 * static_cast<size_t>(commonKeys)) + KeyboardManagerConstants::DUMMY_KEY_EVENT_SIZE;
}
// If the target shortcut's action key is pressed, then it should be released
bool isActionKeyPressed = false;
if (ii.GetVirtualKeyState((std::get<Shortcut>(it->second.targetShortcut).GetActionKey())))
{
isActionKeyPressed = true;
key_count += 1;
}
keyEventList = new INPUT[key_count]{};
bool isActionKeyPressed = ii.GetVirtualKeyState(std::get<Shortcut>(it->second.targetShortcut).GetActionKey());
// Release new shortcut state (release in reverse order of shortcut to be accurate)
int i = 0;
if (isActionKeyPressed)
{
Helpers::SetKeyEvent(keyEventList, i, INPUT_KEYBOARD, static_cast<WORD>(std::get<Shortcut>(it->second.targetShortcut).GetActionKey()), KEYEVENTF_KEYUP, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
i++;
Helpers::SetKeyEvent(keyEventList, INPUT_KEYBOARD, static_cast<WORD>(std::get<Shortcut>(it->second.targetShortcut).GetActionKey()), KEYEVENTF_KEYUP, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
}
Helpers::SetModifierKeyEvents(std::get<Shortcut>(it->second.targetShortcut), it->second.winKeyInvoked, keyEventList, i, false, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG, it->first, data->lParam->vkCode);
Helpers::SetModifierKeyEvents(std::get<Shortcut>(it->second.targetShortcut), it->second.winKeyInvoked, keyEventList, false, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG, it->first, data->lParam->vkCode);
// Set original shortcut key down state except the action key and the released modifier since the original action key may or may not be held down. If it is held down it will generate it's own key message
Helpers::SetModifierKeyEvents(it->first, it->second.winKeyInvoked, keyEventList, i, true, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG, std::get<Shortcut>(it->second.targetShortcut), data->lParam->vkCode);
Helpers::SetModifierKeyEvents(it->first, it->second.winKeyInvoked, keyEventList, true, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG, std::get<Shortcut>(it->second.targetShortcut), data->lParam->vkCode);
// Send a dummy key event to prevent modifier press+release from being triggered. Example: Win+Ctrl+A->Ctrl+V, press Win+Ctrl+A and release A then Ctrl, since Win will be pressed here we need to send a dummy event after it
Helpers::SetDummyKeyEvent(keyEventList, i, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
Helpers::SetDummyKeyEvent(keyEventList, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
}
else if (remapToKey)
{
// 1 for releasing new key and original shortcut modifiers except the one released and dummy key
key_count = dest_size + src_size - 2 + KeyboardManagerConstants::DUMMY_KEY_EVENT_SIZE;
bool isTargetKeyPressed = false;
// Do not send Disable key up
if (std::get<DWORD>(it->second.targetShortcut) == CommonSharedConstants::VK_DISABLED)
{
key_count--;
}
else if (ii.GetVirtualKeyState(Helpers::FilterArtificialKeys(std::get<DWORD>(it->second.targetShortcut))))
{
isTargetKeyPressed = true;
}
else
{
isTargetKeyPressed = false;
key_count--;
}
keyEventList = new INPUT[key_count]{};
bool isTargetKeyPressed = (std::get<DWORD>(it->second.targetShortcut) != CommonSharedConstants::VK_DISABLED) && ii.GetVirtualKeyState(Helpers::FilterArtificialKeys(std::get<DWORD>(it->second.targetShortcut)));
// Release new key state
int i = 0;
if (std::get<DWORD>(it->second.targetShortcut) != CommonSharedConstants::VK_DISABLED && isTargetKeyPressed)
{
Helpers::SetKeyEvent(keyEventList, i, INPUT_KEYBOARD, static_cast<WORD>(Helpers::FilterArtificialKeys(std::get<DWORD>(it->second.targetShortcut))), KEYEVENTF_KEYUP, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
i++;
Helpers::SetKeyEvent(keyEventList, INPUT_KEYBOARD, static_cast<WORD>(Helpers::FilterArtificialKeys(std::get<DWORD>(it->second.targetShortcut))), KEYEVENTF_KEYUP, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
}
// Set original shortcut key down state except the action key and the released modifier since the original action key may or may not be held down. If it is held down it will generate it's own key message
Helpers::SetModifierKeyEvents(it->first, it->second.winKeyInvoked, keyEventList, i, true, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG, Shortcut(), data->lParam->vkCode);
Helpers::SetModifierKeyEvents(it->first, it->second.winKeyInvoked, keyEventList, true, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG, Shortcut(), data->lParam->vkCode);
// Send a dummy key event to prevent modifier press+release from being triggered. Example: Win+Ctrl+A->V, press Win+Ctrl+A and release A then Ctrl, since Win will be pressed here we need to send a dummy event after it
Helpers::SetDummyKeyEvent(keyEventList, i, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
Helpers::SetDummyKeyEvent(keyEventList, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
}
// Reset the remap state
@ -665,12 +577,7 @@ namespace KeyboardEventHandlers
state.SetActivatedApp(KeyboardManagerConstants::NoActivatedApp);
}
// key count can be 0 if both shortcuts have same modifiers and the action key is not held down. delete will throw an error if keyEventList is empty
if (key_count > 0)
{
UINT res = ii.SendVirtualInput(static_cast<UINT>(key_count), keyEventList, sizeof(INPUT));
delete[] keyEventList;
}
ii.SendVirtualInput(keyEventList);
return 1;
}
@ -678,7 +585,7 @@ namespace KeyboardEventHandlers
if (!remapToShortcut || (remapToShortcut && std::get<Shortcut>(it->second.targetShortcut).CheckModifiersKeyboardState(ii)))
{
// Case 2: If the original shortcut is still held down the keyboard will get a key down message of the action key in the original shortcut and the new shortcut's modifiers will be held down (keys held down send repeated keydown messages)
if (((data->lParam->vkCode == it->first.GetActionKey() && !it->first.HasChord())||(data->lParam->vkCode == it->first.GetSecondKey() && it->first.HasChord())) && (data->wParam == WM_KEYDOWN || data->wParam == WM_SYSKEYDOWN))
if (((data->lParam->vkCode == it->first.GetActionKey() && !it->first.HasChord()) || (data->lParam->vkCode == it->first.GetSecondKey() && it->first.HasChord())) && (data->wParam == WM_KEYDOWN || data->wParam == WM_SYSKEYDOWN))
{
// In case of mapping to disable do not send anything
if (remapToKey && std::get<DWORD>(it->second.targetShortcut) == CommonSharedConstants::VK_DISABLED)
@ -688,69 +595,46 @@ namespace KeyboardEventHandlers
return 1;
}
size_t key_count = 1;
LPINPUT keyEventList = nullptr;
std::vector<INPUT> keyEventList;
if (remapToShortcut)
{
keyEventList = new INPUT[key_count]{};
Helpers::SetKeyEvent(keyEventList, 0, INPUT_KEYBOARD, static_cast<WORD>(std::get<Shortcut>(it->second.targetShortcut).GetActionKey()), 0, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
Helpers::SetKeyEvent(keyEventList, INPUT_KEYBOARD, static_cast<WORD>(std::get<Shortcut>(it->second.targetShortcut).GetActionKey()), 0, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
}
else if (remapToKey)
{
keyEventList = new INPUT[key_count]{};
Helpers::SetKeyEvent(keyEventList, 0, INPUT_KEYBOARD, static_cast<WORD>(Helpers::FilterArtificialKeys(std::get<DWORD>(it->second.targetShortcut))), 0, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
Helpers::SetKeyEvent(keyEventList, INPUT_KEYBOARD, static_cast<WORD>(Helpers::FilterArtificialKeys(std::get<DWORD>(it->second.targetShortcut))), 0, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
}
else if (remapToText)
{
auto remapping = std::get<std::wstring>(it->second.targetShortcut);
const UINT inputEventCount = static_cast<UINT>(remapping.length() * 2);
key_count = inputEventCount;
keyEventList = new INPUT[key_count]{};
for (size_t idx = 0; idx < inputEventCount; ++idx)
{
auto& input = keyEventList[idx];
input.type = INPUT_KEYBOARD;
const bool upEvent = idx & 0x1;
input.ki.dwFlags = KEYEVENTF_UNICODE | (upEvent ? KEYEVENTF_KEYUP : 0);
input.ki.dwExtraInfo = KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG;
input.ki.wScan = remapping[idx >> 1];
}
auto& remapping = std::get<std::wstring>(it->second.targetShortcut);
Helpers::SetTextKeyEvents(keyEventList, remapping);
}
UINT res = ii.SendVirtualInput(static_cast<UINT>(key_count), keyEventList, sizeof(INPUT));
delete[] keyEventList;
ii.SendVirtualInput(keyEventList);
return 1;
}
// Case 3: If the action key is released from the original shortcut, keep modifiers of the new shortcut until some other key event which doesn't apply to the original shortcut
if (!remapToText && ((!it->first.HasChord() && data->lParam->vkCode == it->first.GetActionKey()) || (it->first.HasChord() && data->lParam->vkCode==it->first.GetSecondKey())) && (data->wParam == WM_KEYUP || data->wParam == WM_SYSKEYUP))
if (!remapToText && ((!it->first.HasChord() && data->lParam->vkCode == it->first.GetActionKey()) || (it->first.HasChord() && data->lParam->vkCode == it->first.GetSecondKey())) && (data->wParam == WM_KEYUP || data->wParam == WM_SYSKEYUP))
{
size_t key_count = 1;
LPINPUT keyEventList = nullptr;
std::vector<INPUT> keyEventList;
if (remapToShortcut && !it->first.HasChord())
{
// Just lift the action key for no chords.
keyEventList = new INPUT[key_count]{};
Helpers::SetKeyEvent(keyEventList, 0, INPUT_KEYBOARD, static_cast<WORD>(std::get<Shortcut>(it->second.targetShortcut).GetActionKey()), KEYEVENTF_KEYUP, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
Helpers::SetKeyEvent(keyEventList, INPUT_KEYBOARD, static_cast<WORD>(std::get<Shortcut>(it->second.targetShortcut).GetActionKey()), KEYEVENTF_KEYUP, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
}
else if (remapToShortcut && it->first.HasChord())
{
// If it has a chord, we'll want a full clean contemplated in the else, since you can't really repeat chords by pressing the end key again.
// Key up for all new shortcut keys, key down for original shortcut modifiers and current key press but common keys aren't repeated
key_count = (dest_size) + (src_size +1) - (2 * static_cast<size_t>(commonKeys));
int i = 0;
keyEventList = new INPUT[key_count]{};
Helpers::SetKeyEvent(keyEventList, i, INPUT_KEYBOARD, static_cast<WORD>(std::get<Shortcut>(it->second.targetShortcut).GetActionKey()), KEYEVENTF_KEYUP, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
i++;
Helpers::SetKeyEvent(keyEventList, INPUT_KEYBOARD, static_cast<WORD>(std::get<Shortcut>(it->second.targetShortcut).GetActionKey()), KEYEVENTF_KEYUP, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
// Release new shortcut state (release in reverse order of shortcut to be accurate)
Helpers::SetModifierKeyEvents(std::get<Shortcut>(it->second.targetShortcut), it->second.winKeyInvoked, keyEventList, i, false, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG, it->first);
Helpers::SetModifierKeyEvents(std::get<Shortcut>(it->second.targetShortcut), it->second.winKeyInvoked, keyEventList, false, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG, it->first);
// Set old shortcut key down state
Helpers::SetModifierKeyEvents(it->first, it->second.winKeyInvoked, keyEventList, i, true, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG, std::get<Shortcut>(it->second.targetShortcut));
Helpers::SetModifierKeyEvents(it->first, it->second.winKeyInvoked, keyEventList, true, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG, std::get<Shortcut>(it->second.targetShortcut));
// Reset the remap state
it->second.isShortcutInvoked = false;
@ -762,7 +646,6 @@ namespace KeyboardEventHandlers
{
state.SetActivatedApp(KeyboardManagerConstants::NoActivatedApp);
}
}
else if (std::get<DWORD>(it->second.targetShortcut) == CommonSharedConstants::VK_DISABLED)
{
@ -779,29 +662,21 @@ namespace KeyboardEventHandlers
// If the keyboard state is clear, we release the target key but do not reset the remap state
if (isKeyboardStateClear)
{
keyEventList = new INPUT[key_count]{};
Helpers::SetKeyEvent(keyEventList, 0, INPUT_KEYBOARD, static_cast<WORD>(Helpers::FilterArtificialKeys(std::get<DWORD>(it->second.targetShortcut))), KEYEVENTF_KEYUP, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
Helpers::SetKeyEvent(keyEventList, INPUT_KEYBOARD, static_cast<WORD>(Helpers::FilterArtificialKeys(std::get<DWORD>(it->second.targetShortcut))), KEYEVENTF_KEYUP, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
}
else
{
// If any other key is pressed, then the keyboard state must be reverted back to the physical keys.
// This is to take cases like Ctrl+A->D remap and user presses B+Ctrl+A and releases A, or Ctrl+A+B and releases A
// 1 for releasing new key and original shortcut modifiers, and dummy key
key_count = dest_size + (src_size - 1) + KeyboardManagerConstants::DUMMY_KEY_EVENT_SIZE;
keyEventList = new INPUT[key_count]{};
// Release new key state
int i = 0;
Helpers::SetKeyEvent(keyEventList, i, INPUT_KEYBOARD, static_cast<WORD>(Helpers::FilterArtificialKeys(std::get<DWORD>(it->second.targetShortcut))), KEYEVENTF_KEYUP, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
i++;
Helpers::SetKeyEvent(keyEventList, INPUT_KEYBOARD, static_cast<WORD>(Helpers::FilterArtificialKeys(std::get<DWORD>(it->second.targetShortcut))), KEYEVENTF_KEYUP, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
// Set original shortcut key down state except the action key
Helpers::SetModifierKeyEvents(it->first, it->second.winKeyInvoked, keyEventList, i, true, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
Helpers::SetModifierKeyEvents(it->first, it->second.winKeyInvoked, keyEventList, true, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
// Send a dummy key event to prevent modifier press+release from being triggered. Example: Win+A->V, press Shift+Win+A and release A, since Win will be pressed here we need to send a dummy event after it
Helpers::SetDummyKeyEvent(keyEventList, i, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
Helpers::SetDummyKeyEvent(keyEventList, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
// Reset the remap state
it->second.isShortcutInvoked = false;
@ -816,8 +691,7 @@ namespace KeyboardEventHandlers
}
}
UINT res = ii.SendVirtualInput(static_cast<UINT>(key_count), keyEventList, sizeof(INPUT));
delete[] keyEventList;
ii.SendVirtualInput(keyEventList);
return 1;
}
@ -854,8 +728,7 @@ namespace KeyboardEventHandlers
ResetIfModifierKeyForLowerLevelKeyHandlers(ii, data->lParam->vkCode, std::get<Shortcut>(it->second.targetShortcut).GetActionKey());
}
size_t key_count;
LPINPUT keyEventList = nullptr;
std::vector<INPUT> keyEventList;
// Check if a new remapping should be applied
Shortcut currentlyPressed = it->first;
@ -868,40 +741,25 @@ namespace KeyboardEventHandlers
if (newRemapping.RemapToKey())
{
DWORD to = std::get<0>(newRemapping.targetShortcut);
bool isLastKeyStillPressed = ii.GetVirtualKeyState(static_cast<WORD>(from.actionKey));
key_count = static_cast<size_t>(from.Size()) - 1 + 1 + (isLastKeyStillPressed ? 1 : 0);
keyEventList = new INPUT[key_count]{};
int i = 0;
Helpers::SetModifierKeyEvents(from, it->second.winKeyInvoked, keyEventList, i, false, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
Helpers::SetModifierKeyEvents(from, it->second.winKeyInvoked, keyEventList, false, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
if (ii.GetVirtualKeyState(static_cast<WORD>(from.actionKey)))
{
// If the action key from the last shortcut is still being pressed, release it.
Helpers::SetKeyEvent(keyEventList, i, INPUT_KEYBOARD, static_cast<WORD>(from.actionKey), KEYEVENTF_KEYUP, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
i++;
Helpers::SetKeyEvent(keyEventList, INPUT_KEYBOARD, static_cast<WORD>(from.actionKey), KEYEVENTF_KEYUP, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
}
Helpers::SetKeyEvent(keyEventList, i, INPUT_KEYBOARD, static_cast<WORD>(to), 0, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
Helpers::SetKeyEvent(keyEventList, INPUT_KEYBOARD, static_cast<WORD>(to), 0, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
}
else
{
Shortcut to = std::get<Shortcut>(newRemapping.targetShortcut);
bool isLastKeyStillPressed = ii.GetVirtualKeyState(static_cast<WORD>(from.actionKey));
size_t temp_key_count_calculation = static_cast<size_t>(from.Size()) - 1;
temp_key_count_calculation += static_cast<size_t>(to.Size()) - 1;
temp_key_count_calculation -= static_cast<size_t>(2) * from.GetCommonModifiersCount(to);
key_count = temp_key_count_calculation + 1 + (isLastKeyStillPressed ? 1 : 0);
keyEventList = new INPUT[key_count]{};
int i = 0;
Helpers::SetModifierKeyEvents(from, it->second.winKeyInvoked, keyEventList, i, false, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG, to);
Helpers::SetModifierKeyEvents(from, it->second.winKeyInvoked, keyEventList, false, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG, to);
if (ii.GetVirtualKeyState(static_cast<WORD>(from.actionKey)))
{
// If the action key from the last shortcut is still being pressed, release it.
Helpers::SetKeyEvent(keyEventList, i, INPUT_KEYBOARD, static_cast<WORD>(from.actionKey), KEYEVENTF_KEYUP, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
i++;
Helpers::SetKeyEvent(keyEventList, INPUT_KEYBOARD, static_cast<WORD>(from.actionKey), KEYEVENTF_KEYUP, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
}
Helpers::SetModifierKeyEvents(to, it->second.winKeyInvoked, keyEventList, i, true, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG, from);
Helpers::SetKeyEvent(keyEventList, i, INPUT_KEYBOARD, static_cast<WORD>(to.actionKey), 0, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
Helpers::SetModifierKeyEvents(to, it->second.winKeyInvoked, keyEventList, true, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG, from);
Helpers::SetKeyEvent(keyEventList, INPUT_KEYBOARD, static_cast<WORD>(to.actionKey), 0, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
newRemapping.isShortcutInvoked = true;
}
@ -917,41 +775,27 @@ namespace KeyboardEventHandlers
}
else
{
// Key up for all new shortcut keys, key down for original shortcut modifiers and current key press but common keys aren't repeated
key_count = (dest_size) + (src_size - 1) - (2 * static_cast<size_t>(commonKeys));
// If the target shortcut's action key is pressed, then it should be released and original shortcut's action key should be set
bool isActionKeyPressed = false;
if (ii.GetVirtualKeyState((std::get<Shortcut>(it->second.targetShortcut).GetActionKey())))
{
isActionKeyPressed = true;
key_count += 2;
}
keyEventList = new INPUT[key_count]{};
bool isActionKeyPressed = ii.GetVirtualKeyState(std::get<Shortcut>(it->second.targetShortcut).GetActionKey());
// Release new shortcut state (release in reverse order of shortcut to be accurate)
int i = 0;
if (isActionKeyPressed)
{
Helpers::SetKeyEvent(keyEventList, i, INPUT_KEYBOARD, static_cast<WORD>(std::get<Shortcut>(it->second.targetShortcut).GetActionKey()), KEYEVENTF_KEYUP, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
i++;
Helpers::SetKeyEvent(keyEventList, INPUT_KEYBOARD, static_cast<WORD>(std::get<Shortcut>(it->second.targetShortcut).GetActionKey()), KEYEVENTF_KEYUP, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
}
Helpers::SetModifierKeyEvents(std::get<Shortcut>(it->second.targetShortcut), it->second.winKeyInvoked, keyEventList, i, false, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG, it->first);
Helpers::SetModifierKeyEvents(std::get<Shortcut>(it->second.targetShortcut), it->second.winKeyInvoked, keyEventList, false, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG, it->first);
// Set old shortcut key down state
Helpers::SetModifierKeyEvents(it->first, it->second.winKeyInvoked, keyEventList, i, true, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG, std::get<Shortcut>(it->second.targetShortcut));
Helpers::SetModifierKeyEvents(it->first, it->second.winKeyInvoked, keyEventList, true, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG, std::get<Shortcut>(it->second.targetShortcut));
// key down for original shortcut action key with shortcut flag so that we don't invoke the same shortcut remap again
if (isActionKeyPressed)
{
Helpers::SetKeyEvent(keyEventList, i, INPUT_KEYBOARD, static_cast<WORD>(it->first.GetActionKey()), 0, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
i++;
Helpers::SetKeyEvent(keyEventList, INPUT_KEYBOARD, static_cast<WORD>(it->first.GetActionKey()), 0, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
}
// Send current key pressed without shortcut flag so that it can be reprocessed in case the physical keys pressed are a different remapped shortcut
Helpers::SetKeyEvent(keyEventList, i, INPUT_KEYBOARD, static_cast<WORD>(data->lParam->vkCode), 0, 0);
i++;
Helpers::SetKeyEvent(keyEventList, INPUT_KEYBOARD, static_cast<WORD>(data->lParam->vkCode), 0, 0);
// Do not send a dummy key as we want the current key press to behave as normal i.e. it can do press+release functionality if required. Required to allow a shortcut to Win key remap invoked directly after shortcut to shortcut is released to open start menu
}
@ -967,8 +811,7 @@ namespace KeyboardEventHandlers
state.SetActivatedApp(KeyboardManagerConstants::NoActivatedApp);
}
UINT res = ii.SendVirtualInput(static_cast<UINT>(key_count), keyEventList, sizeof(INPUT));
delete[] keyEventList;
ii.SendVirtualInput(keyEventList);
return 1;
}
else
@ -1003,30 +846,20 @@ namespace KeyboardEventHandlers
if (isRemapToDisable || !isOriginalActionKeyPressed)
{
// Key down for original shortcut modifiers and action key, and current key press
size_t key_count = src_size + 1;
LPINPUT keyEventList = new INPUT[key_count]{};
std::vector<INPUT> keyEventList;
// Set original shortcut key down state
int i = 0;
Helpers::SetModifierKeyEvents(it->first, it->second.winKeyInvoked, keyEventList, i, true, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
Helpers::SetModifierKeyEvents(it->first, it->second.winKeyInvoked, keyEventList, true, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
// Send the original action key only if it is physically pressed. For remappings to keys other than disabled we already check earlier that it is not pressed in this scenario. For remap to disable
if (isRemapToDisable && isOriginalActionKeyPressed)
{
// Set original action key
Helpers::SetKeyEvent(keyEventList, i, INPUT_KEYBOARD, static_cast<WORD>(it->first.GetActionKey()), 0, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
i++;
}
else
{
key_count--;
Helpers::SetKeyEvent(keyEventList, INPUT_KEYBOARD, static_cast<WORD>(it->first.GetActionKey()), 0, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
}
// Send current key pressed without shortcut flag so that it can be reprocessed in case the physical keys pressed are a different remapped shortcut
Helpers::SetKeyEvent(keyEventList, i, INPUT_KEYBOARD, static_cast<WORD>(data->lParam->vkCode), 0, 0);
i++;
Helpers::SetKeyEvent(keyEventList, INPUT_KEYBOARD, static_cast<WORD>(data->lParam->vkCode), 0, 0);
// Do not send a dummy key as we want the current key press to behave as normal i.e. it can do press+release functionality if required. Required to allow a shortcut to Win key remap invoked directly after another shortcut to key remap is released to open start menu
@ -1041,8 +874,7 @@ namespace KeyboardEventHandlers
state.SetActivatedApp(KeyboardManagerConstants::NoActivatedApp);
}
UINT res = ii.SendVirtualInput(static_cast<UINT>(key_count), keyEventList, sizeof(INPUT));
delete[] keyEventList;
ii.SendVirtualInput(keyEventList);
return 1;
}
else
@ -1344,7 +1176,7 @@ namespace KeyboardEventHandlers
if (targetPid != 0 && shortcut.alreadyRunningAction != Shortcut::ProgramAlreadyRunningAction::StartAnother)
{
if (shortcut.alreadyRunningAction == Shortcut::ProgramAlreadyRunningAction::EndTask)
if (shortcut.alreadyRunningAction == Shortcut::ProgramAlreadyRunningAction::EndTask)
{
TerminateProcessesByName(fileNamePart);
return;
@ -1803,13 +1635,11 @@ namespace KeyboardEventHandlers
// If the argument is either of the Ctrl/Shift/Alt modifier key codes
if (Helpers::IsModifierKey(key) && !(key == VK_LWIN || key == VK_RWIN || key == CommonSharedConstants::VK_WIN_BOTH))
{
int key_count = 1;
LPINPUT keyEventList = new INPUT[size_t(key_count)]{};
std::vector<INPUT> keyEventList;
// Use the suppress flag to ensure these are not intercepted by any remapped keys or shortcuts
Helpers::SetKeyEvent(keyEventList, 0, INPUT_KEYBOARD, static_cast<WORD>(key), KEYEVENTF_KEYUP, KeyboardManagerConstants::KEYBOARDMANAGER_SUPPRESS_FLAG);
UINT res = ii.SendVirtualInput(static_cast<UINT>(key_count), keyEventList, sizeof(INPUT));
delete[] keyEventList;
Helpers::SetKeyEvent(keyEventList, INPUT_KEYBOARD, static_cast<WORD>(key), KEYEVENTF_KEYUP, KeyboardManagerConstants::KEYBOARDMANAGER_SUPPRESS_FLAG);
ii.SendVirtualInput(keyEventList);
}
}
}
@ -1834,21 +1664,9 @@ namespace KeyboardEventHandlers
return 0;
}
const size_t keyCount = remapping->length();
const UINT eventCount = static_cast<UINT>(keyCount * 2);
auto keyEventList = std::make_unique<INPUT[]>(keyCount * 2);
for (size_t i = 0; i < eventCount; ++i)
{
auto& input = keyEventList[i];
input.type = INPUT_KEYBOARD;
const bool upEvent = i & 0x1;
input.ki.dwFlags = KEYEVENTF_UNICODE | (upEvent ? KEYEVENTF_KEYUP : 0);
input.ki.dwExtraInfo = KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG;
input.ki.wScan = (*remapping)[i >> 1];
}
UINT res = ii.SendVirtualInput(eventCount, keyEventList.get(), sizeof(INPUT));
std::vector<INPUT> keyEventList;
Helpers::SetTextKeyEvents(keyEventList, *remapping);
ii.SendVirtualInput(keyEventList);
return 1;
}

View File

@ -51,15 +51,13 @@ namespace RemappingLogicTests
// Set the testApp as the foreground process
mockedInputHandler.SetForegroundProcess(testApp1);
const int nInputs = 2;
INPUT input[nInputs] = {};
input[0].type = INPUT_KEYBOARD;
input[0].ki.wVk = VK_CONTROL;
input[1].type = INPUT_KEYBOARD;
input[1].ki.wVk = 0x41;
std::vector<INPUT> inputs{
{ .type = INPUT_KEYBOARD, .ki = { .wVk = VK_CONTROL } },
{ .type = INPUT_KEYBOARD, .ki = { .wVk = 'A' } }
};
// Send Ctrl+A keydown
mockedInputHandler.SendVirtualInput(nInputs, input, sizeof(INPUT));
mockedInputHandler.SendVirtualInput(inputs);
// Ctrl and A key states should be unchanged, Alt and V key states should be true
Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_CONTROL), false);
@ -83,15 +81,13 @@ namespace RemappingLogicTests
// Set the testApp as the foreground process
mockedInputHandler.SetForegroundProcess(testApp2);
const int nInputs = 2;
INPUT input[nInputs] = {};
input[0].type = INPUT_KEYBOARD;
input[0].ki.wVk = VK_CONTROL;
input[1].type = INPUT_KEYBOARD;
input[1].ki.wVk = 0x41;
std::vector<INPUT> inputs{
{ .type = INPUT_KEYBOARD, .ki = { .wVk = VK_CONTROL } },
{ .type = INPUT_KEYBOARD, .ki = { .wVk = 'A' } }
};
// Send Ctrl+A keydown
mockedInputHandler.SendVirtualInput(nInputs, input, sizeof(INPUT));
mockedInputHandler.SendVirtualInput(inputs);
// Ctrl and A key states should be true, Alt and V key states should be false
Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_CONTROL), true);
@ -115,28 +111,24 @@ namespace RemappingLogicTests
// Set the testApp as the foreground process
mockedInputHandler.SetForegroundProcess(testApp1);
const int nInputs = 2;
INPUT input[nInputs] = {};
input[0].type = INPUT_KEYBOARD;
input[0].ki.wVk = VK_CONTROL;
input[1].type = INPUT_KEYBOARD;
input[1].ki.wVk = 0x41;
std::vector<INPUT> inputs1{
{ .type = INPUT_KEYBOARD, .ki = { .wVk = VK_CONTROL } },
{ .type = INPUT_KEYBOARD, .ki = { .wVk = 'A' } }
};
// Send Ctrl+A keydown
mockedInputHandler.SendVirtualInput(nInputs, input, sizeof(INPUT));
mockedInputHandler.SendVirtualInput(inputs1);
// Activated app should be testApp1
Assert::AreEqual(testApp1, testState.GetActivatedApp());
input[0].type = INPUT_KEYBOARD;
input[0].ki.wVk = 0x41;
input[0].ki.dwFlags = KEYEVENTF_KEYUP;
input[1].type = INPUT_KEYBOARD;
input[1].ki.wVk = VK_CONTROL;
input[1].ki.dwFlags = KEYEVENTF_KEYUP;
std::vector<INPUT> inputs2{
{ .type = INPUT_KEYBOARD, .ki = { .wVk = 'A', .dwFlags = KEYEVENTF_KEYUP } },
{ .type = INPUT_KEYBOARD, .ki = { .wVk = VK_CONTROL, .dwFlags = KEYEVENTF_KEYUP } }
};
// Release A then Ctrl
mockedInputHandler.SendVirtualInput(nInputs, input, sizeof(INPUT));
mockedInputHandler.SendVirtualInput(inputs2);
// Activated app should be empty string
Assert::AreEqual(std::wstring(KeyboardManagerConstants::NoActivatedApp), testState.GetActivatedApp());
@ -156,28 +148,24 @@ namespace RemappingLogicTests
// Set the testApp as the foreground process
mockedInputHandler.SetForegroundProcess(testApp1);
const int nInputs = 2;
INPUT input[nInputs] = {};
input[0].type = INPUT_KEYBOARD;
input[0].ki.wVk = VK_CONTROL;
input[1].type = INPUT_KEYBOARD;
input[1].ki.wVk = 0x41;
std::vector<INPUT> inputs1{
{ .type = INPUT_KEYBOARD, .ki = { .wVk = VK_CONTROL } },
{ .type = INPUT_KEYBOARD, .ki = { .wVk = 'A' } }
};
// Send Ctrl+A keydown
mockedInputHandler.SendVirtualInput(nInputs, input, sizeof(INPUT));
mockedInputHandler.SendVirtualInput(inputs1);
// Set the testApp as the foreground process
mockedInputHandler.SetForegroundProcess(testApp2);
input[0].type = INPUT_KEYBOARD;
input[0].ki.wVk = 0x41;
input[0].ki.dwFlags = KEYEVENTF_KEYUP;
input[1].type = INPUT_KEYBOARD;
input[1].ki.wVk = VK_CONTROL;
input[1].ki.dwFlags = KEYEVENTF_KEYUP;
std::vector<INPUT> inputs2{
{ .type = INPUT_KEYBOARD, .ki = { .wVk = 'A', .dwFlags = KEYEVENTF_KEYUP } },
{ .type = INPUT_KEYBOARD, .ki = { .wVk = VK_CONTROL, .dwFlags = KEYEVENTF_KEYUP } }
};
// Release A then Ctrl
mockedInputHandler.SendVirtualInput(nInputs, input, sizeof(INPUT));
mockedInputHandler.SendVirtualInput(inputs2);
// Ctrl, A, Alt and Tab should all be false
Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_CONTROL), false);
@ -198,15 +186,13 @@ namespace RemappingLogicTests
// Set the testApp as the foreground process
mockedInputHandler.SetForegroundProcess(testApp1);
const int nInputs = 2;
INPUT input[nInputs] = {};
input[0].type = INPUT_KEYBOARD;
input[0].ki.wVk = VK_CONTROL;
input[1].type = INPUT_KEYBOARD;
input[1].ki.wVk = 0x41;
std::vector<INPUT> inputs{
{ .type = INPUT_KEYBOARD, .ki = { .wVk = VK_CONTROL } },
{ .type = INPUT_KEYBOARD, .ki = { .wVk = 'A' } }
};
// Send Ctrl+A keydown
mockedInputHandler.SendVirtualInput(nInputs, input, sizeof(INPUT));
mockedInputHandler.SendVirtualInput(inputs);
// Ctrl and A key states should be unchanged, V key states should be true
Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_CONTROL), false);
@ -226,15 +212,13 @@ namespace RemappingLogicTests
// Set the testApp as the foreground process
mockedInputHandler.SetForegroundProcess(testApp2);
const int nInputs = 2;
INPUT input[nInputs] = {};
input[0].type = INPUT_KEYBOARD;
input[0].ki.wVk = VK_CONTROL;
input[1].type = INPUT_KEYBOARD;
input[1].ki.wVk = 0x41;
std::vector<INPUT> inputs{
{ .type = INPUT_KEYBOARD, .ki = { .wVk = VK_CONTROL } },
{ .type = INPUT_KEYBOARD, .ki = { .wVk = 'A' } }
};
// Send Ctrl+A keydown
mockedInputHandler.SendVirtualInput(nInputs, input, sizeof(INPUT));
mockedInputHandler.SendVirtualInput(inputs);
// Ctrl and A key states should be true, V key state should be false
Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_CONTROL), true);
@ -254,28 +238,24 @@ namespace RemappingLogicTests
// Set the testApp as the foreground process
mockedInputHandler.SetForegroundProcess(testApp1);
const int nInputs = 2;
INPUT input[nInputs] = {};
input[0].type = INPUT_KEYBOARD;
input[0].ki.wVk = VK_CONTROL;
input[1].type = INPUT_KEYBOARD;
input[1].ki.wVk = 0x41;
std::vector<INPUT> inputs1{
{ .type = INPUT_KEYBOARD, .ki = { .wVk = VK_CONTROL } },
{ .type = INPUT_KEYBOARD, .ki = { .wVk = 'A' } }
};
// Send Ctrl+A keydown
mockedInputHandler.SendVirtualInput(nInputs, input, sizeof(INPUT));
mockedInputHandler.SendVirtualInput(inputs1);
// Activated app should be testApp1
Assert::AreEqual(testApp1, testState.GetActivatedApp());
input[0].type = INPUT_KEYBOARD;
input[0].ki.wVk = 0x41;
input[0].ki.dwFlags = KEYEVENTF_KEYUP;
input[1].type = INPUT_KEYBOARD;
input[1].ki.wVk = VK_CONTROL;
input[1].ki.dwFlags = KEYEVENTF_KEYUP;
std::vector<INPUT> inputs2{
{ .type = INPUT_KEYBOARD, .ki = { .wVk = 'A', .dwFlags = KEYEVENTF_KEYUP } },
{ .type = INPUT_KEYBOARD, .ki = { .wVk = VK_CONTROL, .dwFlags = KEYEVENTF_KEYUP } }
};
// Release A then Ctrl
mockedInputHandler.SendVirtualInput(nInputs, input, sizeof(INPUT));
mockedInputHandler.SendVirtualInput(inputs2);
// Activated app should be empty string
Assert::AreEqual(std::wstring(KeyboardManagerConstants::NoActivatedApp), testState.GetActivatedApp());
@ -292,28 +272,24 @@ namespace RemappingLogicTests
// Set the testApp as the foreground process
mockedInputHandler.SetForegroundProcess(testApp1);
const int nInputs = 2;
INPUT input[nInputs] = {};
input[0].type = INPUT_KEYBOARD;
input[0].ki.wVk = VK_CONTROL;
input[1].type = INPUT_KEYBOARD;
input[1].ki.wVk = 0x41;
std::vector<INPUT> inputs1{
{ .type = INPUT_KEYBOARD, .ki = { .wVk = VK_CONTROL } },
{ .type = INPUT_KEYBOARD, .ki = { .wVk = 'A' } }
};
// Send Ctrl+A keydown
mockedInputHandler.SendVirtualInput(nInputs, input, sizeof(INPUT));
mockedInputHandler.SendVirtualInput(inputs1);
// Set the testApp as the foreground process
mockedInputHandler.SetForegroundProcess(testApp2);
input[0].type = INPUT_KEYBOARD;
input[0].ki.wVk = 0x41;
input[0].ki.dwFlags = KEYEVENTF_KEYUP;
input[1].type = INPUT_KEYBOARD;
input[1].ki.wVk = VK_CONTROL;
input[1].ki.dwFlags = KEYEVENTF_KEYUP;
std::vector<INPUT> inputs2{
{ .type = INPUT_KEYBOARD, .ki = { .wVk = 'A', .dwFlags = KEYEVENTF_KEYUP } },
{ .type = INPUT_KEYBOARD, .ki = { .wVk = VK_CONTROL, .dwFlags = KEYEVENTF_KEYUP } }
};
// Release A then Ctrl
mockedInputHandler.SendVirtualInput(nInputs, input, sizeof(INPUT));
mockedInputHandler.SendVirtualInput(inputs2);
// Ctrl, A, V should all be false
Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_CONTROL), false);
@ -334,15 +310,13 @@ namespace RemappingLogicTests
// Set the testApp as the foreground process
mockedInputHandler.SetForegroundProcess(testApp1);
const int nInputs = 2;
INPUT input[nInputs] = {};
input[0].type = INPUT_KEYBOARD;
input[0].ki.wVk = VK_CONTROL;
input[1].type = INPUT_KEYBOARD;
input[1].ki.wVk = actionKey;
std::vector<INPUT> inputs{
{ .type = INPUT_KEYBOARD, .ki = { .wVk = VK_CONTROL } },
{ .type = INPUT_KEYBOARD, .ki = { .wVk = actionKey } }
};
// Send Ctrl+A keydown
mockedInputHandler.SendVirtualInput(nInputs, input, sizeof(INPUT));
mockedInputHandler.SendVirtualInput(inputs);
// Check if Ctrl+A is released and disable key was not send
Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_CONTROL), false);

View File

@ -10,15 +10,15 @@ void MockedInput::SetHookProc(std::function<intptr_t(LowlevelKeyboardEvent*)> ho
}
// Function to simulate keyboard input - arguments and return value based on SendInput function (https://learn.microsoft.com/windows/win32/api/winuser/nf-winuser-sendinput)
UINT MockedInput::SendVirtualInput(UINT cInputs, LPINPUT pInputs, int /*cbSize*/)
void MockedInput::SendVirtualInput(const std::vector<INPUT>& inputs)
{
// Iterate over inputs
for (UINT i = 0; i < cInputs; i++)
for (const INPUT& input : inputs)
{
LowlevelKeyboardEvent keyEvent;
LowlevelKeyboardEvent keyEvent{};
// Distinguish between key and sys key by checking if the key is either F10 (for syskeydown) or if the key message is sent while Alt is held down. SYSKEY messages are also sent if there is no window in focus, but that has not been mocked since it would require many changes. More details on key messages at https://learn.microsoft.com/windows/win32/inputdev/wm-syskeydown
if (pInputs[i].ki.dwFlags & KEYEVENTF_KEYUP)
if (input.ki.dwFlags & KEYEVENTF_KEYUP)
{
if (keyboardState[VK_MENU] == true)
{
@ -31,7 +31,7 @@ UINT MockedInput::SendVirtualInput(UINT cInputs, LPINPUT pInputs, int /*cbSize*/
}
else
{
if (pInputs[i].ki.wVk == VK_F10 || keyboardState[VK_MENU] == true)
if (input.ki.wVk == VK_F10 || keyboardState[VK_MENU] == true)
{
keyEvent.wParam = WM_SYSKEYDOWN;
}
@ -43,8 +43,8 @@ UINT MockedInput::SendVirtualInput(UINT cInputs, LPINPUT pInputs, int /*cbSize*/
KBDLLHOOKSTRUCT lParam = {};
// Set only vkCode and dwExtraInfo since other values are unused
lParam.vkCode = pInputs[i].ki.wVk;
lParam.dwExtraInfo = pInputs[i].ki.dwExtraInfo;
lParam.vkCode = input.ki.wVk;
lParam.dwExtraInfo = input.ki.dwExtraInfo;
keyEvent.lParam = &lParam;
// If the SendVirtualInput call condition is true, increment the count. If no condition is set then always increment the count
@ -60,55 +60,53 @@ UINT MockedInput::SendVirtualInput(UINT cInputs, LPINPUT pInputs, int /*cbSize*/
if (result == 0)
{
// If key up flag is set, then set keyboard state to false
keyboardState[pInputs[i].ki.wVk] = (pInputs[i].ki.dwFlags & KEYEVENTF_KEYUP) ? false : true;
keyboardState[input.ki.wVk] = (input.ki.dwFlags & KEYEVENTF_KEYUP) ? false : true;
// Handling modifier key codes
switch (pInputs[i].ki.wVk)
switch (input.ki.wVk)
{
case VK_CONTROL:
if (pInputs[i].ki.dwFlags & KEYEVENTF_KEYUP)
if (input.ki.dwFlags & KEYEVENTF_KEYUP)
{
keyboardState[VK_LCONTROL] = false;
keyboardState[VK_RCONTROL] = false;
}
break;
case VK_LCONTROL:
keyboardState[VK_CONTROL] = (pInputs[i].ki.dwFlags & KEYEVENTF_KEYUP) ? false : true;
keyboardState[VK_CONTROL] = (input.ki.dwFlags & KEYEVENTF_KEYUP) ? false : true;
break;
case VK_RCONTROL:
keyboardState[VK_CONTROL] = (pInputs[i].ki.dwFlags & KEYEVENTF_KEYUP) ? false : true;
keyboardState[VK_CONTROL] = (input.ki.dwFlags & KEYEVENTF_KEYUP) ? false : true;
break;
case VK_MENU:
if (pInputs[i].ki.dwFlags & KEYEVENTF_KEYUP)
if (input.ki.dwFlags & KEYEVENTF_KEYUP)
{
keyboardState[VK_LMENU] = false;
keyboardState[VK_RMENU] = false;
}
break;
case VK_LMENU:
keyboardState[VK_MENU] = (pInputs[i].ki.dwFlags & KEYEVENTF_KEYUP) ? false : true;
keyboardState[VK_MENU] = (input.ki.dwFlags & KEYEVENTF_KEYUP) ? false : true;
break;
case VK_RMENU:
keyboardState[VK_MENU] = (pInputs[i].ki.dwFlags & KEYEVENTF_KEYUP) ? false : true;
keyboardState[VK_MENU] = (input.ki.dwFlags & KEYEVENTF_KEYUP) ? false : true;
break;
case VK_SHIFT:
if (pInputs[i].ki.dwFlags & KEYEVENTF_KEYUP)
if (input.ki.dwFlags & KEYEVENTF_KEYUP)
{
keyboardState[VK_LSHIFT] = false;
keyboardState[VK_RSHIFT] = false;
}
break;
case VK_LSHIFT:
keyboardState[VK_SHIFT] = (pInputs[i].ki.dwFlags & KEYEVENTF_KEYUP) ? false : true;
keyboardState[VK_SHIFT] = (input.ki.dwFlags & KEYEVENTF_KEYUP) ? false : true;
break;
case VK_RSHIFT:
keyboardState[VK_SHIFT] = (pInputs[i].ki.dwFlags & KEYEVENTF_KEYUP) ? false : true;
keyboardState[VK_SHIFT] = (input.ki.dwFlags & KEYEVENTF_KEYUP) ? false : true;
break;
}
}
}
return cInputs;
}
// Function to simulate keyboard hook behavior

View File

@ -34,7 +34,7 @@ namespace KeyboardManagerInput
void SetHookProc(std::function<intptr_t(LowlevelKeyboardEvent*)> hookProcedure);
// Function to simulate keyboard input
UINT SendVirtualInput(UINT cInputs, LPINPUT pInputs, int cbSize);
void SendVirtualInput(const std::vector<INPUT>& inputs);
// Function to simulate keyboard hook behavior
intptr_t MockedKeyboardHook(LowlevelKeyboardEvent* data);

View File

@ -33,20 +33,22 @@ namespace RemappingLogicTests
TEST_METHOD (MockedInput_ShouldSetKeyboardState_OnKeyEvent)
{
// Send key down and key up for A key (0x41) and check keyboard state both times
const int nInputs = 1;
INPUT input[nInputs] = {};
input[0].type = INPUT_KEYBOARD;
input[0].ki.wVk = 0x41;
std::vector<INPUT> inputs1{
{ .type = INPUT_KEYBOARD, .ki = { .wVk = 'A' } },
};
// Send A keydown
mockedInputHandler.SendVirtualInput(nInputs, input, sizeof(INPUT));
mockedInputHandler.SendVirtualInput(inputs1);
// A key state should be true
Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x41), true);
input[0].ki.dwFlags = KEYEVENTF_KEYUP;
std::vector<INPUT> inputs2{
{ .type = INPUT_KEYBOARD, .ki = { .wVk = 'A', .dwFlags = KEYEVENTF_KEYUP } },
};
// Send A keyup
mockedInputHandler.SendVirtualInput(nInputs, input, sizeof(INPUT));
mockedInputHandler.SendVirtualInput(inputs2);
// A key state should be false
Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x41), false);

View File

@ -34,7 +34,7 @@ namespace RemappingLogicTests
TEST_METHOD (SetKeyEvent_ShouldUseExtendedKeyFlag_WhenArgumentIsExtendedKey)
{
const int nInputs = 15;
INPUT input[nInputs] = {};
std::vector<INPUT> inputs;
// List of extended keys
WORD keyCodes[nInputs] = { VK_RCONTROL, VK_RMENU, VK_NUMLOCK, VK_SNAPSHOT, VK_CANCEL, VK_INSERT, VK_HOME, VK_PRIOR, VK_DELETE, VK_END, VK_NEXT, VK_LEFT, VK_DOWN, VK_RIGHT, VK_UP };
@ -42,24 +42,22 @@ namespace RemappingLogicTests
for (int i = 0; i < nInputs; i++)
{
// Set key events for all the extended keys
Helpers::SetKeyEvent(input, i, INPUT_KEYBOARD, keyCodes[i], 0, 0);
Helpers::SetKeyEvent(inputs, INPUT_KEYBOARD, keyCodes[i], 0, 0);
// Extended key flag should be set
Assert::AreEqual(true, bool(input[i].ki.dwFlags & KEYEVENTF_EXTENDEDKEY));
Assert::AreEqual(true, bool(inputs[i].ki.dwFlags & KEYEVENTF_EXTENDEDKEY));
}
}
// Test if SetKeyEvent sets the scan code field to 0 for dummy key
TEST_METHOD (SetKeyEvent_ShouldSetScanCodeFieldTo0_WhenArgumentIsDummyKey)
{
const int nInputs = KeyboardManagerConstants::DUMMY_KEY_EVENT_SIZE;
INPUT input[nInputs] = {};
std::vector<INPUT> inputs{};
int index = 0;
Helpers::SetDummyKeyEvent(input, index, 0);
Helpers::SetDummyKeyEvent(inputs, 0);
// Assert that wScan for both inputs is 0
Assert::AreEqual<unsigned int>(0, input[0].ki.wScan);
Assert::AreEqual<unsigned int>(0, input[1].ki.wScan);
Assert::AreEqual<unsigned int>(0, inputs[0].ki.wScan);
Assert::AreEqual<unsigned int>(0, inputs[1].ki.wScan);
}
};
}

View File

@ -39,22 +39,24 @@ namespace RemappingLogicTests
{
// Remap A to B
testState.AddSingleKeyRemap(0x41, (DWORD)0x42);
const int nInputs = 1;
INPUT input[nInputs] = {};
input[0].type = INPUT_KEYBOARD;
input[0].ki.wVk = 0x41;
std::vector<INPUT> inputs1{
{ .type = INPUT_KEYBOARD, .ki = { .wVk = 'A' } },
};
// Send A keydown
mockedInputHandler.SendVirtualInput(nInputs, input, sizeof(INPUT));
mockedInputHandler.SendVirtualInput(inputs1);
// A key state should be unchanged, and B key state should be true
Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x41), false);
Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x42), true);
input[0].ki.dwFlags = KEYEVENTF_KEYUP;
std::vector<INPUT> inputs2{
{ .type = INPUT_KEYBOARD, .ki = { .wVk = 'A', .dwFlags = KEYEVENTF_KEYUP } },
};
// Send A keyup
mockedInputHandler.SendVirtualInput(nInputs, input, sizeof(INPUT));
mockedInputHandler.SendVirtualInput(inputs2);
// A key state should be unchanged, and B key state should be false
Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x41), false);
@ -66,21 +68,23 @@ namespace RemappingLogicTests
{
// Remap A to VK_DISABLE (disabled)
testState.AddSingleKeyRemap(0x41, CommonSharedConstants::VK_DISABLED);
const int nInputs = 1;
INPUT input[nInputs] = {};
input[0].type = INPUT_KEYBOARD;
input[0].ki.wVk = 0x41;
std::vector<INPUT> inputs1{
{ .type = INPUT_KEYBOARD, .ki = { .wVk = 'A' } },
};
// Send A keydown
mockedInputHandler.SendVirtualInput(nInputs, input, sizeof(INPUT));
mockedInputHandler.SendVirtualInput(inputs1);
// A key state should be unchanged
Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x41), false);
input[0].ki.dwFlags = KEYEVENTF_KEYUP;
std::vector<INPUT> inputs2{
{ .type = INPUT_KEYBOARD, .ki = { .wVk = 'A', .dwFlags = KEYEVENTF_KEYUP } },
};
// Send A keyup
mockedInputHandler.SendVirtualInput(nInputs, input, sizeof(INPUT));
mockedInputHandler.SendVirtualInput(inputs2);
// A key state should be unchanged
Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x41), false);
@ -91,22 +95,24 @@ namespace RemappingLogicTests
{
// Remap A to Common Win key
testState.AddSingleKeyRemap(0x41, CommonSharedConstants::VK_WIN_BOTH);
const int nInputs = 1;
INPUT input[nInputs] = {};
input[0].type = INPUT_KEYBOARD;
input[0].ki.wVk = 0x41;
std::vector<INPUT> inputs1{
{ .type = INPUT_KEYBOARD, .ki = { .wVk = 'A' } },
};
// Send A keydown
mockedInputHandler.SendVirtualInput(nInputs, input, sizeof(INPUT));
mockedInputHandler.SendVirtualInput(inputs1);
// A key state should be unchanged, and common Win key state should be true
Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x41), false);
Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_LWIN), true);
input[0].ki.dwFlags = KEYEVENTF_KEYUP;
std::vector<INPUT> inputs2{
{ .type = INPUT_KEYBOARD, .ki = { .wVk = 'A', .dwFlags = KEYEVENTF_KEYUP } },
};
// Send A keyup
mockedInputHandler.SendVirtualInput(nInputs, input, sizeof(INPUT));
mockedInputHandler.SendVirtualInput(inputs2);
// A key state should be unchanged, and common Win key state should be false
Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x41), false);
@ -126,14 +132,13 @@ namespace RemappingLogicTests
// Remap Caps Lock to Ctrl
testState.AddSingleKeyRemap(VK_CAPITAL, (DWORD)VK_CONTROL);
const int nInputs = 1;
INPUT input[nInputs] = {};
input[0].type = INPUT_KEYBOARD;
input[0].ki.wVk = VK_CAPITAL;
std::vector<INPUT> inputs{
{ .type = INPUT_KEYBOARD, .ki = { .wVk = VK_CAPITAL } },
};
// Send Caps Lock keydown
mockedInputHandler.SendVirtualInput(nInputs, input, sizeof(INPUT));
mockedInputHandler.SendVirtualInput(inputs);
// SendVirtualInput should be called exactly once with the above condition
Assert::AreEqual(1, mockedInputHandler.GetSendVirtualInputCallCount());
@ -152,14 +157,13 @@ namespace RemappingLogicTests
// Remap Ctrl to Caps Lock
testState.AddSingleKeyRemap(VK_CONTROL, (DWORD)VK_CAPITAL);
const int nInputs = 1;
INPUT input[nInputs] = {};
input[0].type = INPUT_KEYBOARD;
input[0].ki.wVk = VK_CONTROL;
std::vector<INPUT> inputs{
{ .type = INPUT_KEYBOARD, .ki = { .wVk = VK_CONTROL } },
};
// Send Ctrl keydown
mockedInputHandler.SendVirtualInput(nInputs, input, sizeof(INPUT));
mockedInputHandler.SendVirtualInput(inputs);
// SendVirtualInput should be called exactly once with the above condition
Assert::AreEqual(1, mockedInputHandler.GetSendVirtualInputCallCount());
@ -182,14 +186,13 @@ namespace RemappingLogicTests
dest.SetKey(VK_SHIFT);
dest.SetKey(0x56);
testState.AddSingleKeyRemap(VK_CAPITAL, dest);
const int nInputs = 1;
INPUT input[nInputs] = {};
input[0].type = INPUT_KEYBOARD;
input[0].ki.wVk = VK_CAPITAL;
std::vector<INPUT> inputs{
{ .type = INPUT_KEYBOARD, .ki = { .wVk = VK_CAPITAL } },
};
// Send Caps Lock keydown
mockedInputHandler.SendVirtualInput(nInputs, input, sizeof(INPUT));
mockedInputHandler.SendVirtualInput(inputs);
// SendVirtualInput should be called exactly twice with the above condition
Assert::AreEqual(2, mockedInputHandler.GetSendVirtualInputCallCount());
@ -211,14 +214,13 @@ namespace RemappingLogicTests
dest.SetKey(VK_CONTROL);
dest.SetKey(VK_CAPITAL);
testState.AddSingleKeyRemap(VK_CONTROL, dest);
const int nInputs = 1;
INPUT input[nInputs] = {};
input[0].type = INPUT_KEYBOARD;
input[0].ki.wVk = VK_CONTROL;
std::vector<INPUT> inputs{
{ .type = INPUT_KEYBOARD, .ki = { .wVk = VK_CONTROL } },
};
// Send Ctrl keydown
mockedInputHandler.SendVirtualInput(nInputs, input, sizeof(INPUT));
mockedInputHandler.SendVirtualInput(inputs);
// SendVirtualInput should be called exactly once with the above condition
Assert::AreEqual(1, mockedInputHandler.GetSendVirtualInputCallCount());
@ -232,23 +234,25 @@ namespace RemappingLogicTests
dest.SetKey(VK_CONTROL);
dest.SetKey(0x56);
testState.AddSingleKeyRemap(0x41, dest);
const int nInputs = 1;
INPUT input[nInputs] = {};
input[0].type = INPUT_KEYBOARD;
input[0].ki.wVk = 0x41;
std::vector<INPUT> inputs1{
{ .type = INPUT_KEYBOARD, .ki = { .wVk = 'A' } },
};
// Send A keydown
mockedInputHandler.SendVirtualInput(nInputs, input, sizeof(INPUT));
mockedInputHandler.SendVirtualInput(inputs1);
// A key state should be unchanged, and Ctrl, V key state should be true
Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x41), false);
Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_CONTROL), true);
Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x56), true);
input[0].ki.dwFlags = KEYEVENTF_KEYUP;
std::vector<INPUT> inputs2{
{ .type = INPUT_KEYBOARD, .ki = { .wVk = 'A', .dwFlags = KEYEVENTF_KEYUP } },
};
// Send A keyup
mockedInputHandler.SendVirtualInput(nInputs, input, sizeof(INPUT));
mockedInputHandler.SendVirtualInput(inputs2);
// A key state should be unchanged, and Ctrl, V key state should be false
Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x41), false);
@ -265,24 +269,26 @@ namespace RemappingLogicTests
dest.SetKey(VK_SHIFT);
dest.SetKey(0x56);
testState.AddSingleKeyRemap(0x41, dest);
const int nInputs = 1;
INPUT input[nInputs] = {};
input[0].type = INPUT_KEYBOARD;
input[0].ki.wVk = 0x41;
std::vector<INPUT> inputs1{
{ .type = INPUT_KEYBOARD, .ki = { .wVk = 'A' } },
};
// Send A keydown
mockedInputHandler.SendVirtualInput(nInputs, input, sizeof(INPUT));
mockedInputHandler.SendVirtualInput(inputs1);
// A key state should be unchanged, and Ctrl, Shift, V key state should be true
Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x41), false);
Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_CONTROL), true);
Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_SHIFT), true);
Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x56), true);
input[0].ki.dwFlags = KEYEVENTF_KEYUP;
std::vector<INPUT> inputs2{
{ .type = INPUT_KEYBOARD, .ki = { .wVk = 'A', .dwFlags = KEYEVENTF_KEYUP } },
};
// Send A keyup
mockedInputHandler.SendVirtualInput(nInputs, input, sizeof(INPUT));
mockedInputHandler.SendVirtualInput(inputs2);
// A key state should be unchanged, and Ctrl, Shift, V key state should be false
Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x41), false);
@ -299,22 +305,24 @@ namespace RemappingLogicTests
dest.SetKey(VK_LCONTROL);
dest.SetKey(0x56);
testState.AddSingleKeyRemap(VK_LCONTROL, dest);
const int nInputs = 1;
INPUT input[nInputs] = {};
input[0].type = INPUT_KEYBOARD;
input[0].ki.wVk = VK_LCONTROL;
std::vector<INPUT> inputs1{
{ .type = INPUT_KEYBOARD, .ki = { .wVk = VK_LCONTROL } },
};
// Send LCtrl keydown
mockedInputHandler.SendVirtualInput(nInputs, input, sizeof(INPUT));
mockedInputHandler.SendVirtualInput(inputs1);
// LCtrl, V key state should be true
Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_LCONTROL), true);
Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x56), true);
input[0].ki.dwFlags = KEYEVENTF_KEYUP;
std::vector<INPUT> inputs2{
{ .type = INPUT_KEYBOARD, .ki = { .wVk = VK_LCONTROL, .dwFlags = KEYEVENTF_KEYUP } },
};
// Send LCtrl keyup
mockedInputHandler.SendVirtualInput(nInputs, input, sizeof(INPUT));
mockedInputHandler.SendVirtualInput(inputs2);
// LCtrl, V key state should be false
Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_LCONTROL), false);

View File

@ -154,29 +154,29 @@ namespace Helpers
}
// Function to set the value of a key event based on the arguments
void SetKeyEvent(LPINPUT keyEventArray, int index, DWORD inputType, WORD keyCode, DWORD flags, ULONG_PTR extraInfo)
void SetKeyEvent(std::vector<INPUT>& keyEventArray, DWORD inputType, WORD keyCode, DWORD flags, ULONG_PTR extraInfo)
{
keyEventArray[index].type = inputType;
keyEventArray[index].ki.wVk = keyCode;
keyEventArray[index].ki.dwFlags = flags;
INPUT keyEvent{};
keyEvent.type = inputType;
keyEvent.ki.wVk = keyCode;
keyEvent.ki.dwFlags = flags;
if (IsExtendedKey(keyCode))
{
keyEventArray[index].ki.dwFlags |= KEYEVENTF_EXTENDEDKEY;
keyEvent.ki.dwFlags |= KEYEVENTF_EXTENDEDKEY;
}
keyEventArray[index].ki.dwExtraInfo = extraInfo;
keyEvent.ki.dwExtraInfo = extraInfo;
// Set wScan to the value from MapVirtualKey as some applications may use the scan code for handling input, for instance, Windows Terminal ignores non-character input which has scancode set to 0.
// MapVirtualKey returns 0 if the key code does not correspond to a physical key (such as unassigned/reserved keys). More details at https://github.com/microsoft/PowerToys/pull/7143#issue-498877747
keyEventArray[index].ki.wScan = static_cast<WORD>(MapVirtualKey(keyCode, MAPVK_VK_TO_VSC));
keyEvent.ki.wScan = static_cast<WORD>(MapVirtualKey(keyCode, MAPVK_VK_TO_VSC));
keyEventArray.push_back(keyEvent);
}
// Function to set the dummy key events used for remapping shortcuts, required to ensure releasing a modifier doesn't trigger another action (For example, Win->Start Menu or Alt->Menu bar)
void SetDummyKeyEvent(LPINPUT keyEventArray, int& index, ULONG_PTR extraInfo)
void SetDummyKeyEvent(std::vector<INPUT>& keyEventArray, ULONG_PTR extraInfo)
{
SetKeyEvent(keyEventArray, index, INPUT_KEYBOARD, static_cast<WORD>(KeyboardManagerConstants::DUMMY_KEY), 0, extraInfo);
index++;
SetKeyEvent(keyEventArray, index, INPUT_KEYBOARD, static_cast<WORD>(KeyboardManagerConstants::DUMMY_KEY), KEYEVENTF_KEYUP, extraInfo);
index++;
SetKeyEvent(keyEventArray, INPUT_KEYBOARD, static_cast<WORD>(KeyboardManagerConstants::DUMMY_KEY), 0, extraInfo);
SetKeyEvent(keyEventArray, INPUT_KEYBOARD, static_cast<WORD>(KeyboardManagerConstants::DUMMY_KEY), KEYEVENTF_KEYUP, extraInfo);
}
// Function to return window handle for a full screen UWP app
@ -240,7 +240,7 @@ namespace Helpers
}
// Function to set key events for modifier keys: When shortcutToCompare is passed (non-empty shortcut), then the key event is sent only if both shortcut's don't have the same modifier key. When keyToBeReleased is passed (non-NULL), then the key event is sent if either the shortcuts don't have the same modifier or if the shortcutToBeSent's modifier matches the keyToBeReleased
void SetModifierKeyEvents(const Shortcut& shortcutToBeSent, const ModifierKey& winKeyInvoked, LPINPUT keyEventArray, int& index, bool isKeyDown, ULONG_PTR extraInfoFlag, const Shortcut& shortcutToCompare, const DWORD& keyToBeReleased)
void SetModifierKeyEvents(const Shortcut& shortcutToBeSent, const ModifierKey& winKeyInvoked, std::vector<INPUT>& keyEventArray, bool isKeyDown, ULONG_PTR extraInfoFlag, const Shortcut& shortcutToCompare, const DWORD& keyToBeReleased)
{
// If key down is to be sent, send in the order Win, Ctrl, Alt, Shift
if (isKeyDown)
@ -248,23 +248,19 @@ namespace Helpers
// If shortcutToCompare is non-empty, then the key event is sent only if both shortcut's don't have the same modifier key. If keyToBeReleased is non-NULL, then the key event is sent if either the shortcuts don't have the same modifier or if the shortcutToBeSent's modifier matches the keyToBeReleased
if (shortcutToBeSent.GetWinKey(winKeyInvoked) != NULL && (shortcutToCompare.IsEmpty() || shortcutToBeSent.GetWinKey(winKeyInvoked) != shortcutToCompare.GetWinKey(winKeyInvoked)) && (keyToBeReleased == NULL || !shortcutToBeSent.CheckWinKey(keyToBeReleased)))
{
Helpers::SetKeyEvent(keyEventArray, index, INPUT_KEYBOARD, static_cast<WORD>(shortcutToBeSent.GetWinKey(winKeyInvoked)), 0, extraInfoFlag);
index++;
Helpers::SetKeyEvent(keyEventArray, INPUT_KEYBOARD, static_cast<WORD>(shortcutToBeSent.GetWinKey(winKeyInvoked)), 0, extraInfoFlag);
}
if (shortcutToBeSent.GetCtrlKey() != NULL && (shortcutToCompare.IsEmpty() || shortcutToBeSent.GetCtrlKey() != shortcutToCompare.GetCtrlKey()) && (keyToBeReleased == NULL || !shortcutToBeSent.CheckCtrlKey(keyToBeReleased)))
{
Helpers::SetKeyEvent(keyEventArray, index, INPUT_KEYBOARD, static_cast<WORD>(shortcutToBeSent.GetCtrlKey()), 0, extraInfoFlag);
index++;
Helpers::SetKeyEvent(keyEventArray, INPUT_KEYBOARD, static_cast<WORD>(shortcutToBeSent.GetCtrlKey()), 0, extraInfoFlag);
}
if (shortcutToBeSent.GetAltKey() != NULL && (shortcutToCompare.IsEmpty() || shortcutToBeSent.GetAltKey() != shortcutToCompare.GetAltKey()) && (keyToBeReleased == NULL || !shortcutToBeSent.CheckAltKey(keyToBeReleased)))
{
Helpers::SetKeyEvent(keyEventArray, index, INPUT_KEYBOARD, static_cast<WORD>(shortcutToBeSent.GetAltKey()), 0, extraInfoFlag);
index++;
Helpers::SetKeyEvent(keyEventArray, INPUT_KEYBOARD, static_cast<WORD>(shortcutToBeSent.GetAltKey()), 0, extraInfoFlag);
}
if (shortcutToBeSent.GetShiftKey() != NULL && (shortcutToCompare.IsEmpty() || shortcutToBeSent.GetShiftKey() != shortcutToCompare.GetShiftKey()) && (keyToBeReleased == NULL || !shortcutToBeSent.CheckShiftKey(keyToBeReleased)))
{
Helpers::SetKeyEvent(keyEventArray, index, INPUT_KEYBOARD, static_cast<WORD>(shortcutToBeSent.GetShiftKey()), 0, extraInfoFlag);
index++;
Helpers::SetKeyEvent(keyEventArray, INPUT_KEYBOARD, static_cast<WORD>(shortcutToBeSent.GetShiftKey()), 0, extraInfoFlag);
}
}
@ -274,23 +270,35 @@ namespace Helpers
// If shortcutToCompare is non-empty, then the key event is sent only if both shortcut's don't have the same modifier key. If keyToBeReleased is non-NULL, then the key event is sent if either the shortcuts don't have the same modifier or if the shortcutToBeSent's modifier matches the keyToBeReleased
if (shortcutToBeSent.GetShiftKey() != NULL && (shortcutToCompare.IsEmpty() || shortcutToBeSent.GetShiftKey() != shortcutToCompare.GetShiftKey() || shortcutToBeSent.CheckShiftKey(keyToBeReleased)))
{
Helpers::SetKeyEvent(keyEventArray, index, INPUT_KEYBOARD, static_cast<WORD>(shortcutToBeSent.GetShiftKey()), KEYEVENTF_KEYUP, extraInfoFlag);
index++;
Helpers::SetKeyEvent(keyEventArray, INPUT_KEYBOARD, static_cast<WORD>(shortcutToBeSent.GetShiftKey()), KEYEVENTF_KEYUP, extraInfoFlag);
}
if (shortcutToBeSent.GetAltKey() != NULL && (shortcutToCompare.IsEmpty() || shortcutToBeSent.GetAltKey() != shortcutToCompare.GetAltKey() || shortcutToBeSent.CheckAltKey(keyToBeReleased)))
{
Helpers::SetKeyEvent(keyEventArray, index, INPUT_KEYBOARD, static_cast<WORD>(shortcutToBeSent.GetAltKey()), KEYEVENTF_KEYUP, extraInfoFlag);
index++;
Helpers::SetKeyEvent(keyEventArray, INPUT_KEYBOARD, static_cast<WORD>(shortcutToBeSent.GetAltKey()), KEYEVENTF_KEYUP, extraInfoFlag);
}
if (shortcutToBeSent.GetCtrlKey() != NULL && (shortcutToCompare.IsEmpty() || shortcutToBeSent.GetCtrlKey() != shortcutToCompare.GetCtrlKey() || shortcutToBeSent.CheckCtrlKey(keyToBeReleased)))
{
Helpers::SetKeyEvent(keyEventArray, index, INPUT_KEYBOARD, static_cast<WORD>(shortcutToBeSent.GetCtrlKey()), KEYEVENTF_KEYUP, extraInfoFlag);
index++;
Helpers::SetKeyEvent(keyEventArray, INPUT_KEYBOARD, static_cast<WORD>(shortcutToBeSent.GetCtrlKey()), KEYEVENTF_KEYUP, extraInfoFlag);
}
if (shortcutToBeSent.GetWinKey(winKeyInvoked) != NULL && (shortcutToCompare.IsEmpty() || shortcutToBeSent.GetWinKey(winKeyInvoked) != shortcutToCompare.GetWinKey(winKeyInvoked) || shortcutToBeSent.CheckWinKey(keyToBeReleased)))
{
Helpers::SetKeyEvent(keyEventArray, index, INPUT_KEYBOARD, static_cast<WORD>(shortcutToBeSent.GetWinKey(winKeyInvoked)), KEYEVENTF_KEYUP, extraInfoFlag);
index++;
Helpers::SetKeyEvent(keyEventArray, INPUT_KEYBOARD, static_cast<WORD>(shortcutToBeSent.GetWinKey(winKeyInvoked)), KEYEVENTF_KEYUP, extraInfoFlag);
}
}
}
void SetTextKeyEvents(std::vector<INPUT>& keyEventArray, const std::wstring& remapping)
{
for (wchar_t c : remapping)
{
for (DWORD flag : { 0, KEYEVENTF_KEYUP })
{
INPUT input{};
input.type = INPUT_KEYBOARD;
input.ki.dwFlags = KEYEVENTF_UNICODE | flag;
input.ki.dwExtraInfo = KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG;
input.ki.wScan = c;
keyEventArray.push_back(input);
}
}
}

View File

@ -31,10 +31,13 @@ namespace Helpers
KeyType GetKeyType(DWORD key);
// Function to set the value of a key event based on the arguments
void SetKeyEvent(LPINPUT keyEventArray, int index, DWORD inputType, WORD keyCode, DWORD flags, ULONG_PTR extraInfo);
void SetKeyEvent(std::vector<INPUT>& keyEventArray, DWORD inputType, WORD keyCode, DWORD flags, ULONG_PTR extraInfo);
// Function to set the dummy key events used for remapping shortcuts, required to ensure releasing a modifier doesn't trigger another action (For example, Win->Start Menu or Alt->Menu bar)
void SetDummyKeyEvent(LPINPUT keyEventArray, int& index, ULONG_PTR extraInfo);
void SetDummyKeyEvent(std::vector<INPUT>& keyEventArray, ULONG_PTR extraInfo);
// Function to set key events for remapping text.
void SetTextKeyEvents(std::vector<INPUT>& keyEventArray, const std::wstring& remapping);
// Function to return window handle for a full screen UWP app
HWND GetFullscreenUWPWindowHandle();
@ -43,7 +46,7 @@ namespace Helpers
std::wstring GetCurrentApplication(bool keepPath);
// Function to set key events for modifier keys: When shortcutToCompare is passed (non-empty shortcut), then the key event is sent only if both shortcut's don't have the same modifier key. When keyToBeReleased is passed (non-NULL), then the key event is sent if either the shortcuts don't have the same modifier or if the shortcutToBeSent's modifier matches the keyToBeReleased
void SetModifierKeyEvents(const Shortcut& shortcutToBeSent, const ModifierKey& winKeyInvoked, LPINPUT keyEventArray, int& index, bool isKeyDown, ULONG_PTR extraInfoFlag, const Shortcut& shortcutToCompare = Shortcut(), const DWORD& keyToBeReleased = NULL);
void SetModifierKeyEvents(const Shortcut& shortcutToBeSent, const ModifierKey& winKeyInvoked, std::vector<INPUT>& keyEventArray, bool isKeyDown, ULONG_PTR extraInfoFlag, const Shortcut& shortcutToCompare = Shortcut(), const DWORD& keyToBeReleased = NULL);
// Function to filter the key codes for artificial key codes
int32_t FilterArtificialKeys(const int32_t& key);

View File

@ -1,7 +1,9 @@
#pragma once
#include <keyboardmanager/common/InputInterface.h>
#include <common/logger/logger.h>
#include <common/utils/winapi_error.h>
#include <keyboardmanager/common/Helpers.h>
#include <keyboardmanager/common/InputInterface.h>
namespace KeyboardManagerInput
{
@ -10,9 +12,16 @@ namespace KeyboardManagerInput
{
public:
// Function to simulate input
UINT SendVirtualInput(UINT cInputs, LPINPUT pInputs, int cbSize)
void SendVirtualInput(const std::vector<INPUT>& inputs)
{
return SendInput(cInputs, pInputs, cbSize);
std::vector<INPUT> copy = inputs;
UINT eventCount = SendInput(static_cast<UINT>(copy.size()), copy.data(), sizeof(INPUT));
if (eventCount != copy.size())
{
Logger::error(
L"Failed to send input events. {}",
get_last_error_or_default(GetLastError()));
}
}
// Function to get the state of a particular key

View File

@ -1,5 +1,9 @@
#pragma once
#include <string>
#include <vector>
#include <Windows.h>
namespace KeyboardManagerInput
{
// Interface used to wrap keyboard input library methods
@ -7,7 +11,7 @@ namespace KeyboardManagerInput
{
public:
// Function to simulate input
virtual UINT SendVirtualInput(UINT cInputs, LPINPUT pInputs, int cbSize) = 0;
virtual void SendVirtualInput(const std::vector<INPUT>& inputs) = 0;
// Function to get the state of a particular key
virtual bool GetVirtualKeyState(int key) = 0;

View File

@ -10,14 +10,11 @@ namespace KeyboardEventHandlers
void SetNumLockToPreviousState(KeyboardManagerInput::InputInterface& ii)
{
// Num Lock's key state is applied before it is intercepted by low level keyboard hooks, so we have to manually set back the state when we suppress the key. This is done by sending an additional key up, key down set of messages.
// We need 2 key events because after Num Lock is suppressed, key up to release num lock key and key down to revert the num lock state
int key_count = 2;
LPINPUT keyEventList = new INPUT[size_t(key_count)]{};
std::vector<INPUT> keyEventList;
// Use the suppress flag to ensure these are not intercepted by any remapped keys or shortcuts
Helpers::SetKeyEvent(keyEventList, 0, INPUT_KEYBOARD, VK_NUMLOCK, KEYEVENTF_KEYUP, KeyboardManagerConstants::KEYBOARDMANAGER_SUPPRESS_FLAG);
Helpers::SetKeyEvent(keyEventList, 1, INPUT_KEYBOARD, VK_NUMLOCK, 0, KeyboardManagerConstants::KEYBOARDMANAGER_SUPPRESS_FLAG);
ii.SendVirtualInput(static_cast<UINT>(key_count), keyEventList, sizeof(INPUT));
delete[] keyEventList;
Helpers::SetKeyEvent(keyEventList, INPUT_KEYBOARD, VK_NUMLOCK, KEYEVENTF_KEYUP, KeyboardManagerConstants::KEYBOARDMANAGER_SUPPRESS_FLAG);
Helpers::SetKeyEvent(keyEventList, INPUT_KEYBOARD, VK_NUMLOCK, 0, KeyboardManagerConstants::KEYBOARDMANAGER_SUPPRESS_FLAG);
ii.SendVirtualInput(keyEventList);
}
}

View File

@ -44,13 +44,13 @@ namespace Community.PowerToys.Run.Plugin.UnitConverter
return first.Value;
}
if (UnitParser.Default.TryParse(unit, unitInfo.UnitType, out Enum enum_unit))
if (UnitsNetSetup.Default.UnitParser.TryParse(unit, unitInfo.UnitType, out Enum enum_unit))
{
return enum_unit;
}
var cultureInfoEnglish = new System.Globalization.CultureInfo("en-US");
if (UnitParser.Default.TryParse(unit, unitInfo.UnitType, cultureInfoEnglish, out Enum enum_unit_en))
if (UnitsNetSetup.Default.UnitParser.TryParse(unit, unitInfo.UnitType, cultureInfoEnglish, out Enum enum_unit_en))
{
return enum_unit_en;
}

View File

@ -19,13 +19,13 @@ namespace Microsoft.Plugin.WindowWalker.Components
/// <param name="hwnd">handle to the window to exclude</param>
public static void SetWindowExclusionFromLivePreview(IntPtr hwnd)
{
int renderPolicy = (int)DwmNCRenderingPolicies.Enabled;
uint renderPolicy = (uint)DwmNCRenderingPolicies.Enabled;
_ = NativeMethods.DwmSetWindowAttribute(
hwnd,
12,
ref renderPolicy,
sizeof(int));
sizeof(uint));
}
/// <summary>

View File

@ -32,9 +32,9 @@
<Grid x:Name="RootGrid" MouseDown="OnMouseDown">
<!-- We set the background here because the Acrylic can be too translucent / background too bright on Light theme -->
<!--<Grid.Background>
<Grid.Background>
<SolidColorBrush Opacity="0.8" Color="{DynamicResource ApplicationBackgroundColor}" />
</Grid.Background>-->
</Grid.Background>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
@ -45,11 +45,6 @@
Grid.Row="0"
Padding="12,4,12,3">
<local:LauncherControl x:Name="SearchBox" />
<Border.Background>
<!-- Setting the background of the search bar to fix https://github.com/microsoft/PowerToys/issues/30206 -->
<!-- The title bar accent would bleed if the option to "Show accent color on title bars and windows borders" is enabled on Windows -->
<SolidColorBrush Opacity="1" Color="{DynamicResource ApplicationBackgroundColor}" />
</Border.Background>
</Border>
<!-- Have to use a Grid instead of a StackPanel for scrolling to work? -->

View File

@ -63,7 +63,7 @@ namespace PowerLauncher
if (OSVersionHelper.IsWindows11())
{
WindowBackdropType = Wpf.Ui.Controls.WindowBackdropType.Mica;
WindowBackdropType = Wpf.Ui.Controls.WindowBackdropType.Acrylic;
}
else
{

View File

@ -8,11 +8,13 @@ using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using System.Windows.Interop;
using System.Windows.Threading;
using Common.UI;
using interop;
@ -1006,6 +1008,46 @@ namespace PowerLauncher.ViewModel
if (MainWindowVisibility != Visibility.Visible)
{
MainWindowVisibility = Visibility.Visible;
// HACK: The following code in this if is a fix for https://github.com/microsoft/PowerToys/issues/30206 and https://github.com/microsoft/PowerToys/issues/33135
// WPF UI theme watcher removes the composition target background color, among other weird stuff.
// https://github.com/lepoco/wpfui/blob/303f0aefcd59a142bc681415dc4360a34a15f33d/src/Wpf.Ui/Controls/Window/WindowBackdrop.cs#L280
// So we set it back with https://github.com/lepoco/wpfui/blob/303f0aefcd59a142bc681415dc4360a34a15f33d/src/Wpf.Ui/Controls/Window/WindowBackdrop.cs#L191
var window = Application.Current.MainWindow;
// Only makes sense for Windows 11 or greater, since Windows 10 doesn't have Mica.
if (OSVersionHelper.IsWindows11())
{
Wpf.Ui.Controls.WindowBackdrop.RemoveBackground(window);
}
// Setting uint titlebarPvAttribute = 0xFFFFFFFE; works on 22H2 or higher, 21H2 (aka SV1) this value causes a crash
if (OSVersionHelper.IsGreaterThanWindows11_21H2())
{
// Taken from WPFUI's fix for the title bar issue. We should be able to remove this fix when WPF UI 4 is integrated.
// https://github.com/lepoco/wpfui/pull/1122/files#diff-196b404f4db09632665ef546da6c8e57302b2f3e3d082eb4b5c295ae3482d94a
IntPtr windowHandle = new WindowInteropHelper(window).Handle;
if (windowHandle == IntPtr.Zero)
{
return;
}
HwndSource windowSource = HwndSource.FromHwnd(windowHandle);
// Remove background from client area
if (windowSource != null && windowSource.Handle != IntPtr.Zero && windowSource?.CompositionTarget != null)
{
// NOTE: https://learn.microsoft.com/en-us/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute
// Specifying DWMWA_COLOR_DEFAULT (value 0xFFFFFFFF) for the color will reset the window back to using the system's default behavior for the caption color.
uint titlebarPvAttribute = 0xFFFFFFFE;
_ = Wox.Plugin.Common.Win32.NativeMethods.DwmSetWindowAttribute(
windowSource.Handle,
(int)Wox.Plugin.Common.Win32.DwmWindowAttributes.CaptionColor, // CaptionColor attribute is only available on Windows 11.
ref titlebarPvAttribute,
Marshal.SizeOf(typeof(uint)));
}
}
}
else
{

View File

@ -66,7 +66,7 @@ namespace Wox.Plugin.Common.Win32
public static extern int DwmpActivateLivePreview([MarshalAs(UnmanagedType.Bool)] bool fActivate, IntPtr hWndExclude, IntPtr hWndInsertBefore, LivePreviewTrigger lpt, IntPtr prcFinalRect);
[DllImport("dwmapi.dll", PreserveSig = false)]
public static extern int DwmSetWindowAttribute(IntPtr hwnd, int attr, ref int attrValue, int attrSize);
public static extern int DwmSetWindowAttribute(IntPtr hwnd, int attr, ref uint attrValue, int attrSize);
[DllImport("dwmapi.dll", PreserveSig = false)]
public static extern int DwmGetWindowAttribute(IntPtr hwnd, int dwAttribute, out int pvAttribute, int cbAttribute);
@ -493,28 +493,28 @@ namespace Wox.Plugin.Common.Win32
public enum DwmWindowAttributes
{
NCRenderingEnabled = 1,
NCRenderingPolicy,
TransitionsForceDisabled,
AllowNCPaint,
CaptionButtonBounds,
NonClientRtlLayout,
ForceIconicRepresentation,
Flip3DPolicy,
ExtendedFrameBounds,
HasIconicBitmap,
DisallowPeek,
ExcludedFromPeek,
Cloak,
Cloaked,
FreezeRepresentation,
PassiveUpdateMode,
UseHostbackdropbrush,
UseImmersiveDarkMode,
WindowCornerPreference,
BorderColor,
CaptionColor,
TextColor,
VisibleFrameBorderThickness,
NCRenderingPolicy = 2,
TransitionsForceDisabled = 3,
AllowNCPaint = 4,
CaptionButtonBounds = 5,
NonClientRtlLayout = 6,
ForceIconicRepresentation = 7,
Flip3DPolicy = 8,
ExtendedFrameBounds = 9,
HasIconicBitmap = 10,
DisallowPeek = 11,
ExcludedFromPeek = 12,
Cloak = 13,
Cloaked = 14,
FreezeRepresentation = 15,
PassiveUpdateMode = 16,
UseHostbackdropbrush = 17,
UseImmersiveDarkMode = 20,
WindowCornerPreference = 33,
BorderColor = 34,
CaptionColor = 35,
TextColor = 36,
VisibleFrameBorderThickness = 37,
Last,
}

View File

@ -74,6 +74,9 @@
<!-- This package is a dependency of System.Drawing.Common, but we need to set it here so we can exclude the assets, so it doesn't conflict with the 8.0.1 dll coming from .NET SDK. -->
<ExcludeAssets>runtime</ExcludeAssets> <!-- Should already be present on .net sdk runtime, so we avoid the conflicting runtime version from nuget -->
</PackageReference>
<PackageReference Include="System.Drawing.Common">
<ExcludeAssets>runtime</ExcludeAssets> <!-- Should already be present on .net sdk runtime, so we avoid the conflicting runtime version from nuget -->
</PackageReference>
<PackageReference Include="System.CodeDom">
<!-- This package is a dependency of System.Management, but we need to set it here so we can exclude the assets, so it doesn't conflict with the 8.0.1 dll coming from .NET SDK. -->
<ExcludeAssets>runtime</ExcludeAssets> <!-- Should already be present on .net sdk runtime, so we avoid the conflicting runtime version from nuget -->

View File

@ -0,0 +1,12 @@
// 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;
namespace Peek.Common.Helpers;
public static class PathHelper
{
public static bool IsUncPath(string path) => Uri.TryCreate(path, UriKind.Absolute, out Uri? uri) && uri != null && uri.IsUnc;
}

View File

@ -0,0 +1,26 @@
// 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.Threading;
namespace Peek.Common.Helpers;
public static class ThreadHelper
{
public static void RunOnSTAThread(ThreadStart action)
{
if (Thread.CurrentThread.GetApartmentState() == ApartmentState.STA)
{
action();
}
else
{
Thread staThread = new(action);
staThread.SetApartmentState(ApartmentState.STA);
staThread.Start();
staThread.Join();
}
}
}

View File

@ -24,6 +24,8 @@ namespace Peek.Common.Models
public string Name { get; init; }
public string ParsingName => string.Empty;
public string Path { get; init; }
public string Extension => System.IO.Path.GetExtension(Path).ToLower(CultureInfo.InvariantCulture);

View File

@ -11,19 +11,15 @@ using Windows.Storage;
namespace Peek.Common.Models
{
public class FolderItem : IFileSystemItem
public class FolderItem(string path, string name, string parsingName) : IFileSystemItem
{
private StorageFolder? storageFolder;
public FolderItem(string path, string name)
{
Path = path;
Name = name;
}
public string Name { get; init; } = name;
public string Name { get; init; }
public string ParsingName { get; init; } = parsingName;
public string Path { get; init; }
public string Path { get; init; } = path;
public string Extension => string.Empty;
@ -38,7 +34,7 @@ namespace Peek.Common.Models
{
try
{
storageFolder = await StorageFolder.GetFolderFromPathAsync(Path);
storageFolder = string.IsNullOrEmpty(Path) ? null : await StorageFolder.GetFolderFromPathAsync(Path);
}
catch (Exception ex)
{

View File

@ -17,25 +17,24 @@ namespace Peek.Common.Models
{
get
{
DateTime? dateModified;
try
{
dateModified = System.IO.File.GetCreationTime(Path);
return string.IsNullOrEmpty(Path) ? null : System.IO.File.GetCreationTime(Path);
}
catch
{
dateModified = null;
return null;
}
return dateModified;
}
}
public string Extension { get; }
public string Name { get; init; }
public string Name { get; }
public string Path { get; init; }
public string ParsingName { get; }
public string Path { get; }
public ulong FileSizeBytes => PropertyStoreHelper.TryGetUlongProperty(Path, PropertyKey.FileSizeBytes) ?? 0;

View File

@ -0,0 +1,57 @@
<!-- Copyright (c) Microsoft Corporation and Contributors. -->
<!-- Licensed under the MIT License. -->
<UserControl
x:Class="Peek.FilePreviewer.Controls.SpecialFolderInformationalPreviewControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid ColumnSpacing="24">
<Grid.ColumnDefinitions>
<ColumnDefinition x:Name="Icon" Width="Auto" />
<ColumnDefinition x:Name="FileInfo" Width="*" />
</Grid.ColumnDefinitions>
<Image
x:Name="FileIcon"
Grid.Column="0"
Width="180"
Height="180"
Source="{x:Bind Source.IconPreview, Mode=OneWay}" />
<StackPanel
Grid.Column="1"
VerticalAlignment="Center"
Spacing="4">
<TextBlock
MaxLines="3"
Style="{StaticResource TitleTextBlockStyle}"
Text="{x:Bind Source.FileName, Mode=OneWay}"
TextTrimming="CharacterEllipsis"
TextWrapping="Wrap">
<ToolTipService.ToolTip>
<ToolTip Content="{x:Bind Source.FileName, Mode=OneWay}" />
</ToolTipService.ToolTip>
</TextBlock>
<TextBlock Text="{x:Bind FormatFileType(Source.FileType), Mode=OneWay}">
<ToolTipService.ToolTip>
<ToolTip Content="{x:Bind FormatFileType(Source.FileType), Mode=OneWay}" />
</ToolTipService.ToolTip>
</TextBlock>
<TextBlock Text="{x:Bind FormatFileSize(Source.FileSize), Mode=OneWay}">
<ToolTipService.ToolTip>
<ToolTip Content="{x:Bind FormatFileSize(Source.FileSize), Mode=OneWay}" />
</ToolTipService.ToolTip>
</TextBlock>
<TextBlock Text="{x:Bind FormatFileDateModified(Source.DateModified), Mode=OneWay}">
<ToolTipService.ToolTip>
<ToolTip Content="{x:Bind FormatFileDateModified(Source.DateModified), Mode=OneWay}" />
</ToolTipService.ToolTip>
</TextBlock>
</StackPanel>
</Grid>
</UserControl>

View File

@ -0,0 +1,41 @@
// 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 Peek.Common.Helpers;
using Peek.FilePreviewer.Models;
namespace Peek.FilePreviewer.Controls;
public sealed partial class SpecialFolderInformationalPreviewControl : UserControl
{
public static readonly DependencyProperty SourceProperty = DependencyProperty.Register(
nameof(Source),
typeof(SpecialFolderPreviewData),
typeof(SpecialFolderInformationalPreviewControl),
new PropertyMetadata(null));
public SpecialFolderPreviewData? Source
{
get { return (SpecialFolderPreviewData)GetValue(SourceProperty); }
set { SetValue(SourceProperty, value); }
}
public SpecialFolderInformationalPreviewControl()
{
InitializeComponent();
}
public string FormatFileType(string? fileType) => FormatField("UnsupportedFile_FileType", fileType);
public string FormatFileSize(string? fileSize) => FormatField("UnsupportedFile_FileSize", fileSize);
public string FormatFileDateModified(string? fileDateModified) => FormatField("UnsupportedFile_DateModified", fileDateModified);
private static string FormatField(string resourceId, string? fieldValue)
{
return string.IsNullOrWhiteSpace(fieldValue) ? string.Empty : ReadableStringHelper.FormatResourceString(resourceId, fieldValue);
}
}

View File

@ -0,0 +1,25 @@
<!-- Copyright (c) Microsoft Corporation and Contributors. -->
<!-- Licensed under the MIT License. -->
<UserControl
x:Class="Peek.FilePreviewer.Controls.SpecialFolderPreview"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Peek.FilePreviewer.Controls"
xmlns:conv="using:Peek.Common.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:prev="using:Peek.FilePreviewer.Previewers"
mc:Ignorable="d">
<Grid
Margin="24"
HorizontalAlignment="Stretch"
VerticalAlignment="Center">
<controls:SpecialFolderInformationalPreviewControl
x:Name="InformationPreview"
Source="{x:Bind Source, Mode=OneWay}"
Visibility="{x:Bind IsVisibleIfStatesMatch(LoadingState, prev:PreviewState.Loaded), Mode=OneWay}" />
</Grid>
</UserControl>

View File

@ -0,0 +1,47 @@
// 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 CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Peek.Common.Converters;
using Peek.FilePreviewer.Models;
using Peek.FilePreviewer.Previewers;
namespace Peek.FilePreviewer.Controls;
[INotifyPropertyChanged]
public sealed partial class SpecialFolderPreview : UserControl
{
public static readonly DependencyProperty SourceProperty = DependencyProperty.Register(
nameof(Source),
typeof(SpecialFolderPreviewData),
typeof(SpecialFolderPreview),
new PropertyMetadata(null));
public static readonly DependencyProperty LoadingStateProperty = DependencyProperty.Register(
nameof(LoadingState),
typeof(PreviewState),
typeof(SpecialFolderPreview),
new PropertyMetadata(PreviewState.Uninitialized));
public SpecialFolderPreviewData? Source
{
get { return (SpecialFolderPreviewData)GetValue(SourceProperty); }
set { SetValue(SourceProperty, value); }
}
public PreviewState? LoadingState
{
get { return (PreviewState)GetValue(LoadingStateProperty); }
set { SetValue(LoadingStateProperty, value); }
}
public SpecialFolderPreview()
{
InitializeComponent();
}
public Visibility IsVisibleIfStatesMatch(PreviewState? a, PreviewState? b) => VisibilityConverter.Convert(a == b);
}

View File

@ -83,6 +83,12 @@
Source="{x:Bind DrivePreviewer.Preview, Mode=OneWay}"
Visibility="{x:Bind IsPreviewVisible(DrivePreviewer, Previewer.State), Mode=OneWay}" />
<controls:SpecialFolderPreview
x:Name="SpecialFolderPreview"
LoadingState="{x:Bind SpecialFolderPreviewer.State, Mode=OneWay}"
Source="{x:Bind SpecialFolderPreviewer.Preview, Mode=OneWay}"
Visibility="{x:Bind IsPreviewVisible(SpecialFolderPreviewer, Previewer.State), Mode=OneWay}" />
<controls:UnsupportedFilePreview
x:Name="UnsupportedFilePreview"
LoadingState="{x:Bind UnsupportedFilePreviewer.State, Mode=OneWay}"

View File

@ -53,6 +53,7 @@ namespace Peek.FilePreviewer
[NotifyPropertyChangedFor(nameof(ArchivePreviewer))]
[NotifyPropertyChangedFor(nameof(ShellPreviewHandlerPreviewer))]
[NotifyPropertyChangedFor(nameof(DrivePreviewer))]
[NotifyPropertyChangedFor(nameof(SpecialFolderPreviewer))]
[NotifyPropertyChangedFor(nameof(UnsupportedFilePreviewer))]
private IPreviewer? previewer;
@ -105,6 +106,8 @@ namespace Peek.FilePreviewer
public IDrivePreviewer? DrivePreviewer => Previewer as IDrivePreviewer;
public ISpecialFolderPreviewer? SpecialFolderPreviewer => Previewer as ISpecialFolderPreviewer;
public IUnsupportedFilePreviewer? UnsupportedFilePreviewer => Previewer as IUnsupportedFilePreviewer;
public IFileSystemItem Item

View File

@ -0,0 +1,26 @@
// 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 CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.UI.Xaml.Media;
namespace Peek.FilePreviewer.Models;
public partial class SpecialFolderPreviewData : ObservableObject
{
[ObservableProperty]
private ImageSource? iconPreview;
[ObservableProperty]
private string? fileName;
[ObservableProperty]
private string? fileType;
[ObservableProperty]
private string? fileSize;
[ObservableProperty]
private string? dateModified;
}

View File

@ -21,6 +21,8 @@
<None Remove="Controls\BrowserControl.xaml" />
<None Remove="Controls\DriveControl.xaml" />
<None Remove="Controls\ShellPreviewHandlerControl.xaml" />
<None Remove="Controls\SpecialFolderPreview\SpecialFolderInformationalPreviewControl.xaml" />
<None Remove="Controls\SpecialFolderPreview\SpecialFolderPreview.xaml" />
<None Remove="Controls\UnsupportedFilePreview\FailedFallbackPreviewControl.xaml" />
<None Remove="Controls\UnsupportedFilePreview\InformationalPreviewControl.xaml" />
<None Remove="FilePreview.xaml" />
@ -42,6 +44,27 @@
</PackageReference>
</ItemGroup>
<ItemGroup>
<COMReference Include="Shell32">
<VersionMinor>0</VersionMinor>
<VersionMajor>1</VersionMajor>
<Guid>50a7e9b0-70ef-11d1-b75a-00a0c90564fe</Guid>
<Lcid>0</Lcid>
<WrapperTool>tlbimp</WrapperTool>
<Isolated>false</Isolated>
<EmbedInteropTypes>true</EmbedInteropTypes>
</COMReference>
<COMReference Include="SHDocVw">
<VersionMinor>1</VersionMinor>
<VersionMajor>1</VersionMajor>
<Guid>eab22ac0-30c1-11cf-a7eb-0000c05bae0b</Guid>
<Lcid>0</Lcid>
<WrapperTool>tlbimp</WrapperTool>
<Isolated>false</Isolated>
<EmbedInteropTypes>true</EmbedInteropTypes>
</COMReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\common\Common.UI\Common.UI.csproj" />
<ProjectReference Include="..\..\..\common\FilePreviewCommon\FilePreviewCommon.csproj" />
@ -71,6 +94,9 @@
<Page Update="Controls\ArchiveControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Controls\SpecialFolderPreview\SpecialFolderInformationalPreviewControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>

View File

@ -0,0 +1,12 @@
// 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 Peek.FilePreviewer.Models;
namespace Peek.FilePreviewer.Previewers;
public interface ISpecialFolderPreviewer : IPreviewer
{
public SpecialFolderPreviewData? Preview { get; }
}

View File

@ -53,6 +53,10 @@ namespace Peek.FilePreviewer.Previewers
{
return new DrivePreviewer(item);
}
else if (SpecialFolderPreviewer.IsItemSupported(item))
{
return new SpecialFolderPreviewer(item);
}
// Other previewer types check their supported file types here
return CreateDefaultPreviewer(item);

View File

@ -0,0 +1,58 @@
// 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 Peek.Common;
using Peek.Common.Models;
namespace Peek.FilePreviewer.Previewers;
public enum KnownSpecialFolder
{
None,
RecycleBin,
}
public static class KnownSpecialFolders
{
private static readonly Lazy<IReadOnlyDictionary<string, KnownSpecialFolder>> FoldersByParsingNameDict = new(GetFoldersByParsingName);
public static IReadOnlyDictionary<string, KnownSpecialFolder> FoldersByParsingName => FoldersByParsingNameDict.Value;
private static Dictionary<string, KnownSpecialFolder> GetFoldersByParsingName()
{
var folders = new (KnownSpecialFolder Folder, string? ParsingName)[]
{
(KnownSpecialFolder.RecycleBin, GetParsingName("shell:RecycleBinFolder")),
};
return folders.Where(folder => !string.IsNullOrEmpty(folder.ParsingName))
.ToDictionary(folder => folder.ParsingName!, folder => folder.Folder);
}
private static string? GetParsingName(string shellName)
{
try
{
return CreateShellItemFromShellName(shellName)?.GetDisplayName(Windows.Win32.UI.Shell.SIGDN.SIGDN_DESKTOPABSOLUTEPARSING);
}
catch (Exception)
{
return null;
}
}
private static IShellItem? CreateShellItemFromShellName(string shellName)
{
// Based on https://stackoverflow.com/a/42966899
const string ShellItem = "43826d1e-e718-42ee-bc55-a1e261c37bfe";
Guid shellItem2Guid = new(ShellItem);
int retCode = NativeMethods.SHCreateItemFromParsingName(shellName, IntPtr.Zero, ref shellItem2Guid, out IShellItem? nativeShellItem);
return retCode == 0 ? nativeShellItem : null;
}
}

View File

@ -0,0 +1,215 @@
// 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.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using ManagedCommon;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media.Imaging;
using Peek.Common.Extensions;
using Peek.Common.Helpers;
using Peek.Common.Models;
using Peek.FilePreviewer.Models;
using Peek.FilePreviewer.Previewers.Helpers;
using Windows.Foundation;
namespace Peek.FilePreviewer.Previewers;
public partial class SpecialFolderPreviewer : ObservableObject, ISpecialFolderPreviewer, IDisposable
{
private readonly DispatcherTimer _syncDetailsDispatcherTimer = new();
private ulong _folderSize;
private DateTime? _dateModified;
[ObservableProperty]
private SpecialFolderPreviewData preview = new();
[ObservableProperty]
private PreviewState state;
public SpecialFolderPreviewer(IFileSystemItem file)
{
_syncDetailsDispatcherTimer.Interval = TimeSpan.FromMilliseconds(500);
_syncDetailsDispatcherTimer.Tick += DetailsDispatcherTimer_Tick;
Item = file;
Preview.FileName = file.Name;
Dispatcher = DispatcherQueue.GetForCurrentThread();
}
public static bool IsItemSupported(IFileSystemItem item)
{
// Always allow know special folders.
bool isKnownSpecialFolder = KnownSpecialFolders.FoldersByParsingName.ContainsKey(item.ParsingName);
// Allow empty paths unless Unc; icons don't load correctly for Unc paths.
bool isEmptyNonUncPath = string.IsNullOrEmpty(item.Path) && !PathHelper.IsUncPath(item.ParsingName);
return isKnownSpecialFolder || isEmptyNonUncPath;
}
private IFileSystemItem Item { get; }
private DispatcherQueue Dispatcher { get; }
public void Dispose()
{
_syncDetailsDispatcherTimer.Tick -= DetailsDispatcherTimer_Tick;
GC.SuppressFinalize(this);
}
public Task<PreviewSize> GetPreviewSizeAsync(CancellationToken cancellationToken)
{
Size? size = new(680, 500);
var previewSize = new PreviewSize { MonitorSize = size, UseEffectivePixels = true };
return Task.FromResult(previewSize);
}
public async Task LoadPreviewAsync(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
State = PreviewState.Loading;
var tasks = await Task.WhenAll(LoadIconPreviewAsync(cancellationToken), LoadDisplayInfoAsync(cancellationToken));
State = tasks.All(task => task) ? PreviewState.Loaded : PreviewState.Error;
}
public async Task CopyAsync()
{
await Dispatcher.RunOnUiThread(async () =>
{
var storageItem = await Item.GetStorageItemAsync();
ClipboardHelper.SaveToClipboard(storageItem);
});
}
public async Task<bool> LoadIconPreviewAsync(CancellationToken cancellationToken)
{
bool isIconValid = false;
var isTaskSuccessful = await TaskExtension.RunSafe(async () =>
{
cancellationToken.ThrowIfCancellationRequested();
await Dispatcher.RunOnUiThread(async () =>
{
cancellationToken.ThrowIfCancellationRequested();
var iconBitmap = await IconHelper.GetIconAsync(Item.ParsingName, cancellationToken);
cancellationToken.ThrowIfCancellationRequested();
isIconValid = iconBitmap != null;
Preview.IconPreview = iconBitmap ?? new SvgImageSource(new Uri("ms-appx:///Assets/Peek/DefaultFileIcon.svg"));
});
});
return isIconValid && isTaskSuccessful;
}
public async Task<bool> LoadDisplayInfoAsync(CancellationToken cancellationToken)
{
bool isDisplayValid = false;
var isTaskSuccessful = await TaskExtension.RunSafe(async () =>
{
cancellationToken.ThrowIfCancellationRequested();
var fileType = await Task.Run(Item.GetContentTypeAsync);
cancellationToken.ThrowIfCancellationRequested();
isDisplayValid = fileType != null;
await Dispatcher.RunOnUiThread(() =>
{
Preview.FileType = fileType;
return Task.CompletedTask;
});
RunUpdateDetailsWorkflow(cancellationToken);
});
return isDisplayValid && isTaskSuccessful;
}
private void RunUpdateDetailsWorkflow(CancellationToken cancellationToken)
{
Task.Run(
async () =>
{
try
{
await Dispatcher.RunOnUiThread(_syncDetailsDispatcherTimer.Start);
ComputeDetails(cancellationToken);
}
catch (OperationCanceledException)
{
}
catch (Exception ex)
{
Logger.LogError("Failed to update special folder details", ex);
}
finally
{
await Dispatcher.RunOnUiThread(_syncDetailsDispatcherTimer.Stop);
}
await Dispatcher.RunOnUiThread(SyncDetails);
},
cancellationToken);
}
private void ComputeDetails(CancellationToken cancellationToken)
{
_dateModified = Item.DateModified;
switch (KnownSpecialFolders.FoldersByParsingName.GetValueOrDefault(Item.ParsingName, KnownSpecialFolder.None))
{
case KnownSpecialFolder.None:
break;
case KnownSpecialFolder.RecycleBin:
ThreadHelper.RunOnSTAThread(() => { ComputeRecycleBinDetails(cancellationToken); });
cancellationToken.ThrowIfCancellationRequested();
break;
}
}
private void ComputeRecycleBinDetails(CancellationToken cancellationToken)
{
var shell = new Shell32.Shell();
var recycleBin = shell.NameSpace(10); // CSIDL_BITBUCKET
foreach (dynamic item in recycleBin.Items())
{
if (cancellationToken.IsCancellationRequested)
{
return;
}
_folderSize += Convert.ToUInt64(item.Size);
}
}
private void SyncDetails()
{
Preview.FileSize = _folderSize == 0 ? string.Empty : ReadableStringHelper.BytesToReadableString(_folderSize);
Preview.DateModified = _dateModified?.ToString(CultureInfo.CurrentCulture) ?? string.Empty;
}
private void DetailsDispatcherTimer_Tick(object? sender, object e)
{
SyncDetails();
}
}

View File

@ -45,8 +45,6 @@ namespace Peek.FilePreviewer.Previewers
Dispatcher = DispatcherQueue.GetForCurrentThread();
}
public bool IsPreviewLoaded => Preview.IconPreview != null;
private IFileSystemItem Item { get; }
private DispatcherQueue Dispatcher { get; }

View File

@ -7,49 +7,41 @@ using System.IO;
using ManagedCommon;
using Peek.Common.Models;
namespace Peek.UI.Extensions
namespace Peek.UI.Extensions;
public static class IShellItemExtensions
{
public static class IShellItemExtensions
public static IFileSystemItem ToIFileSystemItem(this IShellItem shellItem)
{
public static IFileSystemItem ToIFileSystemItem(this IShellItem shellItem)
{
string path = shellItem.GetPath();
string name = shellItem.GetName();
string path = shellItem.GetPath();
string name = shellItem.GetName();
return File.Exists(path) ? new FileItem(path, name) : new FolderItem(path, name);
return File.Exists(path) ? new FileItem(path, name) : new FolderItem(path, name, shellItem.GetParsingName());
}
private static string GetPath(this IShellItem shellItem) =>
shellItem.GetNameCore(Windows.Win32.UI.Shell.SIGDN.SIGDN_FILESYSPATH, logError: false);
private static string GetName(this IShellItem shellItem) =>
shellItem.GetNameCore(Windows.Win32.UI.Shell.SIGDN.SIGDN_NORMALDISPLAY, logError: true);
private static string GetParsingName(this IShellItem shellItem) =>
shellItem.GetNameCore(Windows.Win32.UI.Shell.SIGDN.SIGDN_DESKTOPABSOLUTEPARSING, logError: true);
private static string GetNameCore(this IShellItem shellItem, Windows.Win32.UI.Shell.SIGDN displayNameType, bool logError)
{
try
{
return shellItem.GetDisplayName(displayNameType);
}
private static string GetPath(this IShellItem shellItem)
catch (Exception ex)
{
string path = string.Empty;
try
if (logError)
{
path = shellItem.GetDisplayName(Windows.Win32.UI.Shell.SIGDN.SIGDN_FILESYSPATH);
}
catch (Exception ex)
{
// TODO: Handle cases that do not have a file system path like Recycle Bin.
path = string.Empty;
Logger.LogError("Getting path failed. " + ex.Message);
Logger.LogError($"Getting {Enum.GetName(displayNameType)} failed. {ex.Message}");
}
return path;
}
private static string GetName(this IShellItem shellItem)
{
string name = string.Empty;
try
{
name = shellItem.GetDisplayName(Windows.Win32.UI.Shell.SIGDN.SIGDN_NORMALDISPLAY);
}
catch (Exception ex)
{
name = string.Empty;
Logger.LogError("Getting path failed. " + ex.Message);
}
return name;
return string.Empty;
}
}
}

View File

@ -187,7 +187,7 @@ private:
}
// Enumerate all Shell Windows to compare the window handle against.
IUnknownPtr spEnum{};
IUnknownPtr spEnum{}; // _com_ptr_t; no Release required.
result = spShellWindows->_NewEnum(&spEnum);
if (result != S_OK || spEnum == nullptr)
{
@ -195,7 +195,7 @@ private:
return true; // Might as well assume it's possible it's an explorer window.
}
IEnumVARIANTPtr spEnumVariant{};
IEnumVARIANTPtr spEnumVariant{}; // _com_ptr_t; no Release required.
result = spEnum.QueryInterface(__uuidof(spEnumVariant), &spEnumVariant);
if (result != S_OK || spEnumVariant == nullptr)
{
@ -219,8 +219,6 @@ private:
{
VariantClear(&variantElement);
spWebBrowserApp->Release();
spEnumVariant->Release();
spEnum->Release();
return true;
}
}
@ -229,8 +227,6 @@ private:
VariantClear(&variantElement);
}
spEnumVariant->Release();
spEnum->Release();
return false;
}

View File

@ -475,11 +475,11 @@ namespace PowerAccent.Core
LetterKey.VK_7 => new[] { "₇", "⁷" },
LetterKey.VK_8 => new[] { "₈", "⁸" },
LetterKey.VK_9 => new[] { "₉", "⁹" },
LetterKey.VK_A => new[] { "á", "à", "â", "ã" },
LetterKey.VK_A => new[] { "á", "à", "â", "ã", "ª" },
LetterKey.VK_C => new[] { "ç" },
LetterKey.VK_E => new[] { "é", "ê", "€" },
LetterKey.VK_I => new[] { "í" },
LetterKey.VK_O => new[] { "ô", "ó", "õ" },
LetterKey.VK_O => new[] { "ô", "ó", "õ", "º" },
LetterKey.VK_P => new[] { "π" },
LetterKey.VK_S => new[] { "$" },
LetterKey.VK_U => new[] { "ú" },

View File

@ -15,6 +15,7 @@ namespace PowerRenameUI
Windows.Foundation.Collections.IObservableVector<PatternSnippet> SearchRegExShortcuts { get; };
Windows.Foundation.Collections.IObservableVector<PatternSnippet> DateTimeShortcuts { get; };
Windows.Foundation.Collections.IObservableVector<PatternSnippet> CounterShortcuts { get; };
Windows.Foundation.Collections.IObservableVector<PatternSnippet> RandomizerShortcuts { get; };
String OriginalCount;
String RenamedCount;

View File

@ -327,6 +327,8 @@
<RowDefinition Height="Auto" />
<RowDefinition Height="28" />
<RowDefinition Height="*" />
<RowDefinition Height="28" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock x:Uid="DateTimeCheatSheet_Title" FontWeight="SemiBold" />
<ListView
@ -407,6 +409,47 @@
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<TextBlock
x:Uid="RandomizerCheatSheet_Title"
Grid.Row="4"
Margin="0,10,0,0"
FontWeight="SemiBold" />
<ListView
Grid.Row="5"
Margin="-4,12,0,0"
IsItemClickEnabled="True"
ItemClick="DateTimeItemClick"
ItemsSource="{x:Bind RandomizerShortcuts}"
SelectionMode="None">
<ListView.ItemTemplate>
<DataTemplate x:DataType="local:PatternSnippet">
<Grid Margin="-10,0,0,0" ColumnSpacing="8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Border
Padding="8"
HorizontalAlignment="Left"
Background="{ThemeResource ButtonBackground}"
BorderBrush="{ThemeResource ButtonBorderBrush}"
BorderThickness="1"
CornerRadius="4">
<TextBlock
FontFamily="Consolas"
Foreground="{ThemeResource ButtonForeground}"
Text="{x:Bind Code}" />
</Border>
<TextBlock
Grid.Column="1"
VerticalAlignment="Center"
FontSize="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="{x:Bind Description}" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</Flyout>
</Button.Flyout>
@ -508,6 +551,12 @@
Height="32"
Content="&#xEA40;"
FontFamily="{ThemeResource SymbolThemeFontFamily}" />
<ToggleButton
x:Name="toggleButton_randItems"
x:Uid="ToggleButton_RandItems"
Height="32"
Content="&#xE8B1;"
FontFamily="{ThemeResource SymbolThemeFontFamily}" />
</StackPanel>
</StackPanel>

View File

@ -203,6 +203,12 @@ namespace winrt::PowerRenameUI::implementation
m_CounterShortcuts.Append(winrt::make<PatternSnippet>(L"${padding=8}", manager.MainResourceMap().GetValue(L"Resources/CounterCheatSheet_Padding").ValueAsString()));
m_CounterShortcuts.Append(winrt::make<PatternSnippet>(L"${increment=3,padding=4,start=900}", manager.MainResourceMap().GetValue(L"Resources/CounterCheatSheet_Complex").ValueAsString()));
m_RandomizerShortcuts = winrt::single_threaded_observable_vector<PowerRenameUI::PatternSnippet>();
m_RandomizerShortcuts.Append(winrt::make<PatternSnippet>(L"${rstringalnum=9}", manager.MainResourceMap().GetValue(L"Resources/RandomizerCheatSheet_Alnum").ValueAsString()));
m_RandomizerShortcuts.Append(winrt::make<PatternSnippet>(L"${rstringalpha=13}", manager.MainResourceMap().GetValue(L"Resources/RandomizerCheatSheet_Alpha").ValueAsString()));
m_RandomizerShortcuts.Append(winrt::make<PatternSnippet>(L"${rstringdigit=36}", manager.MainResourceMap().GetValue(L"Resources/RandomizerCheatSheet_Digit").ValueAsString()));
m_RandomizerShortcuts.Append(winrt::make<PatternSnippet>(L"${ruuidv4}", manager.MainResourceMap().GetValue(L"Resources/RandomizerCheatSheet_Uuid").ValueAsString()));
InitializeComponent();
listView_ExplorerItems().ApplyTemplate();
@ -283,6 +289,7 @@ namespace winrt::PowerRenameUI::implementation
button_rename().IsEnabled(false);
toggleButton_enumItems().IsChecked(true);
toggleButton_randItems().IsChecked(false);
InitAutoComplete();
SearchReplaceChanged();
InvalidateItemListViewState();
@ -749,6 +756,15 @@ namespace winrt::PowerRenameUI::implementation
UpdateFlag(EnumerateItems, UpdateFlagCommand::Reset);
});
// CheckBox RandomizeItems
toggleButton_randItems().Checked([&](auto const&, auto const&) {
ValidateFlags(RandomizeItems);
UpdateFlag(RandomizeItems, UpdateFlagCommand::Set);
});
toggleButton_randItems().Unchecked([&](auto const&, auto const&) {
UpdateFlag(RandomizeItems, UpdateFlagCommand::Reset);
});
// ButtonSettings
button_settings().Click([&](auto const&, auto const&) {
OpenSettingsApp();
@ -944,6 +960,10 @@ namespace winrt::PowerRenameUI::implementation
{
toggleButton_enumItems().IsChecked(true);
}
if (flags & RandomizeItems)
{
toggleButton_randItems().IsChecked(true);
}
if (flags & ExcludeFiles)
{
toggleButton_includeFiles().IsChecked(false);

View File

@ -85,6 +85,7 @@ namespace winrt::PowerRenameUI::implementation
winrt::Windows::Foundation::Collections::IObservableVector<PowerRenameUI::PatternSnippet> SearchRegExShortcuts() { return m_searchRegExShortcuts; }
winrt::Windows::Foundation::Collections::IObservableVector<PowerRenameUI::PatternSnippet> DateTimeShortcuts() { return m_dateTimeShortcuts; }
winrt::Windows::Foundation::Collections::IObservableVector<PowerRenameUI::PatternSnippet> CounterShortcuts() { return m_CounterShortcuts; }
winrt::Windows::Foundation::Collections::IObservableVector<PowerRenameUI::PatternSnippet> RandomizerShortcuts() { return m_RandomizerShortcuts; }
hstring OriginalCount();
void OriginalCount(hstring value);
@ -107,6 +108,7 @@ namespace winrt::PowerRenameUI::implementation
winrt::Windows::Foundation::Collections::IObservableVector<PowerRenameUI::PatternSnippet> m_searchRegExShortcuts;
winrt::Windows::Foundation::Collections::IObservableVector<PowerRenameUI::PatternSnippet> m_dateTimeShortcuts;
winrt::Windows::Foundation::Collections::IObservableVector<PowerRenameUI::PatternSnippet> m_CounterShortcuts;
winrt::Windows::Foundation::Collections::IObservableVector<PowerRenameUI::PatternSnippet> m_RandomizerShortcuts;
// Used by PowerRenameManagerEvents
HRESULT OnRename(_In_ IPowerRenameItem* renameItem);

View File

@ -151,7 +151,7 @@
<value>Replace with</value>
</data>
<data name="CounterCheatSheet_Title.Text" xml:space="preserve">
<value>Replace using advanced counter syntax.</value>
<value>Replace using advanced counter syntax</value>
</data>
<data name="CounterCheatSheet_Simple" xml:space="preserve">
<value>A simple counter that you can use anywhere in a replace string.</value>
@ -295,10 +295,10 @@
<value>Capitalize each word</value>
</data>
<data name="ToggleButton_EnumItems.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
<value>Enumerate items</value>
<value>Enumeration features</value>
</data>
<data name="ToggleButton_EnumItems.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Enumerate items</value>
<value>Enumeration features</value>
</data>
<data name="SelectAllCheckBox.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Select or deselect all</value>
@ -363,4 +363,25 @@
<data name="RenameParts_ExtensionOnly.Content" xml:space="preserve">
<value>Extension only</value>
</data>
<data name="RandomizerCheatSheet_Alnum" xml:space="preserve">
<value>Random string with uppercase letters, lowercase letters and 0-9 digits, customized length.</value>
</data>
<data name="RandomizerCheatSheet_Alpha" xml:space="preserve">
<value>Random string with uppercase letters and lowercase letters, customized length.</value>
</data>
<data name="RandomizerCheatSheet_Digit" xml:space="preserve">
<value>Random string with 0-9 digits, customized length.</value>
</data>
<data name="RandomizerCheatSheet_Title.Text" xml:space="preserve">
<value>Replace using random values</value>
</data>
<data name="RandomizerCheatSheet_Uuid" xml:space="preserve">
<value>Random UUID according to v4 specification.</value>
</data>
<data name="ToggleButton_RandItems.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Random string features</value>
</data>
<data name="ToggleButton_RandItems.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
<value>Random string features</value>
</data>
</root>

View File

@ -661,3 +661,20 @@ bool LastModifiedTime(const std::wstring& filePath, FILETIME* lpFileTime)
}
return false;
}
std::wstring CreateGuidStringWithoutBrackets()
{
GUID guid;
if (CoCreateGuid(&guid) == S_OK)
{
OLECHAR* guidString;
if (StringFromCLSID(guid, &guidString) == S_OK)
{
std::wstring guidStr{ guidString };
CoTaskMemFree(guidString);
return guidStr.substr(1, guidStr.length() - 2);
}
}
return L"";
}

View File

@ -26,3 +26,4 @@ void SetRegNumber(const std::wstring& valueName, unsigned int value);
bool GetRegBoolean(const std::wstring& valueName, bool defaultValue);
void SetRegBoolean(const std::wstring& valueName, bool value);
bool LastModifiedTime(const std::wstring& filePath, FILETIME* lpFileTime);
std::wstring CreateGuidStringWithoutBrackets();

View File

@ -17,7 +17,8 @@ enum PowerRenameFlags
Uppercase = 0x200,
Lowercase = 0x400,
Titlecase = 0x800,
Capitalized = 0x1000
Capitalized = 0x1000,
RandomizeItems = 0x2000
};
enum PowerRenameFilters
@ -150,3 +151,12 @@ public:
(_In_ IEnumShellItems * enumShellItems) = 0;
IFACEMETHOD(Cancel)() = 0;
};
interface __declspec(uuid("FAB18E93-2E76-436B-8E26-B1240519AF12")) IPowerRenameRand : public IUnknown
{
public:
IFACEMETHOD(Start)
(_In_ IEnumShellItems * enumShellItems) = 0;
IFACEMETHOD(Cancel)
() = 0;
};

View File

@ -40,6 +40,7 @@
<ClInclude Include="PowerRenameManager.h" />
<ClInclude Include="PowerRenameMRU.h" />
<ClInclude Include="PowerRenameRegEx.h" />
<ClInclude Include="Randomizer.h" />
<ClInclude Include="Renaming.h" />
<ClInclude Include="Settings.h" />
<ClInclude Include="srwlock.h" />
@ -56,6 +57,7 @@
<ClCompile Include="PowerRenameManager.cpp" />
<ClCompile Include="PowerRenameMRU.cpp" />
<ClCompile Include="PowerRenameRegEx.cpp" />
<ClCompile Include="Randomizer.cpp" />
<ClCompile Include="Renaming.cpp" />
<ClCompile Include="Settings.cpp" />
<ClCompile Include="pch.cpp">

View File

@ -130,23 +130,106 @@ IFACEMETHODIMP CPowerRenameRegEx::GetReplaceTerm(_Outptr_ PWSTR* replaceTerm)
return hr;
}
HRESULT CPowerRenameRegEx::_OnEnumerateItemsChanged()
HRESULT CPowerRenameRegEx::_OnEnumerateOrRandomizeItemsChanged()
{
m_enumerators.clear();
const auto options = parseEnumOptions(m_RawReplaceTerm);
for (const auto e : options)
m_enumerators.emplace_back(e);
m_randomizer.clear();
if (m_flags & RandomizeItems)
{
const auto options = parseRandomizerOptions(m_RawReplaceTerm);
for (const auto& option : options)
{
m_randomizer.emplace_back(option);
}
}
if (m_flags & EnumerateItems)
{
const auto options = parseEnumOptions(m_RawReplaceTerm);
for (const auto& option : options)
{
if (m_randomizer.end() ==
std::find_if(
m_randomizer.begin(),
m_randomizer.end(),
[option](const Randomizer& r) -> bool { return r.options.replaceStrSpan.offset == option.replaceStrSpan.offset; }
))
{
// Only add as enumerator if we didn't find a randomizer already at this offset.
// Every randomizer will also be a valid enumerator according to the definition of enumerators, which allows any string to mean the default enumerator, so it should be interpreted that the user wanted a randomizer if both were found at the same offset of the replace string.
m_enumerators.emplace_back(option);
}
}
}
m_replaceWithRandomizerOffsets.clear();
m_replaceWithEnumeratorOffsets.clear();
int32_t offset = 0;
int ei = 0; // Enumerators index
int ri = 0; // Randomizer index
std::wstring replaceWith{ m_RawReplaceTerm };
// Remove counter expressions and calculate their offsets in replaceWith string.
int32_t offset = 0;
for (const auto& e : options)
if ((m_flags & EnumerateItems) && (m_flags & RandomizeItems))
{
replaceWith.erase(e.replaceStrSpan.offset + offset, e.replaceStrSpan.length);
m_replaceWithEnumeratorOffsets.push_back(offset);
offset -= static_cast<int32_t>(e.replaceStrSpan.length);
// Both flags are on, we need to merge which ones should be applied.
while ((ei < m_enumerators.size()) && (ri < m_randomizer.size()))
{
const auto& e = m_enumerators[ei];
const auto& r = m_randomizer[ri];
if (e.replaceStrSpan.offset < r.options.replaceStrSpan.offset)
{
// if the enumerator is next in line, remove counter expression and calculate offset with it.
replaceWith.erase(e.replaceStrSpan.offset + offset, e.replaceStrSpan.length);
m_replaceWithEnumeratorOffsets.push_back(offset);
offset -= static_cast<int32_t>(e.replaceStrSpan.length);
ei++;
}
else
{
// if the randomizer is next in line, remove randomizer expression and calculate offset with it.
replaceWith.erase(r.options.replaceStrSpan.offset + offset, r.options.replaceStrSpan.length);
m_replaceWithRandomizerOffsets.push_back(offset);
offset -= static_cast<int32_t>(r.options.replaceStrSpan.length);
ri++;
}
}
}
if (m_flags & EnumerateItems)
{
// Continue with all remaining enumerators
while (ei < m_enumerators.size())
{
const auto& e = m_enumerators[ei];
replaceWith.erase(e.replaceStrSpan.offset + offset, e.replaceStrSpan.length);
m_replaceWithEnumeratorOffsets.push_back(offset);
offset -= static_cast<int32_t>(e.replaceStrSpan.length);
ei++;
}
}
if (m_flags & RandomizeItems)
{
// Continue with all remaining randomizer instances
while (ri < m_randomizer.size())
{
const auto& r = m_randomizer[ri];
replaceWith.erase(r.options.replaceStrSpan.offset + offset, r.options.replaceStrSpan.length);
m_replaceWithRandomizerOffsets.push_back(offset);
offset -= static_cast<int32_t>(r.options.replaceStrSpan.length);
ri++;
}
}
return SHStrDup(replaceWith.data(), &m_replaceTerm);
}
@ -163,8 +246,8 @@ IFACEMETHODIMP CPowerRenameRegEx::PutReplaceTerm(_In_ PCWSTR replaceTerm, bool f
CoTaskMemFree(m_replaceTerm);
m_RawReplaceTerm = replaceTerm;
if (m_flags & EnumerateItems)
hr = _OnEnumerateItemsChanged();
if ((m_flags & RandomizeItems) || (m_flags & EnumerateItems))
hr = _OnEnumerateOrRandomizeItemsChanged();
else
hr = SHStrDup(replaceTerm, &m_replaceTerm);
}
@ -189,13 +272,20 @@ IFACEMETHODIMP CPowerRenameRegEx::PutFlags(_In_ DWORD flags)
if (m_flags != flags)
{
const bool newEnumerate = flags & EnumerateItems;
const bool refreshReplaceTerm = !!(m_flags & EnumerateItems) != newEnumerate;
const bool newRandomizer = flags & RandomizeItems;
const bool refreshReplaceTerm =
(!!(m_flags & EnumerateItems) != newEnumerate) ||
(!!(m_flags & RandomizeItems) != newRandomizer);
m_flags = flags;
if (refreshReplaceTerm)
{
CSRWExclusiveAutoLock lock(&m_lock);
if (newEnumerate)
_OnEnumerateItemsChanged();
if (newEnumerate || newRandomizer)
{
_OnEnumerateOrRandomizeItemsChanged();
}
else
{
CoTaskMemFree(m_replaceTerm);
@ -325,17 +415,75 @@ HRESULT CPowerRenameRegEx::Replace(_In_ PCWSTR source, _Outptr_ PWSTR* result, u
static const std::wregex zeroGroupRegex(L"(([^\\$]|^)(\\$\\$)*)\\$[0]");
static const std::wregex otherGroupsRegex(L"(([^\\$]|^)(\\$\\$)*)\\$([1-9])");
if (m_flags & EnumerateItems)
if ((m_flags & EnumerateItems) || (m_flags & RandomizeItems))
{
int ei = 0; // Enumerators index
int ri = 0; // Randomizer index
std::array<wchar_t, MAX_PATH> buffer;
int32_t offset = 0;
for (size_t ei = 0; ei < m_enumerators.size(); ++ei)
if ((m_flags & EnumerateItems) && (m_flags & RandomizeItems))
{
const auto& e = m_enumerators[ei];
const auto replacementLength = static_cast<int32_t>(e.printTo(buffer.data(), buffer.size(), enumIndex));
replaceTerm.insert(e.replaceStrSpan.offset + offset + m_replaceWithEnumeratorOffsets[ei], buffer.data());
offset += replacementLength;
// Both flags are on, we need to merge which ones should be applied.
while ((ei < m_enumerators.size()) && (ri < m_randomizer.size()))
{
const auto& e = m_enumerators[ei];
const auto& r = m_randomizer[ri];
if (e.replaceStrSpan.offset < r.options.replaceStrSpan.offset)
{
// if the enumerator is next in line, apply it.
const auto replacementLength = static_cast<int32_t>(e.printTo(buffer.data(), buffer.size(), enumIndex));
replaceTerm.insert(e.replaceStrSpan.offset + offset + m_replaceWithEnumeratorOffsets[ei], buffer.data());
offset += replacementLength;
ei++;
}
else
{
// if the randomizer is next in line, apply it.
std::string randomValue = r.randomize();
std::wstring wRandomValue(randomValue.begin(), randomValue.end());
replaceTerm.insert(r.options.replaceStrSpan.offset + offset + m_replaceWithRandomizerOffsets[ri], wRandomValue);
offset += static_cast<int32_t>(wRandomValue.length());
if (e.replaceStrSpan.offset == r.options.replaceStrSpan.offset)
{
// In theory, this shouldn't happen here as we were guarding against it when filling the randomizer and enumerator structures, but it's still here as a fail safe.
// Every randomizer will also be a valid enumerator according to the definition of enumerators, which allow any string to mean the default enumerator, so it should be interpreted that the user wanted a randomizer if both were found at the same offset of the replace string.
ei++;
}
ri++;
}
}
}
if (m_flags & EnumerateItems)
{
// Replace all remaining enumerators
while (ei < m_enumerators.size())
{
const auto& e = m_enumerators[ei];
const auto replacementLength = static_cast<int32_t>(e.printTo(buffer.data(), buffer.size(), enumIndex));
replaceTerm.insert(e.replaceStrSpan.offset + offset + m_replaceWithEnumeratorOffsets[ei], buffer.data());
offset += replacementLength;
ei++;
}
}
if (m_flags & RandomizeItems)
{
// Replace all remaining randomizer instances
while (ri < m_randomizer.size())
{
const auto& r = m_randomizer[ri];
std::string randomValue = r.randomize();
std::wstring wRandomValue(randomValue.begin(), randomValue.end());
replaceTerm.insert(r.options.replaceStrSpan.offset + offset + m_replaceWithRandomizerOffsets[ri], wRandomValue);
offset += static_cast<int32_t>(wRandomValue.length());
ri++;
}
}
}

View File

@ -3,6 +3,9 @@
#include "srwlock.h"
#include "Enumerating.h"
#include "Randomizer.h"
#include "PowerRenameInterfaces.h"
#define DEFAULT_FLAGS 0
@ -38,7 +41,7 @@ protected:
void _OnReplaceTermChanged();
void _OnFlagsChanged();
void _OnFileTimeChanged();
HRESULT _OnEnumerateItemsChanged();
HRESULT _OnEnumerateOrRandomizeItemsChanged();
size_t _Find(std::wstring data, std::wstring toSearch, bool caseInsensitive, size_t pos);
@ -59,6 +62,9 @@ protected:
std::vector<Enumerator> m_enumerators;
std::vector<int32_t> m_replaceWithEnumeratorOffsets;
std::vector<Randomizer> m_randomizer;
std::vector<int32_t> m_replaceWithRandomizerOffsets;
struct RENAME_REGEX_EVENT
{
IPowerRenameRegExEvents* pEvents;

View File

@ -0,0 +1,55 @@
#include "pch.h"
#include "Randomizer.h"
std::vector<RandomizerOptions> parseRandomizerOptions(const std::wstring& replaceWith)
{
static const std::wregex randAlnumRegex(LR"(rstringalnum=(\d+))");
static const std::wregex randAlphaRegex(LR"(rstringalpha=(-?\d+))");
static const std::wregex randDigitRegex(LR"(rstringdigit=(\d+))");
static const std::wregex randUuidRegex(LR"(ruuidv4)");
std::string buf;
std::vector<RandomizerOptions> options;
std::wregex randGroupRegex(LR"(\$\{.*?\})");
for (std::wsregex_iterator i{ begin(replaceWith), end(replaceWith), randGroupRegex }, end; i != end; ++i)
{
std::wsmatch match = *i;
std::wstring matchString = match.str();
RandomizerOptions option;
option.replaceStrSpan.offset = match.position();
option.replaceStrSpan.length = match.length();
std::wsmatch subMatch;
if (std::regex_search(matchString, subMatch, randAlnumRegex))
{
int length = std::stoi(subMatch.str(1));
option.alnum = true;
option.length = length;
}
if (std::regex_search(matchString, subMatch, randAlphaRegex))
{
int length = std::stoi(subMatch.str(1));
option.alpha = true;
option.length = length;
}
if (std::regex_search(matchString, subMatch, randDigitRegex))
{
int length = std::stoi(subMatch.str(1));
option.digit = true;
option.length = length;
}
if (std::regex_search(matchString, subMatch, randUuidRegex))
{
option.uuid = true;
}
if (option.isValid())
{
options.push_back(option);
}
}
return options;
}

View File

@ -0,0 +1,76 @@
#pragma once
#include "pch.h"
#include "Helpers.h"
#include <common\utils\string_utils.h>
struct ReplaceStrSpan
{
size_t offset = 0;
size_t length = 0;
};
struct RandomizerOptions
{
std::optional<int> length;
std::optional<boolean> alnum;
std::optional<boolean> alpha;
std::optional<boolean> digit;
std::optional<boolean> uuid;
ReplaceStrSpan replaceStrSpan;
bool isValid() const
{
return alnum.has_value() || alpha.has_value() || digit.has_value() || uuid.has_value();
}
};
std::vector<RandomizerOptions> parseRandomizerOptions(const std::wstring& replaceWith);
struct Randomizer
{
RandomizerOptions options;
inline Randomizer(RandomizerOptions opts) :
options(opts) {}
std::string randomize() const
{
std::string chars;
if (options.uuid.value_or(false))
{
return unwide(CreateGuidStringWithoutBrackets());
}
if (options.alnum.value_or(false))
{
chars += "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
}
if (options.alpha.value_or(false))
{
chars += "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
}
if (options.digit.value_or(false))
{
chars += "0123456789";
}
if (chars.empty())
{
return "";
}
std::string result;
std::random_device rd;
std::mt19937 generator(rd());
std::uniform_int_distribution<> distribution(0, static_cast<int>(chars.size()) - 1);
for (int i = 0; i < options.length.value_or(10); ++i)
{
result += chars[distribution(generator)];
}
return result;
}
};

View File

@ -27,6 +27,7 @@
#include <variant>
#include <charconv>
#include <string>
#include <random>
#include <ProjectTelemetry.h>

View File

@ -466,6 +466,151 @@ TEST_METHOD (VerifyCounterAllCustomizations)
CoTaskMemFree(result);
}
TEST_METHOD (VerifyRandomizerDefaultFlags)
{
CComPtr<IPowerRenameRegEx> renameRegEx;
Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK);
DWORD flags = 0;
Assert::IsTrue(renameRegEx->PutFlags(flags) == S_OK);
PWSTR result = nullptr;
Assert::IsTrue(renameRegEx->PutSearchTerm(L"bar") == S_OK);
Assert::IsTrue(renameRegEx->PutReplaceTerm(L"$1bar_${rstringalnum=9}") == S_OK);
unsigned long index = {};
Assert::IsTrue(renameRegEx->Replace(L"foobar", &result, index) == S_OK);
Assert::AreEqual(L"foo$1bar_${rstringalnum=9}", result);
CoTaskMemFree(result);
}
TEST_METHOD (VerifyRandomizerNoRegex)
{
CComPtr<IPowerRenameRegEx> renameRegEx;
Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK);
DWORD flags = RandomizeItems;
Assert::IsTrue(renameRegEx->PutFlags(flags) == S_OK);
PWSTR result = nullptr;
Assert::IsTrue(renameRegEx->PutSearchTerm(L"bar") == S_OK);
Assert::IsTrue(renameRegEx->PutReplaceTerm(L"$1bar_${}") == S_OK);
unsigned long index = {};
Assert::IsTrue(renameRegEx->Replace(L"foobar", &result, index) == S_OK);
Assert::AreEqual(L"foo$1bar_${}", result);
CoTaskMemFree(result);
}
TEST_METHOD (VerifyRandomizerNoRandomizerRegEx)
{
CComPtr<IPowerRenameRegEx> renameRegEx;
Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK);
DWORD flags = UseRegularExpressions;
Assert::IsTrue(renameRegEx->PutFlags(flags) == S_OK);
PWSTR result = nullptr;
Assert::IsTrue(renameRegEx->PutSearchTerm(L"bar") == S_OK);
Assert::IsTrue(renameRegEx->PutReplaceTerm(L"$1bar_${rstringalnum=9}") == S_OK);
unsigned long index = {};
Assert::IsTrue(renameRegEx->Replace(L"foobar", &result, index) == S_OK);
Assert::AreEqual(L"foobar_${rstringalnum=9}", result);
CoTaskMemFree(result);
}
TEST_METHOD (VerifyRandomizerRegEx)
{
CComPtr<IPowerRenameRegEx> renameRegEx;
Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK);
DWORD flags = RandomizeItems | UseRegularExpressions;
Assert::IsTrue(renameRegEx->PutFlags(flags) == S_OK);
PWSTR result = nullptr;
Assert::IsTrue(renameRegEx->PutSearchTerm(L"bar") == S_OK);
Assert::IsTrue(renameRegEx->PutReplaceTerm(L"$1bar_${rstringalnum=9}") == S_OK);
unsigned long index = {};
Assert::IsTrue(renameRegEx->Replace(L"foobar", &result, index) == S_OK);
std::wstring resultStr(result);
std::wregex pattern(L"foobar_\\w{9}");
Assert::IsTrue(std::regex_match(resultStr, pattern));
CoTaskMemFree(result);
}
TEST_METHOD (VerifyRandomizerRegExZeroValue)
{
CComPtr<IPowerRenameRegEx> renameRegEx;
Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK);
DWORD flags = RandomizeItems | UseRegularExpressions;
Assert::IsTrue(renameRegEx->PutFlags(flags) == S_OK);
PWSTR result = nullptr;
Assert::IsTrue(renameRegEx->PutSearchTerm(L"bar") == S_OK);
Assert::IsTrue(renameRegEx->PutReplaceTerm(L"$1bar_${rstringalnum=0}") == S_OK);
unsigned long index = {};
Assert::IsTrue(renameRegEx->Replace(L"foobar", &result, index) == S_OK);
Assert::AreEqual(L"foobar_", result);
CoTaskMemFree(result);
}
TEST_METHOD (VerifyRandomizerRegExChar)
{
CComPtr<IPowerRenameRegEx> renameRegEx;
Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK);
DWORD flags = RandomizeItems | UseRegularExpressions;
Assert::IsTrue(renameRegEx->PutFlags(flags) == S_OK);
PWSTR result = nullptr;
Assert::IsTrue(renameRegEx->PutSearchTerm(L"bar") == S_OK);
Assert::IsTrue(renameRegEx->PutReplaceTerm(L"$1bar_${rstringalpha=9}") == S_OK);
unsigned long index = {};
Assert::IsTrue(renameRegEx->Replace(L"foobar", &result, index) == S_OK);
std::wstring resultStr(result);
std::wregex pattern(L"foobar_[A-Za-z]{9}");
Assert::IsTrue(std::regex_match(resultStr, pattern));
CoTaskMemFree(result);
}
TEST_METHOD (VerifyRandomizerRegExNum)
{
CComPtr<IPowerRenameRegEx> renameRegEx;
Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK);
DWORD flags = RandomizeItems | UseRegularExpressions;
Assert::IsTrue(renameRegEx->PutFlags(flags) == S_OK);
PWSTR result = nullptr;
Assert::IsTrue(renameRegEx->PutSearchTerm(L"bar") == S_OK);
Assert::IsTrue(renameRegEx->PutReplaceTerm(L"$1bar_${rstringdigit=9}") == S_OK);
unsigned long index = {};
Assert::IsTrue(renameRegEx->Replace(L"foobar", &result, index) == S_OK);
std::wstring resultStr(result);
std::wregex pattern(L"foobar_\\d{9}");
Assert::IsTrue(std::regex_match(resultStr, pattern));
CoTaskMemFree(result);
}
TEST_METHOD (VerifyRandomizerRegExUuid)
{
CComPtr<IPowerRenameRegEx> renameRegEx;
Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK);
DWORD flags = RandomizeItems | UseRegularExpressions;
Assert::IsTrue(renameRegEx->PutFlags(flags) == S_OK);
PWSTR result = nullptr;
Assert::IsTrue(renameRegEx->PutSearchTerm(L"bar") == S_OK);
Assert::IsTrue(renameRegEx->PutReplaceTerm(L"$1bar_${ruuidv4}") == S_OK);
unsigned long index = {};
Assert::IsTrue(renameRegEx->Replace(L"foobar", &result, index) == S_OK);
std::wstring resultStr(result);
std::wregex pattern(L"foobar_[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89aAbB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}");
Assert::IsTrue(std::regex_match(resultStr, pattern));
CoTaskMemFree(result);
}
TEST_METHOD (VerifyRandomizerRegExAllBackToBack)
{
CComPtr<IPowerRenameRegEx> renameRegEx;
Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK);
DWORD flags = RandomizeItems | UseRegularExpressions;
Assert::IsTrue(renameRegEx->PutFlags(flags) == S_OK);
PWSTR result = nullptr;
Assert::IsTrue(renameRegEx->PutSearchTerm(L"bar") == S_OK);
Assert::IsTrue(renameRegEx->PutReplaceTerm(L"$1bar_${rstringalnum=2}${rstringalpha=2}${rstringdigit=2}${ruuidv4}") == S_OK);
unsigned long index = {};
Assert::IsTrue(renameRegEx->Replace(L"foobar", &result, index) == S_OK);
std::wstring resultStr(result);
std::wregex pattern(L"foobar_\\w{2}[A-Za-z]{2}\\d{2}[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89aAbB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}");
Assert::IsTrue(std::regex_match(resultStr, pattern));
CoTaskMemFree(result);
}
#ifndef TESTS_PARTIAL
};
}

View File

@ -400,7 +400,6 @@ namespace Microsoft.PowerToys.PreviewHandler.Monaco
_html = _html.Replace("[[PT_STICKY_SCROLL]]", _settings.StickyScroll ? "1" : "0", StringComparison.InvariantCulture);
_html = _html.Replace("[[PT_FONT_SIZE]]", _settings.FontSize.ToString(CultureInfo.InvariantCulture), StringComparison.InvariantCulture);
_html = _html.Replace("[[PT_CODE]]", _base64FileCode, StringComparison.InvariantCulture);
_html = _html.Replace("[[PT_STICKY_SCROLL]]", _settings.StickyScroll ? "1" : "0", StringComparison.InvariantCulture);
_html = _html.Replace("[[PT_URL]]", FilePreviewCommon.MonacoHelper.VirtualHostName, StringComparison.InvariantCulture);
}

View File

@ -120,6 +120,12 @@ namespace Common
/// </summary>
public void UpdateWindowBounds(IntPtr hwnd, Rectangle newBounds)
{
if (this.Disposing || this.IsDisposed)
{
// For unclear reasons, this can be called when handling an error and the form has already been disposed.
return;
}
// We must set the WS_CHILD style to change the form to a control within the Explorer preview pane
int windowStyle = NativeMethods.GetWindowLong(Handle, gwlStyle);
if ((windowStyle & wsChild) == 0)

View File

@ -22,6 +22,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
PasteAsJsonShortcut = new();
ShowCustomPreview = true;
SendPasteKeyCombination = true;
CloseAfterLosingFocus = false;
}
[JsonConverter(typeof(BoolPropertyJsonConverter))]
@ -31,6 +32,9 @@ namespace Microsoft.PowerToys.Settings.UI.Library
[CmdConfigureIgnore]
public bool SendPasteKeyCombination { get; set; }
[JsonConverter(typeof(BoolPropertyJsonConverter))]
public bool CloseAfterLosingFocus { get; set; }
[JsonPropertyName("advanced-paste-ui-hotkey")]
public HotkeySettings AdvancedPasteUIShortcut { get; set; }

View File

@ -76,9 +76,16 @@ namespace Microsoft.PowerToys.Settings.UI
/// </summary>
public App()
{
Logger.InitializeLogger("\\Settings\\Logs");
Logger.InitializeLogger(@"\Settings\Logs");
this.InitializeComponent();
InitializeComponent();
UnhandledException += App_UnhandledException;
}
private void App_UnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
{
Logger.LogError("Unhandled exception", e.Exception);
}
public static void OpenSettingsWindow(Type type = null, bool ensurePageIsSelected = false)

View File

@ -5,7 +5,6 @@
using System;
using System.Collections.ObjectModel;
using System.Globalization;
using AllExperiments;
using global::PowerToys.GPOWrapper;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.OOBE.Enums;
@ -52,13 +51,16 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
private static ISettingsUtils settingsUtils = new SettingsUtils();
private bool ExperimentationToggleSwitchEnabled { get; set; } = true;
/* NOTE: Experimentation for OOBE is currently turned off on server side. Keeping this code in a comment to allow future experiments.
private bool ExperimentationToggleSwitchEnabled { get; set; } = true;
*/
public OobeShellPage()
{
InitializeComponent();
ExperimentationToggleSwitchEnabled = SettingsRepository<GeneralSettings>.GetInstance(settingsUtils).SettingsConfig.EnableExperimentation;
// NOTE: Experimentation for OOBE is currently turned off on server side. Keeping this code in a comment to allow future experiments.
// ExperimentationToggleSwitchEnabled = SettingsRepository<GeneralSettings>.GetInstance(settingsUtils).SettingsConfig.EnableExperimentation;
SetTitleBar();
DataContext = ViewModel;
OobeShellHandler = this;
@ -235,7 +237,8 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
{
switch (selectedItem.Tag)
{
case "Overview":
case "Overview": NavigationFrame.Navigate(typeof(OobeOverview)); break;
/* NOTE: Experimentation for OOBE is currently turned off on server side. Keeping this code in a comment to allow future experiments.
if (ExperimentationToggleSwitchEnabled && GPOWrapper.GetAllowExperimentationValue() != GpoRuleConfigured.Disabled)
{
switch (AllExperiments.Experiments.LandingPageExperiment)
@ -255,7 +258,7 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
NavigationFrame.Navigate(typeof(OobeOverview));
break;
}
*/
case "WhatsNew": NavigationFrame.Navigate(typeof(OobeWhatsNew)); break;
case "AdvancedPaste": NavigationFrame.Navigate(typeof(OobeAdvancedPaste)); break;
case "AlwaysOnTop": NavigationFrame.Navigate(typeof(OobeAlwaysOnTop)); break;

View File

@ -81,7 +81,7 @@
Severity="Informational" />
</controls:SettingsGroup>
<controls:SettingsGroup x:Uid="AdvancedPaste_ClipboardHistorySettingsGroup" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
<controls:SettingsGroup x:Uid="AdvancedPaste_BehaviorSettingsGroup" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
<tkcontrols:SettingsCard
x:Uid="AdvancedPaste_Clipboard_History_Enabled_SettingsCard"
HeaderIcon="{ui:FontIcon Glyph=&#xF0E3;}"
@ -94,6 +94,9 @@
IsOpen="{x:Bind ViewModel.ShowClipboardHistoryIsGpoConfiguredInfoBar, Mode=OneWay}"
IsTabStop="{x:Bind ViewModel.ShowClipboardHistoryIsGpoConfiguredInfoBar, Mode=OneWay}"
Severity="Informational" />
<tkcontrols:SettingsCard x:Uid="AdvancedPaste_CloseAfterLosingFocus" HeaderIcon="{ui:FontIcon Glyph=&#xED1A;}">
<ToggleSwitch IsOn="{x:Bind ViewModel.CloseAfterLosingFocus, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
</controls:SettingsGroup>
<controls:SettingsGroup x:Uid="AdvancedPaste_Direct_Access_Hotkeys_GroupSettings" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">

View File

@ -35,7 +35,7 @@
<tkcontrols:SettingsCard x:Uid="Peek_AlwaysRunNotElevated" HeaderIcon="{ui:FontIcon Glyph=&#xE7EF;}">
<ToggleSwitch x:Uid="ToggleSwitch" IsOn="{x:Bind ViewModel.AlwaysRunNotElevated, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard x:Uid="Peek_CloseAfterLosingFocus">
<tkcontrols:SettingsCard x:Uid="Peek_CloseAfterLosingFocus" HeaderIcon="{ui:FontIcon Glyph=&#xED1A;}">
<ToggleSwitch x:Uid="ToggleSwitch" IsOn="{x:Bind ViewModel.CloseAfterLosingFocus, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
</controls:SettingsGroup>

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