mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-01-16 09:16:49 +01:00
Compare commits
63 Commits
v0.88.0
...
shawn/test
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8755aacd0b | ||
|
|
e59ea36ccf | ||
|
|
2f40aab220 | ||
|
|
33459b718b | ||
|
|
7146da92ea | ||
|
|
bbe248ca90 | ||
|
|
c38faa959a | ||
|
|
fc94cd758e | ||
|
|
61a00aa669 | ||
|
|
cca3f442e2 | ||
|
|
22e29d1253 | ||
|
|
8a2d4745fa | ||
|
|
bf2685757a | ||
|
|
b8cef42776 | ||
|
|
959a54bcd9 | ||
|
|
491d51afaf | ||
|
|
f263042aeb | ||
|
|
c09a5337c4 | ||
|
|
9a658eb884 | ||
|
|
4eb11d6f9b | ||
|
|
a5a354a70f | ||
|
|
744316c400 | ||
|
|
f2370912f3 | ||
|
|
5cc30df4db | ||
|
|
f81f65db3d | ||
|
|
9f008a65d6 | ||
|
|
c656dcc9c5 | ||
|
|
83cea39b66 | ||
|
|
3970e89ee7 | ||
|
|
74214f611e | ||
|
|
908a690316 | ||
|
|
6515374ce9 | ||
|
|
273a45ff1f | ||
|
|
17f3c12a11 | ||
|
|
fa4471a9e6 | ||
|
|
727de3e1fc | ||
|
|
c6f9701818 | ||
|
|
9453e38881 | ||
|
|
a1a02889d5 | ||
|
|
0592e74d3d | ||
|
|
9d148d0a3a | ||
|
|
0a51687b65 | ||
|
|
771fcaba96 | ||
|
|
82e386f63c | ||
|
|
91b53cdc13 | ||
|
|
5c2c74a6c9 | ||
|
|
cb5baad677 | ||
|
|
ec136d7bb7 | ||
|
|
e33efb7f10 | ||
|
|
68afc6623f | ||
|
|
5008d77105 | ||
|
|
58d34087ee | ||
|
|
0c7a1dd316 | ||
|
|
e0cb4018ab | ||
|
|
c19c4b0353 | ||
|
|
cc644b1998 | ||
|
|
33ec492389 | ||
|
|
1205a9d9e0 | ||
|
|
629ca8bd4c | ||
|
|
ab7394f15e | ||
|
|
fd280800cf | ||
|
|
5fe761949f | ||
|
|
2c069ce708 |
6
.github/actions/spell-check/allow/code.txt
vendored
6
.github/actions/spell-check/allow/code.txt
vendored
@@ -263,6 +263,10 @@ onefuzz
|
||||
|
||||
# NameInCode
|
||||
leilzh
|
||||
mengyuanchen
|
||||
|
||||
# DllName
|
||||
testhost
|
||||
|
||||
#Tools
|
||||
OIP
|
||||
OIP
|
||||
|
||||
4
.github/actions/spell-check/expect.txt
vendored
4
.github/actions/spell-check/expect.txt
vendored
@@ -734,6 +734,7 @@ KEYBDINPUT
|
||||
keyboardeventhandlers
|
||||
keyboardmanagercommon
|
||||
KEYBOARDMANAGEREDITOR
|
||||
KEYBOARDMANAGEREDITORLIBRARYWRAPPER
|
||||
keyboardmanagerstate
|
||||
keyboardmanagerui
|
||||
KEYEVENTF
|
||||
@@ -1282,7 +1283,7 @@ rectp
|
||||
RECTSOURCE
|
||||
recyclebin
|
||||
Redist
|
||||
reencode
|
||||
Reencode
|
||||
reencoded
|
||||
REFCLSID
|
||||
REFGUID
|
||||
@@ -1302,6 +1303,7 @@ regroot
|
||||
regsvr
|
||||
REINSTALLMODE
|
||||
reloadable
|
||||
Relogger
|
||||
remappings
|
||||
REMAPSUCCESSFUL
|
||||
REMAPUNSUCCESSFUL
|
||||
|
||||
@@ -128,6 +128,7 @@
|
||||
"PowerToys.KeyboardManager.dll",
|
||||
"KeyboardManagerEditor\\PowerToys.KeyboardManagerEditor.exe",
|
||||
"KeyboardManagerEngine\\PowerToys.KeyboardManagerEngine.exe",
|
||||
"PowerToys.KeyboardManagerEditorLibraryWrapper.dll",
|
||||
|
||||
"PowerToys.Launcher.dll",
|
||||
"PowerToys.PowerLauncher.dll",
|
||||
|
||||
@@ -33,7 +33,7 @@ parameters:
|
||||
default: true
|
||||
- name: winAppSDKVersionNumber
|
||||
type: string
|
||||
default: 1.6
|
||||
default: 1.7
|
||||
- name: useExperimentalVersion
|
||||
type: boolean
|
||||
default: false
|
||||
@@ -47,4 +47,4 @@ extends:
|
||||
useVSPreview: ${{ parameters.useVSPreview }}
|
||||
useLatestWinAppSDK: ${{ parameters.useLatestWinAppSDK }}
|
||||
winAppSDKVersionNumber: ${{ parameters.winAppSDKVersionNumber }}
|
||||
useExperimentalVersion: ${{ parameters.useExperimentalVersion }}
|
||||
useExperimentalVersion: ${{ parameters.useExperimentalVersion }}
|
||||
@@ -88,6 +88,8 @@ extends:
|
||||
akvName: $(SigningAKVName)
|
||||
authCertName: $(SigningAuthCertName)
|
||||
signCertName: $(SigningSignCertName)
|
||||
useManagedIdentity: $(SigningUseManagedIdentity)
|
||||
clientId: $(SigningOriginalClientId)
|
||||
# Have msbuild use the release nuget config profile
|
||||
additionalBuildOptions: /p:RestoreConfigFile="$(Build.SourcesDirectory)\.pipelines\release-nuget.config"
|
||||
beforeBuildSteps:
|
||||
|
||||
@@ -19,6 +19,7 @@ jobs:
|
||||
BuildPlatform: ${{ parameters.platform }}
|
||||
BuildConfiguration: ${{ parameters.configuration }}
|
||||
SrcPath: $(Build.Repository.LocalPath)
|
||||
TestArtifactsName: build-${{ parameters.platform }}-${{ parameters.configuration }}${{ parameters.inputArtifactStem }}
|
||||
pool:
|
||||
${{ if eq(variables['System.CollectionId'], 'cb55739e-4afe-46a3-970f-1b49d8ee7564') }}:
|
||||
${{ if ne(parameters.platform, 'ARM64') }}:
|
||||
@@ -59,14 +60,19 @@ jobs:
|
||||
- script:
|
||||
reg add "HKLM\Software\Policies\Microsoft\Edge\WebView2\ReleaseChannels" /v PowerToys.exe /t REG_SZ /d "3"
|
||||
displayName: "Enable WebView2 Canary Channel"
|
||||
|
||||
- download: current
|
||||
displayName: Download artifacts
|
||||
artifact: build-${{ parameters.platform }}-${{ parameters.configuration }}${{ parameters.inputArtifactStem }}
|
||||
patterns: |-
|
||||
**
|
||||
!**\*.pdb
|
||||
!**\*.lib
|
||||
|
||||
- ${{ if ne(parameters.platform, 'arm64') }}:
|
||||
- download: current
|
||||
displayName: Download artifacts
|
||||
artifact: $(TestArtifactsName)
|
||||
patterns: |-
|
||||
**
|
||||
!**\*.pdb
|
||||
!**\*.lib
|
||||
- ${{ else }}:
|
||||
- template: steps-download-artifacts-with-azure-cli.yml
|
||||
parameters:
|
||||
artifactName: $(TestArtifactsName)
|
||||
|
||||
- template: steps-ensure-dotnet-version.yml
|
||||
parameters:
|
||||
@@ -91,7 +97,7 @@ jobs:
|
||||
platform: '$(BuildPlatform)'
|
||||
configuration: '$(BuildConfiguration)'
|
||||
testSelector: 'testAssemblies'
|
||||
searchFolder: '$(Pipeline.Workspace)\build-${{ parameters.platform }}-${{ parameters.configuration }}${{ parameters.inputArtifactStem }}'
|
||||
searchFolder: '$(Pipeline.Workspace)\$(TestArtifactsName)'
|
||||
vsTestVersion: 'toolsInstaller'
|
||||
uiTests: true
|
||||
rerunFailedTests: true
|
||||
|
||||
@@ -72,7 +72,7 @@ stages:
|
||||
winAppSDKVersionNumber: ${{ parameters.winAppSDKVersionNumber }}
|
||||
useExperimentalVersion: ${{ parameters.useExperimentalVersion }}
|
||||
|
||||
- ${{ if eq(parameters.runTests, true) }}:
|
||||
- ${{ if and(eq(parameters.runTests, true), not(and(eq(platform, 'arm64'), eq(variables['System.PullRequest.IsFork'], true)))) }}:
|
||||
- stage: Test_${{ platform }}
|
||||
displayName: Test ${{ platform }}
|
||||
dependsOn:
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
parameters:
|
||||
- name: artifactName
|
||||
type: string
|
||||
default: ""
|
||||
|
||||
# Why use az cli to download? → The ARM agent may run into OutOfMemory issues.
|
||||
# Why use the Azure CLI ZIP version? → It comes with its own Python and works fine under emulation on ARM64.
|
||||
# Why not use AzureCLI@2 task? → It requires azureSubscription, which is unnecessary for downloading artifacts.
|
||||
|
||||
steps:
|
||||
- powershell: |
|
||||
Write-Host "Downloading Azure CLI ZIP..."
|
||||
$azCliUrl = "https://aka.ms/installazurecliwindowszipx64"
|
||||
$azCliZip = "$(Build.ArtifactStagingDirectory)\azure-cli.zip"
|
||||
|
||||
Invoke-WebRequest -Uri $azCliUrl -OutFile $azCliZip
|
||||
displayName: 'Install Azure CLI from ZIP'
|
||||
|
||||
- task: ExtractFiles@1
|
||||
inputs:
|
||||
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)\azure-cli.zip'
|
||||
destinationFolder: '$(Build.ArtifactStagingDirectory)\AzureCLI'
|
||||
|
||||
- pwsh: |
|
||||
$azureCliPath = "$(Build.ArtifactStagingDirectory)\AzureCLI\bin"
|
||||
$env:Path = "$azureCliPath;" + $env:Path
|
||||
Write-Host "Configuring Azure DevOps defaults..."
|
||||
az devops configure --defaults organization='$(System.TeamFoundationCollectionUri)' project='$(System.TeamProject)' --use-git-aliases true
|
||||
Write-Host "Downloading artifacts..."
|
||||
az pipelines runs artifact download --artifact-name ${{parameters.artifactName}} --path "$(Pipeline.Workspace)/${{parameters.artifactName}}" --run-id $(Build.BuildId) --debug
|
||||
displayName: 'Download artifacts with Azure CLI'
|
||||
env:
|
||||
AZURE_DEVOPS_EXT_PAT: $(System.AccessToken)
|
||||
@@ -19,4 +19,6 @@ steps:
|
||||
AuthAKVName: ${{ parameters.signingIdentity.akvName }}
|
||||
AuthCertName: ${{ parameters.signingIdentity.authCertName }}
|
||||
AuthSignCertName: ${{ parameters.signingIdentity.signCertName }}
|
||||
UseMSIAuthentication: ${{ coalesce(parameters.signingIdentity.useManagedIdentity, 'false') }}
|
||||
EsrpClientId: ${{ parameters.signingIdentity.clientId }}
|
||||
${{ insert }}: ${{ parameters.inputs }}
|
||||
|
||||
@@ -27,6 +27,9 @@ steps:
|
||||
buildType: 'specific'
|
||||
project: '55e8140e-57ac-4e5f-8f9c-c7c15b51929d'
|
||||
definition: '104083'
|
||||
specificBuildWithTriggering: false
|
||||
allowPartiallySucceededBuilds: true
|
||||
allowFailedBuilds: true
|
||||
buildVersionToDownload: 'latestFromBranch'
|
||||
branchName: 'refs/heads/release/${{ parameters.versionNumber }}-stable'
|
||||
artifactName: 'WindowsAppSDK_Nuget_And_MSIX'
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
$LatestVCToolsVersion = (([xml](& 'C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe' -latest $env:VCWhereExtraVersionTarget -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -include packages -format xml)).instances.instance.packages.package | ? { $_.id -eq "Microsoft.VisualCpp.CRT.Source" }).version;
|
||||
|
||||
$VSInstances = ([xml](& 'C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe' -latest -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -include packages -format xml))
|
||||
$VSPackages = $VSInstances.instances.instance.packages.package
|
||||
$LatestVCPackage = ($VSInstances.instances.instance.packages.package | ? { $_.id -eq "Microsoft.VisualCpp.Tools.Core" })
|
||||
$LatestVCToolsVersion = $LatestVCPackage.version;
|
||||
Write-Output "Latest VCToolsVersion: $LatestVCToolsVersion"
|
||||
Write-Output "Updating VCToolsVersion environment variable for job"
|
||||
Write-Output "##vso[task.setvariable variable=VCToolsVersion]$LatestVCToolsVersion"
|
||||
|
||||
@@ -15,8 +15,8 @@ Param(
|
||||
$referencedFileVersionsPerDll = @{}
|
||||
$totalFailures = 0
|
||||
|
||||
Get-ChildItem $targetDir -Recurse -Filter *.deps.json -Exclude UITests-FancyZones*,MouseJump.Common.UnitTests*,AdvancedPaste.FuzzTests* | ForEach-Object {
|
||||
# Temporarily exclude FancyZones UI tests because of Appium.WebDriver dependencies
|
||||
Get-ChildItem $targetDir -Recurse -Filter *.deps.json -Exclude *UITest*,MouseJump.Common.UnitTests*,*.FuzzTests* | ForEach-Object {
|
||||
# Temporarily exclude All UI-Test, Fuzzer-Test projects because of Appium.WebDriver dependencies
|
||||
$depsJsonFullFileName = $_.FullName
|
||||
$depsJsonFileName = $_.Name
|
||||
$depsJson = Get-Content $depsJsonFullFileName | ConvertFrom-Json
|
||||
@@ -41,10 +41,11 @@ Get-ChildItem $targetDir -Recurse -Filter *.deps.json -Exclude UITests-FancyZone
|
||||
$dllName = Split-Path $_.Name -leaf
|
||||
if([bool]($_.Value.PSObject.Properties.name -match 'fileVersion')) {
|
||||
$dllFileVersion = $_.Value.fileVersion
|
||||
if ([string]::IsNullOrEmpty($dllFileVersion) -and $dllName.StartsWith('PowerToys.'))` {
|
||||
if (([string]::IsNullOrEmpty($dllFileVersion) -or ($dllFileVersion -eq '0.0.0.0')) -and $dllName.StartsWith('PowerToys.'))` {
|
||||
# After VS 17.11 update some of PowerToys dlls have no fileVersion in deps.json even though the
|
||||
# version is correctly set. This is a workaround to skip our dlls as we are confident that all of
|
||||
# our dlls share the same version across the dependencies.
|
||||
# After VS 17.13 these error versions started appearing as 0.0.0.0 so we've added that case to the condition as well.
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ $versionExceptions = @(
|
||||
"Microsoft.Xaml.Interactions.dll",
|
||||
"Microsoft.Xaml.Interactivity.dll",
|
||||
"hyjiacan.py4n.dll",
|
||||
"TraceReloggerLib.dll",
|
||||
"Microsoft.WindowsAppRuntime.Release.Net.dll",
|
||||
"Microsoft.Windows.Widgets.Projection.dll",
|
||||
"WinRT.Host.Shim.dll") -join '|';
|
||||
@@ -59,6 +60,11 @@ if ($items.Count -eq 0) {
|
||||
}
|
||||
|
||||
$items | ForEach-Object {
|
||||
if ($_.VersionInfo.FileVersion -eq "0.0.0.0" -and $_.Name -notmatch $versionExceptions) {
|
||||
# These items are exceptions that actually have the 0.0.0.0 version.
|
||||
Write-Host "Version set to 0.0.0.0: " + $_.FullName
|
||||
$totalFailure++;
|
||||
}
|
||||
if ($_.VersionInfo.FileVersion -eq "1.0.0.0" -and $_.Name -notmatch $versionExceptions) {
|
||||
# These items are exceptions that actually have the 1.0.0.0 version.
|
||||
Write-Host "Version set to 1.0.0.0: " + $_.FullName
|
||||
|
||||
@@ -27,21 +27,21 @@
|
||||
<!-- Including MessagePack to force version, since it's used by StreamJsonRpc but contains vulnerabilities. After StreamJsonRpc updates the version of MessagePack, we can upgrade StreamJsonRpc instead. -->
|
||||
<PackageVersion Include="MessagePack" Version="2.5.187" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.1" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.2" />
|
||||
<!-- Including Microsoft.Bcl.AsyncInterfaces to force version, since it's used by Microsoft.SemanticKernel. -->
|
||||
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.1" />
|
||||
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.Diagnostics.Tracing.TraceEvent" Version="3.1.16" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="9.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel" Version="1.15.0" />
|
||||
<PackageVersion Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.1.2" />
|
||||
<PackageVersion Include="Microsoft.Web.WebView2" Version="1.0.2739.15" />
|
||||
<!-- Package Microsoft.Win32.SystemEvents added as a hack for being able to exclude the runtime assets so they don't conflict with 8.0.1. This is a dependency of System.Drawing.Common but the 8.0.1 version wasn't published to nuget. -->
|
||||
<PackageVersion Include="Microsoft.Win32.SystemEvents" Version="9.0.1" />
|
||||
<PackageVersion Include="Microsoft.Windows.Compatibility" Version="9.0.1" />
|
||||
<PackageVersion Include="Microsoft.Win32.SystemEvents" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.Windows.Compatibility" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.Windows.CsWin32" Version="0.2.46-beta" />
|
||||
<!-- CsWinRT version needs to be set to have a WinRT.Runtime.dll at the same version contained inside the NET SDK we're currently building on CI. -->
|
||||
<!--
|
||||
@@ -67,26 +67,26 @@
|
||||
<PackageVersion Include="StreamJsonRpc" Version="2.19.27" />
|
||||
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
|
||||
<!-- Package System.CodeDom added as a hack for being able to exclude the runtime assets so they don't conflict with 8.0.1. This is a dependency of System.Management but the 8.0.1 version wasn't published to nuget. -->
|
||||
<PackageVersion Include="System.CodeDom" Version="9.0.1" />
|
||||
<PackageVersion Include="System.CodeDom" Version="9.0.2" />
|
||||
<PackageVersion Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
|
||||
<PackageVersion Include="System.ComponentModel.Composition" Version="9.0.1" />
|
||||
<PackageVersion Include="System.Configuration.ConfigurationManager" Version="9.0.1" />
|
||||
<PackageVersion Include="System.Data.OleDb" Version="9.0.1" />
|
||||
<PackageVersion Include="System.ComponentModel.Composition" Version="9.0.2" />
|
||||
<PackageVersion Include="System.Configuration.ConfigurationManager" Version="9.0.2" />
|
||||
<PackageVersion Include="System.Data.OleDb" Version="9.0.2" />
|
||||
<!-- Package System.Data.SqlClient added to force it as a dependency of Microsoft.Windows.Compatibility to the latest version available at this time. -->
|
||||
<PackageVersion Include="System.Data.SqlClient" Version="4.8.6" />
|
||||
<!-- Package System.Diagnostics.EventLog added as a hack for being able to exclude the runtime assets so they don't conflict with 8.0.1. This is a dependency of System.Data.OleDb but the 8.0.1 version wasn't published to nuget. -->
|
||||
<PackageVersion Include="System.Diagnostics.EventLog" Version="9.0.1" />
|
||||
<PackageVersion Include="System.Diagnostics.EventLog" Version="9.0.2" />
|
||||
<!-- Package System.Diagnostics.PerformanceCounter added as a hack for being able to exclude the runtime assets so they don't conflict with 8.0.11. -->
|
||||
<PackageVersion Include="System.Diagnostics.PerformanceCounter" Version="9.0.1" />
|
||||
<PackageVersion Include="System.Drawing.Common" Version="9.0.1" />
|
||||
<PackageVersion Include="System.Diagnostics.PerformanceCounter" Version="9.0.2" />
|
||||
<PackageVersion Include="System.Drawing.Common" Version="9.0.2" />
|
||||
<PackageVersion Include="System.IO.Abstractions" Version="21.0.29" />
|
||||
<PackageVersion Include="System.IO.Abstractions.TestingHelpers" Version="21.0.29" />
|
||||
<PackageVersion Include="System.Management" Version="9.0.1" />
|
||||
<PackageVersion Include="System.Management" Version="9.0.2" />
|
||||
<PackageVersion Include="System.Reactive" Version="6.0.1" />
|
||||
<PackageVersion Include="System.Runtime.Caching" Version="9.0.1" />
|
||||
<PackageVersion Include="System.ServiceProcess.ServiceController" Version="9.0.1" />
|
||||
<PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.1" />
|
||||
<PackageVersion Include="System.Text.Json" Version="9.0.1" />
|
||||
<PackageVersion Include="System.Runtime.Caching" Version="9.0.2" />
|
||||
<PackageVersion Include="System.ServiceProcess.ServiceController" Version="9.0.2" />
|
||||
<PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.2" />
|
||||
<PackageVersion Include="System.Text.Json" Version="9.0.2" />
|
||||
<PackageVersion Include="UnicodeInformation" Version="2.6.0" />
|
||||
<PackageVersion Include="UnitsNet" Version="5.56.0" />
|
||||
<PackageVersion Include="UTF.Unknown" Version="2.5.1" />
|
||||
|
||||
43
NOTICE.md
43
NOTICE.md
@@ -1318,21 +1318,22 @@ EXHIBIT A -Mozilla Public License.
|
||||
- Mages 3.0.0
|
||||
- Markdig.Signed 0.34.0
|
||||
- MessagePack 2.5.187
|
||||
- Microsoft.Bcl.AsyncInterfaces 9.0.1
|
||||
- Microsoft.Bcl.AsyncInterfaces 9.0.2
|
||||
- Microsoft.CodeAnalysis.NetAnalyzers 9.0.0
|
||||
- Microsoft.Data.Sqlite 9.0.1
|
||||
- Microsoft.Data.Sqlite 9.0.2
|
||||
- Microsoft.Diagnostics.Tracing.TraceEvent 3.1.16
|
||||
- Microsoft.Extensions.DependencyInjection 9.0.1
|
||||
- Microsoft.Extensions.Hosting 9.0.1
|
||||
- Microsoft.Extensions.Hosting.WindowsServices 9.0.1
|
||||
- Microsoft.Extensions.Logging 9.0.1
|
||||
- Microsoft.Extensions.Logging.Abstractions 9.0.1
|
||||
- Microsoft.DotNet.ILCompiler (A)
|
||||
- Microsoft.Extensions.DependencyInjection 9.0.2
|
||||
- Microsoft.Extensions.Hosting 9.0.2
|
||||
- Microsoft.Extensions.Hosting.WindowsServices 9.0.2
|
||||
- Microsoft.Extensions.Logging 9.0.2
|
||||
- Microsoft.Extensions.Logging.Abstractions 9.0.2
|
||||
- Microsoft.NET.ILLink.Tasks (A)
|
||||
- Microsoft.SemanticKernel 1.15.0
|
||||
- Microsoft.Toolkit.Uwp.Notifications 7.1.2
|
||||
- Microsoft.Web.WebView2 1.0.2739.15
|
||||
- Microsoft.Win32.SystemEvents 9.0.1
|
||||
- Microsoft.Windows.Compatibility 9.0.1
|
||||
- Microsoft.Win32.SystemEvents 9.0.2
|
||||
- Microsoft.Windows.Compatibility 9.0.2
|
||||
- Microsoft.Windows.CsWin32 0.2.46-beta
|
||||
- Microsoft.Windows.CsWinRT 2.1.5
|
||||
- Microsoft.Windows.SDK.BuildTools 10.0.22621.2428
|
||||
@@ -1350,23 +1351,23 @@ EXHIBIT A -Mozilla Public License.
|
||||
- SharpCompress 0.37.2
|
||||
- StreamJsonRpc 2.19.27
|
||||
- StyleCop.Analyzers 1.2.0-beta.556
|
||||
- System.CodeDom 9.0.1
|
||||
- System.CodeDom 9.0.2
|
||||
- System.CommandLine 2.0.0-beta4.22272.1
|
||||
- System.ComponentModel.Composition 9.0.1
|
||||
- System.Configuration.ConfigurationManager 9.0.1
|
||||
- System.Data.OleDb 9.0.1
|
||||
- System.ComponentModel.Composition 9.0.2
|
||||
- System.Configuration.ConfigurationManager 9.0.2
|
||||
- System.Data.OleDb 9.0.2
|
||||
- System.Data.SqlClient 4.8.6
|
||||
- System.Diagnostics.EventLog 9.0.1
|
||||
- System.Diagnostics.PerformanceCounter 9.0.1
|
||||
- System.Drawing.Common 9.0.1
|
||||
- System.Diagnostics.EventLog 9.0.2
|
||||
- System.Diagnostics.PerformanceCounter 9.0.2
|
||||
- System.Drawing.Common 9.0.2
|
||||
- System.IO.Abstractions 21.0.29
|
||||
- System.IO.Abstractions.TestingHelpers 21.0.29
|
||||
- System.Management 9.0.1
|
||||
- System.Management 9.0.2
|
||||
- System.Reactive 6.0.1
|
||||
- System.Runtime.Caching 9.0.1
|
||||
- System.ServiceProcess.ServiceController 9.0.1
|
||||
- System.Text.Encoding.CodePages 9.0.1
|
||||
- System.Text.Json 9.0.1
|
||||
- System.Runtime.Caching 9.0.2
|
||||
- System.ServiceProcess.ServiceController 9.0.2
|
||||
- System.Text.Encoding.CodePages 9.0.2
|
||||
- System.Text.Json 9.0.2
|
||||
- UnicodeInformation 2.6.0
|
||||
- UnitsNet 5.56.0
|
||||
- UTF.Unknown 2.5.1
|
||||
|
||||
645
PowerToys.sln
645
PowerToys.sln
File diff suppressed because it is too large
Load Diff
160
README.md
160
README.md
@@ -18,7 +18,7 @@ Microsoft PowerToys is a set of utilities for power users to tune and streamline
|
||||
| [New+](https://aka.ms/PowerToysOverview_NewPlus) | [Peek](https://aka.ms/PowerToysOverview_Peek) | [Paste as Plain Text](https://aka.ms/PowerToysOverview_PastePlain) |
|
||||
| [PowerRename](https://aka.ms/PowerToysOverview_PowerRename) | [PowerToys Run](https://aka.ms/PowerToysOverview_PowerToysRun) | [Quick Accent](https://aka.ms/PowerToysOverview_QuickAccent) |
|
||||
| [Registry Preview](https://aka.ms/PowerToysOverview_RegistryPreview) | [Screen Ruler](https://aka.ms/PowerToysOverview_ScreenRuler) | [Shortcut Guide](https://aka.ms/PowerToysOverview_ShortcutGuide) |
|
||||
| [Text Extractor](https://aka.ms/PowerToysOverview_TextExtractor) | [Workspaces](https://aka.ms/PowerToysOverview_Workspaces) | [ZoomIt](https://aka.ms/PowerToysOverview_PowerToysOverview_ZoomIt) |
|
||||
| [Text Extractor](https://aka.ms/PowerToysOverview_TextExtractor) | [Workspaces](https://aka.ms/PowerToysOverview_Workspaces) | [ZoomIt](https://aka.ms/PowerToysOverview_ZoomIt) |
|
||||
|
||||
## Installing and running Microsoft PowerToys
|
||||
|
||||
@@ -34,19 +34,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.88%22
|
||||
[github-current-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.87%22
|
||||
[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.87.1/PowerToysUserSetup-0.87.1-x64.exe
|
||||
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.87.1/PowerToysUserSetup-0.87.1-arm64.exe
|
||||
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.87.1/PowerToysSetup-0.87.1-x64.exe
|
||||
[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.87.1/PowerToysSetup-0.87.1-arm64.exe
|
||||
[github-next-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.90%22
|
||||
[github-current-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.89%22
|
||||
[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.89.0/PowerToysUserSetup-0.89.0-x64.exe
|
||||
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.89.0/PowerToysUserSetup-0.89.0-arm64.exe
|
||||
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.89.0/PowerToysSetup-0.89.0-x64.exe
|
||||
[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.89.0/PowerToysSetup-0.89.0-arm64.exe
|
||||
|
||||
| Description | Filename | sha256 hash |
|
||||
|----------------|----------|-------------|
|
||||
| Per user - x64 | [PowerToysUserSetup-0.87.1-x64.exe][ptUserX64] | 8EFAF47ED00BF230D2C2CC3CB6765C903A6A47E0AAED0BBB329CEF918207B486 |
|
||||
| Per user - ARM64 | [PowerToysUserSetup-0.87.1-arm64.exe][ptUserArm64] | 212FC8055789BD2DC4DE554B9AEE291A9C077907E263A302939266263A9D512B |
|
||||
| Machine wide - x64 | [PowerToysSetup-0.87.1-x64.exe][ptMachineX64] | 69AD65DDAC6436AEF292D2CC6AB1530021CE98083CB3F5FD3380A52A3B0DBB9A |
|
||||
| Machine wide - ARM64 | [PowerToysSetup-0.87.1-arm64.exe][ptMachineArm64] | AEC9F1D02F1E23F0C1FCFDF95C337C962902394F44C0568012DF78BEDB45CF19 |
|
||||
| Per user - x64 | [PowerToysUserSetup-0.89.0-x64.exe][ptUserX64] | B4F130CC96F321024A257499247F6FF6DA56612215ED3882E868AAE26C689E33 |
|
||||
| Per user - ARM64 | [PowerToysUserSetup-0.89.0-arm64.exe][ptUserArm64] | F69B00F4E520EB09FA0D1D1669E21910C5225FE7A2EEDC0FA7C283B201A5F9C6 |
|
||||
| Machine wide - x64 | [PowerToysSetup-0.89.0-x64.exe][ptMachineX64] | E18AC8F9023E341CF7DAD35367FB9DDDB6565D83D8155DBCDDB40AE8A24AE731 |
|
||||
| Machine wide - ARM64 | [PowerToysSetup-0.89.0-arm64.exe][ptMachineArm64] | 17DEADEC601D6061D7AF4F487595CC36D9191813003CC2ECE381017F0EC71FBB |
|
||||
|
||||
This is our preferred method.
|
||||
|
||||
@@ -92,119 +92,103 @@ 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.87 - December 2024 Update
|
||||
### 0.89 - February 2025 Update
|
||||
|
||||
In this release, we focused on new features, stability, and improvements.
|
||||
In this release, we focused on new features, stability, accessibility and automation.
|
||||
|
||||
**Highlights**
|
||||
**✨Highlights**
|
||||
|
||||
- Advanced Paste has a new feature called "Advanced AI" that uses Semantic Kernel to allow setting up the orchestration of sequential clipboard transformations.
|
||||
- Workspaces supports Progressive Web Applications.
|
||||
- Workspaces has a new feature to move existing windows instead of creating new ones.
|
||||
- Mouse Jump added new settings to allow customization of screens pop-up. Thanks [@mikeclayton](https://github.com/mikeclayton)!
|
||||
- New+ now works on Windows 10. Thanks [@cgaarden](https://github.com/cgaarden)!
|
||||
- Quick Accent allows selecting the character sets that should appear on the UI. Thanks [@Sirozha1337](https://github.com/Sirozha1337)!
|
||||
- Enhanced Advanced Paste by adding media transcoding support to convert different video and audio file formats! Thanks [@snickler](https://github.com/snickler) for your help!
|
||||
- Fixed crashes when loading thumbnails after the .NET 9 update and resolved PowerLauncher.exe blocking other MSI installers from creating shortcuts!
|
||||
- Fixed accessibility issues across FancyZones, Image Resizer, and Settings to improve screen reader support and clarity!
|
||||
- Enhanced UI automation framework across modules and added new tests to cover manual checks, with more improvements coming!
|
||||
|
||||
### General
|
||||
|
||||
- Fixed an issue where updating PowerToys on Windows 11 did not properly update context menu entries, impacting New+, PowerRename, Image Resizer, and File Locksmith.
|
||||
- Updated .NET Packages from 9.0.1 to 9.0.2. Thanks [@snickler](https://github.com/snickler) for this.
|
||||
- Enabled compatibility with VS17.3 and later, for C++23. Thanks [@LNKLEO](https://github.com/LNKLEO) for this.
|
||||
|
||||
### Advanced Paste
|
||||
|
||||
- Added a new optional feature allowing using AI to set up the orchestration of sequential clipboard transformations.
|
||||
- Added media transcoding support to convert different video and audio file formats, improved UI layouts, refined clipboard handling, and integrated Semantic Kernel for smarter pasting. Thanks [@snickler](https://github.com/snickler) for your help!
|
||||
|
||||
### Awake
|
||||
### FancyZones
|
||||
|
||||
- Initialization, logging and tray icon setup improvements. Thanks [@dend](https://github.com/dend)!
|
||||
- Fixed accessibility by improving the text for monitors, ensuring clearer naming and help text for screen readers.
|
||||
|
||||
### File Explorer add-ons
|
||||
|
||||
- Preview Pane extensions now use the PerMonitorV2 DPI mode to fix errors on different scales. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
|
||||
|
||||
### Keyboard Manager.
|
||||
|
||||
- Added labels to the IME On, IME Off keys. Thanks [@kit494way](https://github.com/kit494way)!
|
||||
- Fixed an issue that caused the Shift key to remain stuck if a numpad key was mapped to the Shift key.
|
||||
### Image Resizer
|
||||
- Fixed issues with Width and Height fields in Image Resizer's Custom preset, ensuring empty values no longer cause errors, settings save correctly, and auto-scaling behaves as expected. Thanks [@daverayment](https://github.com/daverayment)!
|
||||
- Fixed accessibility by ensuring screen readers announce selected image dimensions in the combo-box for better navigation.
|
||||
|
||||
### Monaco Preview
|
||||
|
||||
- Added support for .ahk files to be shown as a plaintext file in Peek and File Explorer add-ons. Thanks [@daverayment](https://github.com/daverayment)!
|
||||
- Added support for .ion files to be shown as a plaintext file in Peek and File Explorer add-ons. Thanks [@octastylos-pseudodipteros](https://github.com/octastylos-pseudodipteros)!
|
||||
- Added support for syntax highlighting for .srt files in Peek and File Explorer add-ons. Thanks [@PesBandi](https://github.com/PesBandi)!
|
||||
- Fixed open link in default browser rather than Microsoft Edge. Thanks [@OldUser101](https://github.com/OldUser101)!
|
||||
|
||||
### Mouse Jump
|
||||
### Mouse Highlighter
|
||||
|
||||
- Allow customizing the appearance of the UI of the Mouse Jump pop-up. Thanks [@mikeclayton](https://github.com/mikeclayton)!
|
||||
- Fixed a highlight released on an Administrator window will start fading, instead of staying on the screen indefinitely until the mouse button is pressed again on an unelevated window.
|
||||
|
||||
### New+
|
||||
### Mouse Without Borders
|
||||
- Fixed an issue in service mode where copy-paste and drag-drop file transfers didn’t work, ensuring seamless file operations.
|
||||
- Enabled GPO for enable/disable for Mouse Without Borders in Service Mode. Thanks [@htcfreek](https://github.com/htcfreek) for review and comments!
|
||||
- Fixed code maintainability by refactoring the oversized 'Common' class in Mouse Without Borders into smaller, focused classes for better structure and clarity. Thanks [@mikeclayton](https://github.com/mikeclayton) and thanks [@htcfreek](https://github.com/htcfreek) for review!
|
||||
|
||||
- Added support for Windows 10. Thanks [@cgaarden](https://github.com/cgaarden)!
|
||||
- Fixed an issue causing the renaming of new files to not trigger some times. Thanks [@cgaarden](https://github.com/cgaarden)!
|
||||
- Updated the New+ icons. Thanks [@niels9001](https://github.com/niels9001)!
|
||||
|
||||
### Peek
|
||||
|
||||
- Peek now checks local capabilities to decide what image formats Image Previewer is able to support. Thanks [@daverayment](https://github.com/daverayment)!
|
||||
- Fixed an issue causing the Code Files Previewer to not load correctly under certain conditions. Thanks [@daverayment](https://github.com/daverayment)!
|
||||
- Refactored, improved and fixed logging when loading the user settings file. Thanks [@daverayment](https://github.com/daverayment)!
|
||||
### PowerRename
|
||||
- Supported negative value as Start value in regular expression, e.g. ${start=-1314}
|
||||
- Enhanced RegEx help by adding $, ^, quantifiers, and common patterns for better usability. Thanks [@PesBandi](https://github.com/PesBandi) and thanks [@htcfreek](https://github.com/htcfreek) for review.
|
||||
|
||||
### PowerToys Run
|
||||
|
||||
- Added a scoring function for proper ordering of the WindowWalker plugin results. Thanks [@andbartol](https://github.com/andbartol)!
|
||||
- Added UUIDv7 support to the ValueGenerator plugin. Thanks [@frederik-hoeft](https://github.com/frederik-hoeft)!
|
||||
- The calculator plugin now allows scientific notation numbers with a lowercase 'e'. Thanks [@PesBandi](https://github.com/PesBandi)!
|
||||
- Ported the UI from WPF-UI to .NET 9 WPF, to fix "Desktop composition is disabled" crashes.
|
||||
- Fixed crashes when loading thumbnails after the .NET 9 update by disabling CETCompat.
|
||||
- Fixed PowerLauncher.exe blocking other MSI installers creating shortcuts. Thanks [@OneBlue](https://github.com/OneBlue)!
|
||||
- Fixed Run’s dark mode detection to work reliably, preventing issues with incorrect theme detection and ensuring a smoother user experience. Thanks [@daverayment](https://github.com/daverayment)!
|
||||
- Fixed list separator handling in Calculator, allowing functions with multiple arguments to work correctly across different locales. For example pow(2;3) would be replaced with pow(2,3). Thanks [@PesBandi](https://github.com/PesBandi) and thanks [@htcfreek](https://github.com/htcfreek) for review!
|
||||
- Fixed angle unit conversions in the PowerToys Run calculator, allowing quick conversions between radians, degrees, and gradians. Thanks [@OldUser101](https://github.com/OldUser101)!
|
||||
|
||||
### Quick Accent
|
||||
|
||||
- Added a setting to allow selecting which character sets to show. Thanks [@Sirozha1337](https://github.com/Sirozha1337)!
|
||||
|
||||
### Screen Ruler
|
||||
|
||||
- Added a Setting to also allow showing measurements in inches, centimeters or millimeters. Thanks [@Sophanatprime](https://github.com/Sophanatprime)!
|
||||
- Added ǎ, ǒ and ǔ to the IPA character set. Thanks [@PesBandi](https://github.com/PesBandi)!
|
||||
- Added ` (backtick) and ~ (tilde) to the VK_OEM_5 character set. Thanks [@xanatos](https://github.com/xanatos)!
|
||||
- Added ς (final sigma) to the Greek character set. Thanks [@IamSmeagol](https://github.com/IamSmeagol)!
|
||||
|
||||
### Settings
|
||||
|
||||
- Fixed an issue causing all the links to milestones in the "What's new?" OOBE page to point to the same milestone.
|
||||
- Removed extra space from the Welcome page. Thanks [@agarwalishita](https://github.com/agarwalishita)!
|
||||
- Updated left navigation bar icons. Thanks [@niels9001](https://github.com/niels9001)!
|
||||
- Fixed accessibility issues in the dashboard page. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
|
||||
- Enabled GPO for the "run at startup" setting. Thanks [@htcfreek](https://github.com/htcfreek) for review and comments!
|
||||
- Fixed accessibility issue by allowing screen readers to announce the group name for secondary links in Settings pages, instead of reading link descriptions without context.
|
||||
- Fixed an issue where the Color Picker shortcut was not displaying correctly in the Dashboard.
|
||||
|
||||
### Workspaces
|
||||
|
||||
- Added support for Progressive Web Applications to Workspaces.
|
||||
- Implemented a feature to move existing windows instead of creating new ones.
|
||||
- Fixed a crash when opening the workspaces editor that was caused by passing incorrect encoder parameters when saving Bitmap files.
|
||||
- Workspaces editor position is now saved so that we can start it at the same position when we open it again.
|
||||
- Fixed an issue causing many instances of the same application to be put in the same position instead of the intended position due to timer issues.
|
||||
- Fixed detection of exact application version when many versions of the same application are installed.
|
||||
- Fixed if a window was last placed on a disconnected monitor, it launches minimized and repositions within the main monitor's visible area when restored, instead of remaining off-screen and invisible.
|
||||
- Fixed on ARM64 to correctly display icons for packaged apps by resolving path mismatches.
|
||||
|
||||
### ZoomIt
|
||||
|
||||
- Fixed warning C4706 and related error C2220 during build. Thanks [@xanatos](https://github.com/xanatos)!
|
||||
|
||||
### Documentation
|
||||
|
||||
- Improved language in CONTRIBUTE.md. Thanks [@sanskaarz](https://github.com/sanskaarz)!
|
||||
- Added Bilibili plugin mention to thirdPartyRunPlugins.md. Thanks [@Whuihuan](https://github.com/Whuihuan)!
|
||||
- Added CanIUse and TailwindCSS plugins mention to thirdPartyRunPlugins.md. Thanks [@skttl](https://github.com/skttl)!
|
||||
- Added HttpStatusCodes plugin mention to thirdPartyRunPlugins.md. Thanks [@grzhan](https://github.com/grzhan)!
|
||||
- Updated COMMUNITY.md with more contributors.
|
||||
- Fixed runner-ipc.md doc on the broken link. Thanks [@daverayment](https://github.com/daverayment)!
|
||||
- Fixed the new plugin checklist by updating the target framework, removing duplicates, and improving statement organization. Thanks [@hlaueriksson](https://github.com/hlaueriksson)!
|
||||
- Updated runner documentation to align with the latest code structure.
|
||||
|
||||
### Development
|
||||
|
||||
- Upgraded to .NET 9. Thanks [@snickler](https://github.com/snickler)!
|
||||
- Fixed building on Visual Studio 17.12.
|
||||
- Upgraded the System.IO.Abstractions dependency to 21.0.29. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
|
||||
- Upgraded the WindowsAppSDK dependency to 1.6.241114003. Thanks [@shuaiyuanxx](https://github.com/shuaiyuanxx)!
|
||||
- Upgraded the MSTest dependency to 3.6.3. Thanks [@Youssef1313](https://github.com/Youssef1313)!
|
||||
- Upgraded the check-spelling CI dependency to 0.0.24 and fixed related spell checking issues. Thanks [@jsoref](https://github.com/jsoref)!
|
||||
- Removed duplicate names from the spellcheck allowed names file. Thanks [@htcfreek](https://github.com/htcfreek)!
|
||||
- Improved logging of asynchronous methods call stacks when logging an error.
|
||||
- Created a MSBuild props file to be imported by other projects to enable AOT support.
|
||||
- Made the Peek utility source code AOT compatible.
|
||||
- Updated .editorconfig rules to relax squiggly IDE errors in Visual Studio 17.12. Thanks [@snickler](https://github.com/snickler)!
|
||||
- Moved Xaml.Styler from the root to the src folder.
|
||||
- Stabilized pipeline on ARM64 and forked build.
|
||||
- Added fuzz testing for HostUILib, added as part of pipeline for OneFuzz.
|
||||
- Fixed and improved UI-Test automation framework, and added new test cases for the FancyZones and Hosts module.
|
||||
- Optimized Logger function as AOT compatible, improving performance by 18%.
|
||||
- Made Common.UI and Setting.UI to be AOT compatible.
|
||||
|
||||
### What is being planned for version 0.90
|
||||
|
||||
#### What is being planned for version 0.88
|
||||
For [v0.90][github-next-release-work], we'll work on the items below:
|
||||
|
||||
For [v0.88][github-next-release-work], we'll work on the items below:
|
||||
|
||||
- Stability / bug fixes
|
||||
- New module: PowerToys Run v2
|
||||
- New module: File Actions Menu
|
||||
- Integrate Sysinternals ZoomIt
|
||||
- Working on installer upgrades
|
||||
- Upgrading keyboard manager's editor UI
|
||||
- Stability / bug fixes
|
||||
|
||||
## PowerToys Community
|
||||
|
||||
|
||||
91
doc/devdocs/UITests.md
Normal file
91
doc/devdocs/UITests.md
Normal file
@@ -0,0 +1,91 @@
|
||||
# UI tests framework
|
||||
|
||||
A specialized UI test framework for PowerToys that makes it easy to write UI tests for PowerToys modules or settings. Let's start writing UI tests!
|
||||
|
||||
## Before running tests
|
||||
|
||||
- Install Windows Application Driver v1.2.1 from https://github.com/microsoft/WinAppDriver/releases/tag/v1.2.1 to the default directory (`C:\Program Files (x86)\Windows Application Driver`)
|
||||
|
||||
- Enable Developer Mode in Windows settings
|
||||
|
||||
## Running tests
|
||||
|
||||
- Exit PowerToys if it's running.
|
||||
|
||||
- Open `PowerToys.sln` in Visual Studio and build the solution.
|
||||
|
||||
- Run tests in the Test Explorer (`Test > Test Explorer` or `Ctrl+E, T`).
|
||||
|
||||
|
||||
## How to add the first UI tests for your modules
|
||||
|
||||
- Create a new project and add the following references to the project file. Change the OutputPath to your own module's path.
|
||||
```
|
||||
<PropertyGroup>
|
||||
<OutputType>Library</OutputType>
|
||||
<!-- This is a UI test, so don't run as part of MSBuild -->
|
||||
<RunVSTest>false</RunVSTest>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputPath>..\..\..\..\$(Platform)\$(Configuration)\tests\KeyboardManagerUITests\</OutputPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MSTest" />
|
||||
<ProjectReference Include="..\..\..\common\UITestAutomation\UITestAutomation.csproj" />
|
||||
<Folder Include="Properties\" />
|
||||
</ItemGroup>
|
||||
```
|
||||
- Inherit your test class from UITestBase.
|
||||
>Set Scope: The default scope starts from the PowerToys settings UI. If you want to start from your own module, set the constructor as shown below:
|
||||
|
||||
>Specify Scope:
|
||||
```
|
||||
[TestClass]
|
||||
public class RunFancyZonesTest : UITestBase
|
||||
{
|
||||
public RunFancyZonesTest()
|
||||
: base(PowerToysModule.FancyZone)
|
||||
{
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- Then you can start using session to perform the UI operations.
|
||||
|
||||
**Example**
|
||||
```
|
||||
using Microsoft.PowerToys.UITest;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace UITests_KeyboardManager
|
||||
{
|
||||
[TestClass]
|
||||
public class RunKeyboardManagerUITests : UITestBase
|
||||
{
|
||||
[TestMethod]
|
||||
public void OpenKeyboardManagerEditor()
|
||||
{
|
||||
// Open KeyboardManagerEditor
|
||||
this.Session.Find<Button>(By.Name("Remap a key")).Click();
|
||||
this.Session.Attach("Remap keys");
|
||||
|
||||
// Maximize window
|
||||
var window = Session.Find<Window>(By.Name("Remap keys")).Maximize();
|
||||
|
||||
// Add Key Remapping
|
||||
this.Session.Find<Button>(By.Name("Add key remapping")).Click();
|
||||
window.Close();
|
||||
|
||||
// Back to Settings
|
||||
this.Session.Attach(PowerToysModule.PowerToysSettings);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Extra tools and information
|
||||
|
||||
**Accessibility Tools**:
|
||||
While working on tests, you may need a tool that helps you to view the element's accessibility data, e.g. for finding the button to click. For this purpose, you could use [AccessibilityInsights](https://accessibilityinsights.io/docs/windows/overview)
|
||||
@@ -3,8 +3,9 @@
|
||||
- [ ] The plugin is a project under `modules\launcher\Plugins`
|
||||
- [ ] Microsoft plugin project name pattern: `Microsoft.PowerToys.Run.Plugin.{PluginName}`
|
||||
- [ ] Community plugin project name pattern: `Community.PowerToys.Run.Plugin.{PluginName}`
|
||||
- [ ] The plugin target framework should be `net8.0-windows`
|
||||
- [ ] The plugin target framework should be `net9.0-windows10.0.22621.0`
|
||||
- [ ] If the plugin uses any 3rd party dependencies the project file should import `DynamicPlugin.props`
|
||||
- [ ] 3rd party dependencies must be compatible with .NET 9
|
||||
- [ ] The plugin has to contain a `plugin.json` file of the following format in its root folder:
|
||||
|
||||
```json
|
||||
@@ -35,7 +36,6 @@ public static string PluginID => "xxxxxxx"; // The part xxxxxxx stands for the p
|
||||
- [ ] Plugin's output code and assets have to be included in the installer [`Product.wxs`](/installer/PowerToysSetup/Product.wxs)
|
||||
- [ ] Test the plugin with a local build. Build the installer, install, check that the plugin works as expected
|
||||
- [ ] All plugin's binaries have to be included in the signed build [`pipeline.user.windows.yml`](/.pipelines/pipeline.user.windows.yml)
|
||||
- [ ] The plugin target framework has to be net8.0-windows. All dependencies should be compatible with .NET 8.
|
||||
|
||||
Some localization steps can only be done after the first pass by the localization team to provide the localized resources.
|
||||
In the PR that adds a new plugin, reference a new issue to track the work for fully enabling localization for the new plugin.
|
||||
|
||||
@@ -4,18 +4,10 @@ Contains the executable starting point, initialization code and the list of know
|
||||
#### [`powertoy_module.h`](/src/runner/powertoy_module.h) and [`powertoy_module.cpp`](/src/runner/powertoy_module.cpp)
|
||||
Contains code for initializing and managing the PowerToy modules. `PowertoyModule` is a RAII-style holder for the `PowertoyModuleIface` pointer, which we got by [invoking module DLL's `powertoy_create` function](https://github.com/microsoft/PowerToys/blob/1760af50c8803588cb575167baae0439af38a9c1/src/runner/powertoy_module.cpp#L13-L24).
|
||||
|
||||
#### [`powertoys_events.cpp`](/src/runner/powertoys_events.cpp)
|
||||
Contains code that handles the various events listeners, and forwards those events to the PowerToys modules. You can learn more about the current event architecture in [shared hooks](/doc/devdocs/shared-hooks.md).
|
||||
|
||||
#### [`lowlevel_keyboard_event.cpp`](/src/runner/lowlevel_keyboard_event.cpp)
|
||||
Contains code for registering the low level keyboard event hook that listens for keyboard events. Please note that `signal_event` is called from the main thread for this event.
|
||||
|
||||
#### [`win_hook_event.cpp`](/src/runner/win_hook_event.cpp)
|
||||
Contains code for registering a Windows event hook through `SetWinEventHook`, that listens for various events raised when a window is interacted with. Please note, that `signal_event` is called from a separate `dispatch_thread_proc` worker thread, so you must provide thread-safety for your `signal_event` if you intend to receive it. This is a subject to change.
|
||||
|
||||
#### [`tray_icon.cpp`](/src/runner/tray_icon.cpp)
|
||||
Contains code for managing the PowerToys tray icon and its menu commands. Note that `dispatch_run_on_main_ui_thread` is used to
|
||||
transfer received json message from the [Settings window](/doc/devdocs/settings.md) to the main thread, since we're communicating with it from [a dedicated thread](https://github.com/microsoft/PowerToys/blob/7357e40d3f54de51176efe54fda6d57028837b8c/src/runner/settings_window.cpp#L267-L271).
|
||||
|
||||
#### [`settings_window.cpp`](/src/runner/settings_window.cpp)
|
||||
Contains code for starting the PowerToys settings window and communicating with it. Settings window is a separate process, so we're using [Windows pipes](https://learn.microsoft.com/windows/win32/ipc/pipes) as a transport for json messages.
|
||||
|
||||
@@ -33,3 +25,24 @@ Contains code for telemetry.
|
||||
|
||||
#### [`svgs`](/src/runner/svgs/)
|
||||
Contains the SVG assets used by the PowerToys modules.
|
||||
|
||||
#### [`bug_report.cpp`](/src/runner/bug_report.cpp)
|
||||
Contains logic to start bug report tool.
|
||||
|
||||
#### [`centralized_hotkeys.cpp`](/src/runner/centralized_hotkeys.cpp)
|
||||
Contains hot key logic registration and un-registration.
|
||||
|
||||
#### [`centralized_kb_hook.cpp`](/src/runner/centralized_kb_hook.cpp)
|
||||
Contains logic to handle PowerToys' keyboard shortcut functionality.
|
||||
|
||||
#### [`restart_elevated.cpp`](/src/runner/restart_elevated.cpp)
|
||||
Contains logic for restarting the current process with different elevation levels.
|
||||
|
||||
#### [`RestartManagement.cpp`](/src/runner/RestartManagement.cpp)
|
||||
Contains code for restarting a process.
|
||||
|
||||
#### [`settings_telemetry.cpp`](/src/runner/settings_telemetry.cpp)
|
||||
Contains logic that periodically triggers module-specific setting's telemetry delivery and manages timing and error handling for the process.
|
||||
|
||||
#### [`UpdateUtils.cpp`](/src/runner/UpdateUtils.cpp)
|
||||
Contains code to handle the automatic update checking, notification, and installation process for PowerToys.
|
||||
@@ -94,5 +94,5 @@ Note that we've supplied `Debug` option, so a `%TEMP\PowerToys.DSC.TestConfigure
|
||||
Finally, you can test it with winget by invoking it as such:
|
||||
|
||||
```ps
|
||||
winget configure .\configuration.dsc.yaml --accept-configuration-agreements --disable-interactivity
|
||||
winget configure .\configuration.winget --accept-configuration-agreements --disable-interactivity
|
||||
```
|
||||
@@ -3,8 +3,8 @@
|
||||
The Settings v2 process uses two way IPC to communicate with the runner process.
|
||||
|
||||
## Initialization
|
||||
- On the settings' side, the two way IPC delegates are contained with the [`ShellPage.xaml.cs`](/src/settings-ui/Settings.UI/Views/ShellPage.xaml.cs) file. The delegates are static and the views for all the powerToys send the ipc information to the viewmodels as `ShellPage.DefaultSndMSGCallBack`.
|
||||
- These delegates are initialized within the [`MainWindow.xaml.cs`](/src/settings-ui/Settings.UI/MainWindow.xaml.cs) file in the `Settings.Runner` project.
|
||||
- On the settings' side, the two way IPC delegates are contained with the [`ShellPage.xaml.cs`](/src/settings-ui/Settings.UI/SettingsXAML/Views/ShellPage.xaml.cs) file. The delegates are static and the views for all the powerToys send the ipc information to the viewmodels as `ShellPage.DefaultSndMSGCallBack`.
|
||||
- These delegates are initialized within the [`MainWindow.xaml.cs`](/src/settings-ui/Settings.UI/SettingsXAML/MainWindow.xaml.cs) file in the `Settings.Runner` project.
|
||||
|
||||
|
||||
## Types of IPC delegates
|
||||
@@ -14,12 +14,12 @@ The Settings v2 process uses two way IPC to communicate with the runner process.
|
||||
3. `CheckForUpdates`
|
||||
|
||||
## Sending information to runner
|
||||
- The settings process communicates with the runner by using the delegates defined within the [`ShellPage.xaml.cs`](/src/settings-ui/Settings.UI/Views/ShellPage.xaml.cs) file.
|
||||
- The settings process communicates with the runner by using the delegates defined within the [`ShellPage.xaml.cs`](/src/settings-ui/Settings.UI/SettingsXAML/Views/ShellPage.xaml.cs) file.
|
||||
- Depending on the type of object sending the information, the json is created accordingly.
|
||||
- If any information has been modified by the user in the GeneralSettings page, then the json file sent to the runner has the name set to `general`, whereas if any information has been modified by the user in any powertoy related settings page, the name of the json file being communicated with the runner is set to `powertoy`.
|
||||
|
||||
## Receiving information from runner
|
||||
- The `ShellPage`object has a `IPCResponseHandleList` which is a list of functions which handle IPC responses.
|
||||
- The `ShellPage` object has a `IPCResponseHandleList` which is a list of functions which handle IPC responses.
|
||||
|
||||
```csharp
|
||||
// receive IPC Message
|
||||
@@ -43,4 +43,4 @@ Program.IPCMessageReceivedCallback = (string msg) =>
|
||||
```
|
||||
|
||||
- Whenever any information is sent from the runner each of the functions in the handle list perform their action on that json object.
|
||||
- One example of where information sent from the runner is being processed by the settings is in [`GeneralPage.xaml.cs`](/src/settings-ui/Settings.UI/Views/GeneralPage.xaml.cs) when the user clicks the check for updates button. The information displayed after, such as the user has the latest version installed is a result of this handle.
|
||||
- One example of where information sent from the runner is being processed by the settings is in [`GeneralPage.xaml.cs`](/src/settings-ui/Settings.UI/SettingsXAML/Views/GeneralPage.xaml.cs) when the user clicks the check for updates button. The information displayed after, such as the user has the latest version installed is a result of this handle.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<!-- Look at Directory.Build.props in root for common stuff as well -->
|
||||
<Import Project="..\..\Common.Dotnet.CsWinRT.props" />
|
||||
<Import Project="..\..\Common.Dotnet.AotCompatibility.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<UseWPF>true</UseWPF>
|
||||
|
||||
@@ -91,20 +91,20 @@ namespace Common.UI
|
||||
{
|
||||
try
|
||||
{
|
||||
var assemblyPath = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
|
||||
var fullPath = new DirectoryInfo(assemblyPath).FullName;
|
||||
var directoryPath = System.AppContext.BaseDirectory;
|
||||
if (mainExecutableIsOnTheParentFolder)
|
||||
{
|
||||
// Need to go into parent folder for PowerToys.exe. Likely a WinUI3 App SDK application.
|
||||
fullPath = fullPath + "\\..\\PowerToys.exe";
|
||||
directoryPath = Path.Combine(directoryPath, "..");
|
||||
directoryPath = Path.Combine(directoryPath, "PowerToys.exe");
|
||||
}
|
||||
else
|
||||
{
|
||||
// PowerToys.exe is in the same path as the application.
|
||||
fullPath = fullPath + "\\PowerToys.exe";
|
||||
directoryPath = Path.Combine(directoryPath, "PowerToys.exe");
|
||||
}
|
||||
|
||||
Process.Start(new ProcessStartInfo(fullPath) { Arguments = "--open-settings=" + SettingsWindowNameToString(window) });
|
||||
Process.Start(new ProcessStartInfo(directoryPath) { Arguments = "--open-settings=" + SettingsWindowNameToString(window) });
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
@@ -11,7 +11,7 @@ using Microsoft.Win32;
|
||||
|
||||
namespace Common.UI
|
||||
{
|
||||
public class ThemeManager : IDisposable
|
||||
public partial class ThemeManager : IDisposable
|
||||
{
|
||||
private readonly Application _app;
|
||||
private const string LightTheme = "Light.Accent1";
|
||||
|
||||
@@ -200,6 +200,10 @@ namespace winrt::PowerToys::GPOWrapper::implementation
|
||||
{
|
||||
return static_cast<GpoRuleConfigured>(powertoys_gpo::getConfiguredMwbDisallowBlockingScreensaverValue());
|
||||
}
|
||||
GpoRuleConfigured GPOWrapper::GetConfiguredMwbAllowServiceModeValue()
|
||||
{
|
||||
return static_cast<GpoRuleConfigured>(powertoys_gpo::getConfiguredMwbAllowServiceModeValue());
|
||||
}
|
||||
GpoRuleConfigured GPOWrapper::GetConfiguredMwbSameSubnetOnlyValue()
|
||||
{
|
||||
return static_cast<GpoRuleConfigured>(powertoys_gpo::getConfiguredMwbSameSubnetOnlyValue());
|
||||
@@ -228,4 +232,8 @@ namespace winrt::PowerToys::GPOWrapper::implementation
|
||||
{
|
||||
return static_cast<GpoRuleConfigured>(powertoys_gpo::getAllowDataDiagnosticsValue());
|
||||
}
|
||||
GpoRuleConfigured GPOWrapper::GetConfiguredRunAtStartupValue()
|
||||
{
|
||||
return static_cast<GpoRuleConfigured>(powertoys_gpo::getConfiguredRunAtStartupValue());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,12 +56,14 @@ namespace winrt::PowerToys::GPOWrapper::implementation
|
||||
static GpoRuleConfigured GetConfiguredMwbFileTransferEnabledValue();
|
||||
static GpoRuleConfigured GetConfiguredMwbUseOriginalUserInterfaceValue();
|
||||
static GpoRuleConfigured GetConfiguredMwbDisallowBlockingScreensaverValue();
|
||||
static GpoRuleConfigured GetConfiguredMwbAllowServiceModeValue();
|
||||
static GpoRuleConfigured GetConfiguredMwbSameSubnetOnlyValue();
|
||||
static GpoRuleConfigured GetConfiguredMwbValidateRemoteIpValue();
|
||||
static GpoRuleConfigured GetConfiguredMwbDisableUserDefinedIpMappingRulesValue();
|
||||
static winrt::hstring GPOWrapper::GetConfiguredMwbPolicyDefinedIpMappingRules();
|
||||
static GpoRuleConfigured GetConfiguredNewPlusHideTemplateFilenameExtensionValue();
|
||||
static GpoRuleConfigured GetAllowDataDiagnosticsValue();
|
||||
static GpoRuleConfigured GetConfiguredRunAtStartupValue();
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -60,12 +60,14 @@ namespace PowerToys
|
||||
static GpoRuleConfigured GetConfiguredMwbFileTransferEnabledValue();
|
||||
static GpoRuleConfigured GetConfiguredMwbUseOriginalUserInterfaceValue();
|
||||
static GpoRuleConfigured GetConfiguredMwbDisallowBlockingScreensaverValue();
|
||||
static GpoRuleConfigured GetConfiguredMwbAllowServiceModeValue();
|
||||
static GpoRuleConfigured GetConfiguredMwbSameSubnetOnlyValue();
|
||||
static GpoRuleConfigured GetConfiguredMwbValidateRemoteIpValue();
|
||||
static GpoRuleConfigured GetConfiguredMwbDisableUserDefinedIpMappingRulesValue();
|
||||
static String GetConfiguredMwbPolicyDefinedIpMappingRules();
|
||||
static GpoRuleConfigured GetConfiguredNewPlusHideTemplateFilenameExtensionValue();
|
||||
static GpoRuleConfigured GetAllowDataDiagnosticsValue();
|
||||
static GpoRuleConfigured GetConfiguredRunAtStartupValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ using System;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using ManagedCommon.Serialization;
|
||||
|
||||
namespace ManagedCommon
|
||||
{
|
||||
@@ -35,7 +36,7 @@ namespace ManagedCommon
|
||||
inputStream.Close();
|
||||
reader.Dispose();
|
||||
|
||||
return JsonSerializer.Deserialize<OutGoingLanguageSettings>(data).LanguageTag;
|
||||
return JsonSerializer.Deserialize<OutGoingLanguageSettings>(data, SourceGenerationContext.Default.OutGoingLanguageSettings).LanguageTag;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
|
||||
@@ -15,15 +15,23 @@ namespace ManagedCommon
|
||||
{
|
||||
public static class Logger
|
||||
{
|
||||
private static readonly Assembly Assembly = Assembly.GetExecutingAssembly();
|
||||
private static readonly string Version = FileVersionInfo.GetVersionInfo(Assembly.Location).ProductVersion;
|
||||
|
||||
private static readonly string Error = "Error";
|
||||
private static readonly string Warning = "Warning";
|
||||
private static readonly string Info = "Info";
|
||||
private static readonly string Debug = "Debug";
|
||||
private static readonly string TraceFlag = "Trace";
|
||||
|
||||
private static readonly Assembly Assembly = Assembly.GetExecutingAssembly();
|
||||
|
||||
/*
|
||||
* Please pay more attention!
|
||||
* If you want to publish it with Native AOT enabled (or publish as a single file).
|
||||
* You need to find another way to remove Assembly.Location usage.
|
||||
*/
|
||||
#pragma warning disable IL3000 // Avoid accessing Assembly file path when publishing as a single file
|
||||
private static readonly string Version = FileVersionInfo.GetVersionInfo(Assembly.Location).ProductVersion;
|
||||
#pragma warning restore IL3000 // Avoid accessing Assembly file path when publishing as a single file
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the logger and sets the path for logging.
|
||||
/// </summary>
|
||||
@@ -53,18 +61,16 @@ namespace ManagedCommon
|
||||
Trace.AutoFlush = true;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static void LogError(string message)
|
||||
public static void LogError(string message, [System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0)
|
||||
{
|
||||
Log(message, Error);
|
||||
Log(message, Error, memberName, sourceFilePath, sourceLineNumber);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static void LogError(string message, Exception ex)
|
||||
public static void LogError(string message, Exception ex, [System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0)
|
||||
{
|
||||
if (ex == null)
|
||||
{
|
||||
Log(message, Error);
|
||||
Log(message, Error, memberName, sourceFilePath, sourceLineNumber);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -83,38 +89,33 @@ namespace ManagedCommon
|
||||
"Stack trace: " + Environment.NewLine +
|
||||
ex.StackTrace;
|
||||
|
||||
Log(exMessage, Error);
|
||||
Log(exMessage, Error, memberName, sourceFilePath, sourceLineNumber);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static void LogWarning(string message)
|
||||
public static void LogWarning(string message, [System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0)
|
||||
{
|
||||
Log(message, Warning);
|
||||
Log(message, Warning, memberName, sourceFilePath, sourceLineNumber);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static void LogInfo(string message)
|
||||
public static void LogInfo(string message, [System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0)
|
||||
{
|
||||
Log(message, Info);
|
||||
Log(message, Info, memberName, sourceFilePath, sourceLineNumber);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static void LogDebug(string message)
|
||||
public static void LogDebug(string message, [System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0)
|
||||
{
|
||||
Log(message, Debug);
|
||||
Log(message, Debug, memberName, sourceFilePath, sourceLineNumber);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static void LogTrace()
|
||||
public static void LogTrace([System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0)
|
||||
{
|
||||
Log(string.Empty, TraceFlag);
|
||||
Log(string.Empty, TraceFlag, memberName, sourceFilePath, sourceLineNumber);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static void Log(string message, string type)
|
||||
private static void Log(string message, string type, string memberName, string sourceFilePath, int sourceLineNumber)
|
||||
{
|
||||
Trace.WriteLine("[" + DateTime.Now.TimeOfDay + "] [" + type + "] " + GetCallerInfo());
|
||||
Trace.WriteLine("[" + DateTime.Now.TimeOfDay + "] [" + type + "] " + GetCallerInfo(memberName, sourceFilePath, sourceLineNumber));
|
||||
Trace.Indent();
|
||||
if (message != string.Empty)
|
||||
{
|
||||
@@ -124,49 +125,27 @@ namespace ManagedCommon
|
||||
Trace.Unindent();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static string GetCallerInfo()
|
||||
private static string GetCallerInfo(string memberName, string sourceFilePath, int sourceLineNumber)
|
||||
{
|
||||
StackTrace stackTrace = new();
|
||||
|
||||
var callerMethod = GetCallerMethod(stackTrace);
|
||||
|
||||
return $"{callerMethod?.DeclaringType?.Name}::{callerMethod.Name}";
|
||||
}
|
||||
|
||||
private static MethodBase GetCallerMethod(StackTrace stackTrace)
|
||||
{
|
||||
const int topFrame = 3;
|
||||
|
||||
var topMethod = stackTrace.GetFrame(topFrame)?.GetMethod();
|
||||
string callerFileName = "Unknown";
|
||||
|
||||
try
|
||||
{
|
||||
if (topMethod?.Name == nameof(IAsyncStateMachine.MoveNext) && typeof(IAsyncStateMachine).IsAssignableFrom(topMethod?.DeclaringType))
|
||||
string fileName = Path.GetFileName(sourceFilePath);
|
||||
if (!string.IsNullOrEmpty(fileName))
|
||||
{
|
||||
// Async method; return actual method as determined by heuristic:
|
||||
// "Nearest method on stack to async state-machine's MoveNext() in same namespace but in a different type".
|
||||
// There are tighter ways of determining the actual method, but this is good enough and probably faster.
|
||||
for (int deepFrame = topFrame + 1; deepFrame < stackTrace.FrameCount; deepFrame++)
|
||||
{
|
||||
var deepMethod = stackTrace.GetFrame(deepFrame)?.GetMethod();
|
||||
|
||||
if (deepMethod?.DeclaringType != topMethod?.DeclaringType && deepMethod?.DeclaringType?.Namespace == topMethod?.DeclaringType?.Namespace)
|
||||
{
|
||||
return deepMethod;
|
||||
}
|
||||
}
|
||||
callerFileName = fileName;
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Ignore exceptions in Release. The code above won't throw, but if it does, we don't want to crash the app.
|
||||
callerFileName = "Unknown";
|
||||
#if DEBUG
|
||||
throw;
|
||||
#endif
|
||||
}
|
||||
|
||||
return topMethod;
|
||||
return $"{callerFileName}::{memberName}::{sourceLineNumber}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<!-- Look at Directory.Build.props in root for common stuff as well -->
|
||||
<Import Project="..\..\Common.Dotnet.CsWinRT.props" />
|
||||
<Import Project="..\..\Common.Dotnet.AotCompatibility.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<Description>PowerToys ManagedCommon</Description>
|
||||
|
||||
@@ -53,7 +53,7 @@ namespace ManagedCommon
|
||||
|
||||
internal static int Size
|
||||
{
|
||||
get { return Marshal.SizeOf(typeof(INPUT)); }
|
||||
get { return Marshal.SizeOf<INPUT>(); }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,12 +14,11 @@ namespace ManagedCommon
|
||||
{
|
||||
public static class RunnerHelper
|
||||
{
|
||||
public static void WaitForPowerToysRunner(int powerToysPID, Action act)
|
||||
public static void WaitForPowerToysRunner(int powerToysPID, Action act, [System.Runtime.CompilerServices.CallerMemberName] string memberName = "")
|
||||
{
|
||||
var stackTrace = new StackTrace();
|
||||
var assembly = Assembly.GetCallingAssembly().GetName();
|
||||
var callingMethod = stackTrace.GetFrame(1).GetMethod().Name;
|
||||
PowerToysTelemetry.Log.WriteEvent(new DebugEvent() { Message = $"[{assembly}][{callingMethod}]WaitForPowerToysRunner waiting for Event powerToysPID={powerToysPID}" });
|
||||
PowerToysTelemetry.Log.WriteEvent(new DebugEvent() { Message = $"[{assembly}][{memberName}]WaitForPowerToysRunner waiting for Event powerToysPID={powerToysPID}" });
|
||||
Task.Run(() =>
|
||||
{
|
||||
const uint INFINITE = 0xFFFFFFFF;
|
||||
@@ -29,7 +28,7 @@ namespace ManagedCommon
|
||||
IntPtr powerToysProcHandle = NativeMethods.OpenProcess(SYNCHRONIZE, false, powerToysPID);
|
||||
if (NativeMethods.WaitForSingleObject(powerToysProcHandle, INFINITE) == WAIT_OBJECT_0)
|
||||
{
|
||||
PowerToysTelemetry.Log.WriteEvent(new DebugEvent() { Message = $"[{assembly}][{callingMethod}]WaitForPowerToysRunner Event Notified powerToysPID={powerToysPID}" });
|
||||
PowerToysTelemetry.Log.WriteEvent(new DebugEvent() { Message = $"[{assembly}][{memberName}]WaitForPowerToysRunner Event Notified powerToysPID={powerToysPID}" });
|
||||
act.Invoke();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
using static ManagedCommon.LanguageHelper;
|
||||
|
||||
namespace ManagedCommon.Serialization;
|
||||
|
||||
[JsonSerializable(typeof(OutGoingLanguageSettings))]
|
||||
internal sealed partial class SourceGenerationContext : JsonSerializerContext
|
||||
{
|
||||
}
|
||||
@@ -14,7 +14,7 @@ namespace ManagedCommon
|
||||
/// <param name="sender">Sender ThemeListener</param>
|
||||
public delegate void ThemeChangedEvent(ThemeListener sender);
|
||||
|
||||
public class ThemeListener : IDisposable
|
||||
public partial class ThemeListener : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the App Theme.
|
||||
|
||||
22
src/common/UITestAutomation/Element/Button.cs
Normal file
22
src/common/UITestAutomation/Element/Button.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
// 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.
|
||||
|
||||
namespace Microsoft.PowerToys.UITest
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a button in the UI test environment.
|
||||
/// </summary>
|
||||
public class Button : Element
|
||||
{
|
||||
private static readonly string ExpectedControlType = "ControlType.Button";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Button"/> class.
|
||||
/// </summary>
|
||||
public Button()
|
||||
{
|
||||
this.TargetControlType = Button.ExpectedControlType;
|
||||
}
|
||||
}
|
||||
}
|
||||
75
src/common/UITestAutomation/Element/By.cs
Normal file
75
src/common/UITestAutomation/Element/By.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
// 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 static OpenQA.Selenium.By;
|
||||
|
||||
namespace Microsoft.PowerToys.UITest
|
||||
{
|
||||
/// <summary>
|
||||
/// This class represents a By selector.
|
||||
/// </summary>
|
||||
public class By
|
||||
{
|
||||
private readonly OpenQA.Selenium.By by;
|
||||
|
||||
private By(OpenQA.Selenium.By by)
|
||||
{
|
||||
this.by = by;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
// override ToString to return detailed debugging content provided by OpenQA.Selenium.By
|
||||
return this.by.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a By object using the name attribute.
|
||||
/// </summary>
|
||||
/// <param name="name">The name attribute to search for.</param>
|
||||
/// <returns>A By object.</returns>
|
||||
public static By Name(string name) => new By(OpenQA.Selenium.By.Name(name));
|
||||
|
||||
/// <summary>
|
||||
/// Creates a By object using the ID attribute.
|
||||
/// </summary>
|
||||
/// <param name="id">The ID attribute to search for.</param>
|
||||
/// <returns>A By object.</returns>
|
||||
public static By Id(string id) => new By(OpenQA.Selenium.By.Id(id));
|
||||
|
||||
/// <summary>
|
||||
/// Creates a By object using the XPath expression.
|
||||
/// </summary>
|
||||
/// <param name="xpath">The XPath expression to search for.</param>
|
||||
/// <returns>A By object.</returns>
|
||||
public static By XPath(string xpath) => new By(OpenQA.Selenium.By.XPath(xpath));
|
||||
|
||||
/// <summary>
|
||||
/// Creates a By object using the CSS selector.
|
||||
/// </summary>
|
||||
/// <param name="cssSelector">The CSS selector to search for.</param>
|
||||
/// <returns>A By object.</returns>
|
||||
public static By CssSelector(string cssSelector) => new By(OpenQA.Selenium.By.CssSelector(cssSelector));
|
||||
|
||||
/// <summary>
|
||||
/// Creates a By object using the link text.
|
||||
/// </summary>
|
||||
/// <param name="linkText">The link text to search for.</param>
|
||||
/// <returns>A By object.</returns>
|
||||
public static By LinkText(string linkText) => new By(OpenQA.Selenium.By.LinkText(linkText));
|
||||
|
||||
/// <summary>
|
||||
/// Creates a By object using the tag name.
|
||||
/// </summary>
|
||||
/// <param name="tagName">The tag name to search for.</param>
|
||||
/// <returns>A By object.</returns>
|
||||
public static By TagName(string tagName) => new By(OpenQA.Selenium.By.TagName(tagName));
|
||||
|
||||
/// <summary>
|
||||
/// Converts the By object to an OpenQA.Selenium.By object.
|
||||
/// </summary>
|
||||
/// <returns>An OpenQA.Selenium.By object.</returns>
|
||||
internal OpenQA.Selenium.By ToSeleniumBy() => by;
|
||||
}
|
||||
}
|
||||
295
src/common/UITestAutomation/Element/Element.cs
Normal file
295
src/common/UITestAutomation/Element/Element.cs
Normal file
@@ -0,0 +1,295 @@
|
||||
// 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.Collections.ObjectModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using OpenQA.Selenium.Appium;
|
||||
using OpenQA.Selenium.Appium.Windows;
|
||||
using OpenQA.Selenium.Interactions;
|
||||
|
||||
[assembly: InternalsVisibleTo("Session")]
|
||||
|
||||
namespace Microsoft.PowerToys.UITest
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a basic UI element in the application.
|
||||
/// </summary>
|
||||
public class Element
|
||||
{
|
||||
private WindowsElement? windowsElement;
|
||||
|
||||
private WindowsDriver<WindowsElement>? driver;
|
||||
|
||||
protected string? TargetControlType { get; set; }
|
||||
|
||||
internal bool IsMatchingTarget()
|
||||
{
|
||||
var ct = this.ControlType;
|
||||
return string.IsNullOrEmpty(this.TargetControlType) || this.TargetControlType == this.ControlType;
|
||||
}
|
||||
|
||||
internal void SetWindowsElement(WindowsElement windowsElement) => this.windowsElement = windowsElement;
|
||||
|
||||
internal void SetSession(WindowsDriver<WindowsElement> driver) => this.driver = driver;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the UI element.
|
||||
/// </summary>
|
||||
public string Name
|
||||
{
|
||||
get { return GetAttribute("Name"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the text of the UI element.
|
||||
/// </summary>
|
||||
public string Text
|
||||
{
|
||||
get { return this.windowsElement?.Text ?? string.Empty; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the UI element is Enabled or not.
|
||||
/// </summary>
|
||||
public bool Enabled
|
||||
{
|
||||
get { return this.windowsElement?.Enabled ?? false; }
|
||||
}
|
||||
|
||||
public bool Selected
|
||||
{
|
||||
get { return this.windowsElement?.Selected ?? false; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the AutomationID of the UI element.
|
||||
/// </summary>
|
||||
public string AutomationId
|
||||
{
|
||||
get { return GetAttribute("AutomationId"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the class name of the UI element.
|
||||
/// </summary>
|
||||
public string ClassName
|
||||
{
|
||||
get { return GetAttribute("ClassName"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the help text of the UI element.
|
||||
/// </summary>
|
||||
public string HelpText
|
||||
{
|
||||
get { return GetAttribute("HelpText"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the control type of the UI element.
|
||||
/// </summary>
|
||||
public string ControlType
|
||||
{
|
||||
get { return GetAttribute("ControlType"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Click the UI element.
|
||||
/// </summary>
|
||||
/// <param name="rightClick">If true, performs a right-click; otherwise, performs a left-click. Default value is false</param>
|
||||
public virtual void Click(bool rightClick = false)
|
||||
{
|
||||
PerformAction((actions, windowElement) =>
|
||||
{
|
||||
actions.MoveToElement(windowElement);
|
||||
|
||||
// Move 2by2 offset to make click more stable instead of click on the border of the element
|
||||
actions.MoveByOffset(2, 2);
|
||||
|
||||
if (rightClick)
|
||||
{
|
||||
actions.ContextClick();
|
||||
}
|
||||
else
|
||||
{
|
||||
actions.Click();
|
||||
}
|
||||
|
||||
actions.Build().Perform();
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Double Click the UI element.
|
||||
/// </summary>
|
||||
public virtual void DoubleClick()
|
||||
{
|
||||
PerformAction((actions, windowElement) =>
|
||||
{
|
||||
actions.MoveToElement(windowElement);
|
||||
|
||||
// Move 2by2 offset to make click more stable instead of click on the border of the element
|
||||
actions.MoveByOffset(2, 2);
|
||||
|
||||
actions.DoubleClick();
|
||||
actions.Build().Perform();
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the attribute value of the UI element.
|
||||
/// </summary>
|
||||
/// <param name="attributeName">The name of the attribute to get.</param>
|
||||
/// <returns>The value of the attribute.</returns>
|
||||
public string GetAttribute(string attributeName)
|
||||
{
|
||||
Assert.IsNotNull(this.windowsElement, $"WindowsElement is null in method GetAttribute with parameter: attributeName = {attributeName}");
|
||||
var attributeValue = this.windowsElement.GetAttribute(attributeName);
|
||||
return attributeValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds an element by the selector.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The class type of the element to find.</typeparam>
|
||||
/// <param name="by">The selector to use for finding the element.</param>
|
||||
/// <param name="timeoutMS">The timeout in milliseconds.</param>
|
||||
/// <returns>The found element.</returns>
|
||||
public T Find<T>(By by, int timeoutMS = 3000)
|
||||
where T : Element, new()
|
||||
{
|
||||
Assert.IsNotNull(this.windowsElement, $"WindowsElement is null in method Find<{typeof(T).Name}> with parameters: by = {by}, timeoutMS = {timeoutMS}");
|
||||
|
||||
// leverage findAll to filter out mismatched elements
|
||||
var collection = this.FindAll<T>(by, timeoutMS);
|
||||
|
||||
Assert.IsTrue(collection.Count > 0, $"Element not found using selector: {by}");
|
||||
|
||||
return collection[0];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds an element by the selector.
|
||||
/// Shortcut for this.Find<T>(By.Name(name), timeoutMS)
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The class type of the element to find.</typeparam>
|
||||
/// <param name="name">The name for finding the element.</param>
|
||||
/// <param name="timeoutMS">The timeout in milliseconds.</param>
|
||||
/// <returns>The found element.</returns>
|
||||
public T Find<T>(string name, int timeoutMS = 3000)
|
||||
where T : Element, new()
|
||||
{
|
||||
return this.Find<T>(By.Name(name), timeoutMS);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds an element by the selector.
|
||||
/// Shortcut for this.Find<Element>(by, timeoutMS)
|
||||
/// </summary>
|
||||
/// <param name="by">The selector to use for finding the element.</param>
|
||||
/// <param name="timeoutMS">The timeout in milliseconds.</param>
|
||||
/// <returns>The found element.</returns>
|
||||
public Element Find(By by, int timeoutMS = 3000)
|
||||
{
|
||||
return this.Find<Element>(by, timeoutMS);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds an element by the selector.
|
||||
/// Shortcut for this.Find<Element>(By.Name(name), timeoutMS)
|
||||
/// </summary>
|
||||
/// <param name="name">The name for finding the element.</param>
|
||||
/// <param name="timeoutMS">The timeout in milliseconds.</param>
|
||||
/// <returns>The found element.</returns>
|
||||
public Element Find(string name, int timeoutMS = 3000)
|
||||
{
|
||||
return this.Find<Element>(By.Name(name), timeoutMS);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds all elements by the selector.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The class type of the elements to find.</typeparam>
|
||||
/// <param name="by">The selector to use for finding the elements.</param>
|
||||
/// <param name="timeoutMS">The timeout in milliseconds.</param>
|
||||
/// <returns>A read-only collection of the found elements.</returns>
|
||||
public ReadOnlyCollection<T> FindAll<T>(By by, int timeoutMS = 3000)
|
||||
where T : Element, new()
|
||||
{
|
||||
Assert.IsNotNull(this.windowsElement, $"WindowsElement is null in method FindAll<{typeof(T).Name}> with parameters: by = {by}, timeoutMS = {timeoutMS}");
|
||||
var foundElements = FindHelper.FindAll<T, AppiumWebElement>(
|
||||
() =>
|
||||
{
|
||||
var elements = this.windowsElement.FindElements(by.ToSeleniumBy());
|
||||
return elements;
|
||||
},
|
||||
this.driver,
|
||||
timeoutMS);
|
||||
|
||||
return foundElements ?? new ReadOnlyCollection<T>([]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds all elements by the selector.
|
||||
/// Shortcut for this.FindAll<T>(By.Name(name), timeoutMS)
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The class type of the elements to find.</typeparam>
|
||||
/// <param name="name">The name for finding the element.</param>
|
||||
/// <param name="timeoutMS">The timeout in milliseconds.</param>
|
||||
/// <returns>A read-only collection of the found elements.</returns>
|
||||
public ReadOnlyCollection<T> FindAll<T>(string name, int timeoutMS = 3000)
|
||||
where T : Element, new()
|
||||
{
|
||||
return this.FindAll<T>(By.Name(name), timeoutMS);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds all elements by the selector.
|
||||
/// Shortcut for this.FindAll<Element>(by, timeoutMS)
|
||||
/// </summary>
|
||||
/// <param name="by">The selector to use for finding the elements.</param>
|
||||
/// <param name="timeoutMS">The timeout in milliseconds.</param>
|
||||
/// <returns>A read-only collection of the found elements.</returns>
|
||||
public ReadOnlyCollection<Element> FindAll(By by, int timeoutMS = 3000)
|
||||
{
|
||||
return this.FindAll<Element>(by, timeoutMS);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds all elements by the selector.
|
||||
/// Shortcut for this.FindAll<Element>(By.Name(name), timeoutMS)
|
||||
/// </summary>
|
||||
/// <param name="name">The name for finding the element.</param>
|
||||
/// <param name="timeoutMS">The timeout in milliseconds.</param>
|
||||
/// <returns>A read-only collection of the found elements.</returns>
|
||||
public ReadOnlyCollection<Element> FindAll(string name, int timeoutMS = 3000)
|
||||
{
|
||||
return this.FindAll<Element>(By.Name(name), timeoutMS);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simulates a manual operation on the element.
|
||||
/// </summary>
|
||||
/// <param name="action">The action to perform on the element.</param>
|
||||
/// <param name="msPreAction">The number of milliseconds to wait before the action. Default value is 500 ms</param>
|
||||
/// <param name="msPostAction">The number of milliseconds to wait after the action. Default value is 500 ms</param>
|
||||
protected void PerformAction(Action<Actions, WindowsElement> action, int msPreAction = 500, int msPostAction = 500)
|
||||
{
|
||||
if (msPreAction > 0)
|
||||
{
|
||||
Task.Delay(msPreAction).Wait();
|
||||
}
|
||||
|
||||
var windowElement = this.windowsElement!;
|
||||
Actions actions = new Actions(this.driver);
|
||||
action(actions, windowElement);
|
||||
|
||||
if (msPostAction > 0)
|
||||
{
|
||||
Task.Delay(msPostAction).Wait();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
23
src/common/UITestAutomation/Element/HyperlinkButton.cs
Normal file
23
src/common/UITestAutomation/Element/HyperlinkButton.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
// 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.
|
||||
|
||||
namespace Microsoft.PowerToys.UITest
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a HyperLinkButton in the UI test environment.
|
||||
/// HyperLinkButton represents a button control that functions as a hyperlink.
|
||||
/// </summary>
|
||||
public class HyperlinkButton : Button
|
||||
{
|
||||
private static readonly string ExpectedControlType = "ControlType.HyperLink";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HyperlinkButton"/> class.
|
||||
/// </summary>
|
||||
public HyperlinkButton()
|
||||
{
|
||||
this.TargetControlType = HyperlinkButton.ExpectedControlType;
|
||||
}
|
||||
}
|
||||
}
|
||||
59
src/common/UITestAutomation/Element/NavigationViewItem.cs
Normal file
59
src/common/UITestAutomation/Element/NavigationViewItem.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.PowerToys.UITest
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a NavigationViewItem in the UI test environment.
|
||||
/// NavigationViewItem represents the container for an item in a NavigationView control.
|
||||
/// </summary>
|
||||
public class NavigationViewItem : Element
|
||||
{
|
||||
private static readonly string ExpectedControlType = "ControlType.ListItem";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="NavigationViewItem"/> class.
|
||||
/// </summary>
|
||||
public NavigationViewItem()
|
||||
{
|
||||
this.TargetControlType = NavigationViewItem.ExpectedControlType;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Click the ListItem element.
|
||||
/// </summary>
|
||||
/// <param name="rightClick">If true, performs a right-click; otherwise, performs a left-click. Default value is false</param>
|
||||
public override void Click(bool rightClick = false)
|
||||
{
|
||||
PerformAction((actions, windowElement) =>
|
||||
{
|
||||
actions.MoveToElement(windowElement, 10, 10);
|
||||
|
||||
if (rightClick)
|
||||
{
|
||||
actions.ContextClick();
|
||||
}
|
||||
else
|
||||
{
|
||||
actions.Click();
|
||||
}
|
||||
|
||||
actions.Build().Perform();
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Double Click the ListItem element.
|
||||
/// </summary>
|
||||
public override void DoubleClick()
|
||||
{
|
||||
PerformAction((actions, windowElement) =>
|
||||
{
|
||||
actions.MoveToElement(windowElement, 10, 10);
|
||||
actions.DoubleClick();
|
||||
actions.Build().Perform();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
23
src/common/UITestAutomation/Element/TextBlock.cs
Normal file
23
src/common/UITestAutomation/Element/TextBlock.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
// 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.
|
||||
|
||||
namespace Microsoft.PowerToys.UITest
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a TextBlock in the UI test environment.
|
||||
/// TextBlock provides a lightweight control for displaying small amounts of flow content.
|
||||
/// </summary>
|
||||
public class TextBlock : Element
|
||||
{
|
||||
private static readonly string ExpectedControlType = "ControlType.Text";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TextBlock"/> class.
|
||||
/// </summary>
|
||||
public TextBlock()
|
||||
{
|
||||
this.TargetControlType = TextBlock.ExpectedControlType;
|
||||
}
|
||||
}
|
||||
}
|
||||
51
src/common/UITestAutomation/Element/TextBox.cs
Normal file
51
src/common/UITestAutomation/Element/TextBox.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
// 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 OpenQA.Selenium;
|
||||
|
||||
namespace Microsoft.PowerToys.UITest
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a TextBox in the UI test environment.
|
||||
/// TextBox represents a control that can be used to display and edit plain text (single or multi-line).
|
||||
/// </summary>
|
||||
public class TextBox : Element
|
||||
{
|
||||
private static readonly string ExpectedControlType = "ControlType.Edit";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TextBox"/> class.
|
||||
/// </summary>
|
||||
public TextBox()
|
||||
{
|
||||
this.TargetControlType = TextBox.ExpectedControlType;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the text of the textbox.
|
||||
/// </summary>
|
||||
/// <param name="value">The text to set.</param>
|
||||
/// <param name="clearText">A value indicating whether to clear the text before setting it. Default value is true</param>
|
||||
/// <returns>The current TextBox instance.</returns>
|
||||
public TextBox SetText(string value, bool clearText = true)
|
||||
{
|
||||
if (clearText)
|
||||
{
|
||||
PerformAction((actions, windowElement) =>
|
||||
{
|
||||
// select all text and delete it
|
||||
windowElement.SendKeys(Keys.Control + "a");
|
||||
windowElement.SendKeys(Keys.Delete);
|
||||
});
|
||||
}
|
||||
|
||||
PerformAction((actions, windowElement) =>
|
||||
{
|
||||
windowElement.SendKeys(value);
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
41
src/common/UITestAutomation/Element/ToggleSwitch.cs
Normal file
41
src/common/UITestAutomation/Element/ToggleSwitch.cs
Normal 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 Newtonsoft.Json.Linq;
|
||||
|
||||
namespace Microsoft.PowerToys.UITest
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a ToggleSwitch in the UI test environment.
|
||||
/// </summary>
|
||||
public class ToggleSwitch : Button
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the ToggleSwitch is on.
|
||||
/// </summary>
|
||||
public bool IsOn
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.Selected;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the ToggleSwitch to the specified value.
|
||||
/// </summary>
|
||||
/// <param name="value">A value indicating whether the ToggleSwitch should be active. Default is true</param>
|
||||
/// <returns>The current ToggleSwitch instance.</returns>
|
||||
public ToggleSwitch Toggle(bool value = true)
|
||||
{
|
||||
if (this.IsOn != value)
|
||||
{
|
||||
// Toggle the switch
|
||||
this.Click();
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
85
src/common/UITestAutomation/Element/Window.cs
Normal file
85
src/common/UITestAutomation/Element/Window.cs
Normal file
@@ -0,0 +1,85 @@
|
||||
// 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.
|
||||
|
||||
namespace Microsoft.PowerToys.UITest
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a window in the UI test environment.
|
||||
/// </summary>
|
||||
public class Window : Element
|
||||
{
|
||||
/// <summary>
|
||||
/// Maximizes the window.
|
||||
/// </summary>
|
||||
/// <param name="byClickButton">If true, clicks the Maximize button; otherwise, sets the window state.</param>
|
||||
/// <returns>The current Window instance.</returns>
|
||||
public Window Maximize(bool byClickButton = true)
|
||||
{
|
||||
if (byClickButton)
|
||||
{
|
||||
Find<Button>("Maximize").Click();
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO: Implement maximizing the window using an alternative method
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restores the window.
|
||||
/// </summary>
|
||||
/// <param name="byClickButton">If true, clicks the Restore button; otherwise, sets the window state.</param>
|
||||
/// <returns>The current Window instance.</returns>
|
||||
public Window Restore(bool byClickButton = true)
|
||||
{
|
||||
if (byClickButton)
|
||||
{
|
||||
Find<Button>("Restore").Click();
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO: Implement restoring the window using an alternative method
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Minimizes the window.
|
||||
/// </summary>
|
||||
/// <param name="byClickButton">If true, clicks the Minimize button; otherwise, sets the window state.</param>
|
||||
/// <returns>The current Window instance.</returns>
|
||||
public Window Minimize(bool byClickButton = true)
|
||||
{
|
||||
if (byClickButton)
|
||||
{
|
||||
Find<Button>("Minimize").Click();
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO: Implement minimizing the window using an alternative method
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes the window.
|
||||
/// </summary>
|
||||
/// <param name="byClickButton">If true, clicks the Close button; otherwise, closes the window using an alternative method.</param>
|
||||
public void Close(bool byClickButton = true)
|
||||
{
|
||||
if (byClickButton)
|
||||
{
|
||||
Find<Button>("Close").Click();
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO: Implement closing the window using an alternative method
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
51
src/common/UITestAutomation/FindHelper.cs
Normal file
51
src/common/UITestAutomation/FindHelper.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
// 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.Collections.ObjectModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using OpenQA.Selenium.Appium.Windows;
|
||||
|
||||
[assembly: InternalsVisibleTo("Element")]
|
||||
[assembly: InternalsVisibleTo("Session")]
|
||||
|
||||
namespace Microsoft.PowerToys.UITest
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper class for finding elements.
|
||||
/// </summary>
|
||||
internal static class FindHelper
|
||||
{
|
||||
public static ReadOnlyCollection<T>? FindAll<T, TW>(Func<ReadOnlyCollection<TW>> findElementsFunc, WindowsDriver<WindowsElement>? driver, int timeoutMS)
|
||||
where T : Element, new()
|
||||
{
|
||||
var items = findElementsFunc();
|
||||
var res = items.Select(item =>
|
||||
{
|
||||
var element = item as WindowsElement;
|
||||
return NewElement<T>(element, driver, timeoutMS);
|
||||
}).Where(item => item.IsMatchingTarget()).ToList();
|
||||
|
||||
return new ReadOnlyCollection<T>(res);
|
||||
}
|
||||
|
||||
public static T NewElement<T>(WindowsElement? element, WindowsDriver<WindowsElement>? driver, int timeoutMS)
|
||||
where T : Element, new()
|
||||
{
|
||||
Assert.IsNotNull(driver, $"New Element {typeof(T).Name} error: driver is null.");
|
||||
Assert.IsNotNull(element, $"New Element {typeof(T).Name} error: element is null.");
|
||||
|
||||
T newElement = new T();
|
||||
if (timeoutMS > 0)
|
||||
{
|
||||
// Only set timeout if it is positive value
|
||||
driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromMilliseconds(timeoutMS);
|
||||
}
|
||||
|
||||
newElement.SetSession(driver);
|
||||
newElement.SetWindowsElement(element);
|
||||
return newElement;
|
||||
}
|
||||
}
|
||||
}
|
||||
72
src/common/UITestAutomation/ModuleConfigData.cs
Normal file
72
src/common/UITestAutomation/ModuleConfigData.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
// 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.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("UITestBase")]
|
||||
[assembly: InternalsVisibleTo("Session")]
|
||||
|
||||
namespace Microsoft.PowerToys.UITest
|
||||
{
|
||||
/// <summary>
|
||||
/// This file manages the configuration of modules for UI tests.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// How to add a new module:
|
||||
/// 1. Define the new module in the PowerToysModule enum.
|
||||
/// 2. Add the exe window name to the ModuleWindowName dictionary in the ModuleConfigData constructor.
|
||||
/// 3. Add the exe path to the ModulePath dictionary in the ModuleConfigData constructor.
|
||||
/// </remarks>
|
||||
|
||||
/// <summary>
|
||||
/// Represents the modules in PowerToys.
|
||||
/// </summary>
|
||||
public enum PowerToysModule
|
||||
{
|
||||
PowerToysSettings,
|
||||
FancyZone,
|
||||
Hosts,
|
||||
}
|
||||
|
||||
internal class ModuleConfigData
|
||||
{
|
||||
private Dictionary<PowerToysModule, string> ModulePath { get; }
|
||||
|
||||
// Singleton instance of ModuleConfigData.
|
||||
private static readonly Lazy<ModuleConfigData> SingletonInstance = new Lazy<ModuleConfigData>(() => new ModuleConfigData());
|
||||
|
||||
public static ModuleConfigData Instance => SingletonInstance.Value;
|
||||
|
||||
public const string WindowsApplicationDriverUrl = "http://127.0.0.1:4723";
|
||||
|
||||
public Dictionary<PowerToysModule, string> ModuleWindowName { get; }
|
||||
|
||||
private ModuleConfigData()
|
||||
{
|
||||
// The exe window name for each module.
|
||||
ModuleWindowName = new Dictionary<PowerToysModule, string>
|
||||
{
|
||||
[PowerToysModule.PowerToysSettings] = "PowerToys Settings",
|
||||
[PowerToysModule.FancyZone] = "FancyZones Layout",
|
||||
[PowerToysModule.Hosts] = "Hosts File Editor",
|
||||
};
|
||||
|
||||
// Exe start path for the module if it exists.
|
||||
ModulePath = new Dictionary<PowerToysModule, string>
|
||||
{
|
||||
[PowerToysModule.PowerToysSettings] = @"\..\..\..\WinUI3Apps\PowerToys.Settings.exe",
|
||||
[PowerToysModule.FancyZone] = @"\..\..\..\PowerToys.FancyZonesEditor.exe",
|
||||
[PowerToysModule.Hosts] = @"\..\..\..\WinUI3Apps\PowerToys.Hosts.exe",
|
||||
};
|
||||
}
|
||||
|
||||
public string GetModulePath(PowerToysModule scope) => ModulePath[scope];
|
||||
|
||||
public string GetWindowsApplicationDriverUrl() => WindowsApplicationDriverUrl;
|
||||
|
||||
public string GetModuleWindowName(PowerToysModule scope) => ModuleWindowName[scope];
|
||||
}
|
||||
}
|
||||
193
src/common/UITestAutomation/Session.cs
Normal file
193
src/common/UITestAutomation/Session.cs
Normal file
@@ -0,0 +1,193 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Xml.Linq;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using OpenQA.Selenium.Appium;
|
||||
using OpenQA.Selenium.Appium.Windows;
|
||||
|
||||
namespace Microsoft.PowerToys.UITest
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides interfaces for interacting with UI elements.
|
||||
/// </summary>
|
||||
public class Session
|
||||
{
|
||||
private WindowsDriver<WindowsElement> Root { get; set; }
|
||||
|
||||
private WindowsDriver<WindowsElement> WindowsDriver { get; set; }
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool SetForegroundWindow(nint hWnd);
|
||||
|
||||
public Session(WindowsDriver<WindowsElement> root, WindowsDriver<WindowsElement> windowsDriver)
|
||||
{
|
||||
this.Root = root;
|
||||
this.WindowsDriver = windowsDriver;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds an element by selector.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The class of the element, should be Element or its derived class.</typeparam>
|
||||
/// <param name="by">The selector to find the element.</param>
|
||||
/// <param name="timeoutMS">The timeout in milliseconds (default is 3000).</param>
|
||||
/// <returns>The found element.</returns>
|
||||
public T Find<T>(By by, int timeoutMS = 3000)
|
||||
where T : Element, new()
|
||||
{
|
||||
Assert.IsNotNull(this.WindowsDriver, $"WindowsElement is null in method Find<{typeof(T).Name}> with parameters: by = {by}, timeoutMS = {timeoutMS}");
|
||||
|
||||
// leverage findAll to filter out mismatched elements
|
||||
var collection = this.FindAll<T>(by, timeoutMS);
|
||||
|
||||
Assert.IsTrue(collection.Count > 0, $"Element not found using selector: {by}");
|
||||
|
||||
return collection[0];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shortcut for this.Find<T>(By.Name(name), timeoutMS)
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The class of the element, should be Element or its derived class.</typeparam>
|
||||
/// <param name="name">The name of the element.</param>
|
||||
/// <param name="timeoutMS">The timeout in milliseconds (default is 3000).</param>
|
||||
/// <returns>The found element.</returns>
|
||||
public T Find<T>(string name, int timeoutMS = 3000)
|
||||
where T : Element, new()
|
||||
{
|
||||
return this.Find<T>(By.Name(name), timeoutMS);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shortcut for this.Find<Element>(by, timeoutMS)
|
||||
/// </summary>
|
||||
/// <param name="by">The selector to find the element.</param>
|
||||
/// <param name="timeoutMS">The timeout in milliseconds (default is 3000).</param>
|
||||
/// <returns>The found element.</returns>
|
||||
public Element Find(By by, int timeoutMS = 3000)
|
||||
{
|
||||
return this.Find<Element>(by, timeoutMS);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shortcut for this.Find<Element>(By.Name(name), timeoutMS)
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the element.</param>
|
||||
/// <param name="timeoutMS">The timeout in milliseconds (default is 3000).</param>
|
||||
/// <returns>The found element.</returns>
|
||||
public Element Find(string name, int timeoutMS = 3000)
|
||||
{
|
||||
return this.Find<Element>(By.Name(name), timeoutMS);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds all elements by selector.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The class of the elements, should be Element or its derived class.</typeparam>
|
||||
/// <param name="by">The selector to find the elements.</param>
|
||||
/// <param name="timeoutMS">The timeout in milliseconds (default is 3000).</param>
|
||||
/// <returns>A read-only collection of the found elements.</returns>
|
||||
public ReadOnlyCollection<T> FindAll<T>(By by, int timeoutMS = 3000)
|
||||
where T : Element, new()
|
||||
{
|
||||
Assert.IsNotNull(this.WindowsDriver, $"WindowsElement is null in method FindAll<{typeof(T).Name}> with parameters: by = {by}, timeoutMS = {timeoutMS}");
|
||||
var foundElements = FindHelper.FindAll<T, WindowsElement>(
|
||||
() =>
|
||||
{
|
||||
var elements = this.WindowsDriver.FindElements(by.ToSeleniumBy());
|
||||
return elements;
|
||||
},
|
||||
this.WindowsDriver,
|
||||
timeoutMS);
|
||||
|
||||
return foundElements ?? new ReadOnlyCollection<T>([]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds all elements by selector.
|
||||
/// Shortcut for this.FindAll<T>(By.Name(name), timeoutMS)
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The class of the elements, should be Element or its derived class.</typeparam>
|
||||
/// <param name="name">The name to find the elements.</param>
|
||||
/// <param name="timeoutMS">The timeout in milliseconds (default is 3000).</param>
|
||||
/// <returns>A read-only collection of the found elements.</returns>
|
||||
public ReadOnlyCollection<T> FindAll<T>(string name, int timeoutMS = 3000)
|
||||
where T : Element, new()
|
||||
{
|
||||
return this.FindAll<T>(By.Name(name), timeoutMS);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds all elements by selector.
|
||||
/// Shortcut for this.FindAll<Element>(by, timeoutMS)
|
||||
/// </summary>
|
||||
/// <param name="by">The selector to find the elements.</param>
|
||||
/// <param name="timeoutMS">The timeout in milliseconds (default is 3000).</param>
|
||||
/// <returns>A read-only collection of the found elements.</returns>
|
||||
public ReadOnlyCollection<Element> FindAll(By by, int timeoutMS = 3000)
|
||||
{
|
||||
return this.FindAll<Element>(by, timeoutMS);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds all elements by selector.
|
||||
/// Shortcut for this.FindAll<Element>(By.Name(name), timeoutMS)
|
||||
/// </summary>
|
||||
/// <param name="name">The name to find the elements.</param>
|
||||
/// <param name="timeoutMS">The timeout in milliseconds (default is 3000).</param>
|
||||
/// <returns>A read-only collection of the found elements.</returns>
|
||||
public ReadOnlyCollection<Element> FindAll(string name, int timeoutMS = 3000)
|
||||
{
|
||||
return this.FindAll<Element>(By.Name(name), timeoutMS);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attaches to an existing PowerToys module.
|
||||
/// </summary>
|
||||
/// <param name="module">The PowerToys module to attach to.</param>
|
||||
/// <returns>The attached session.</returns>
|
||||
public Session Attach(PowerToysModule module)
|
||||
{
|
||||
string windowName = ModuleConfigData.Instance.GetModuleWindowName(module);
|
||||
return this.Attach(windowName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attaches to an existing exe by string window name.
|
||||
/// The session should be attached when a new app is started.
|
||||
/// </summary>
|
||||
/// <param name="windowName">The window name to attach to.</param>
|
||||
/// <returns>The attached session.</returns>
|
||||
public Session Attach(string windowName)
|
||||
{
|
||||
if (this.Root != null)
|
||||
{
|
||||
var window = this.Root.FindElementByName(windowName);
|
||||
Assert.IsNotNull(window, $"Failed to attach. Window '{windowName}' not found");
|
||||
|
||||
var windowHandle = new nint(int.Parse(window.GetAttribute("NativeWindowHandle")));
|
||||
SetForegroundWindow(windowHandle);
|
||||
var hexWindowHandle = windowHandle.ToString("x");
|
||||
var appCapabilities = new AppiumOptions();
|
||||
|
||||
appCapabilities.AddAdditionalCapability("appTopLevelWindow", hexWindowHandle);
|
||||
appCapabilities.AddAdditionalCapability("deviceName", "WindowsPC");
|
||||
this.WindowsDriver = new WindowsDriver<WindowsElement>(new Uri(ModuleConfigData.Instance.GetWindowsApplicationDriverUrl()), appCapabilities);
|
||||
Assert.IsNotNull(this.WindowsDriver, "Attach WindowsDriver is null");
|
||||
|
||||
// Set implicit timeout to make element search retry every 500 ms
|
||||
this.WindowsDriver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(3);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.IsNotNull(this.Root, $"Failed to attach to the window '{windowName}'. Root driver is null");
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
101
src/common/UITestAutomation/SessionHelper.cs
Normal file
101
src/common/UITestAutomation/SessionHelper.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
// 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;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Reflection;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using OpenQA.Selenium.Appium;
|
||||
using OpenQA.Selenium.Appium.Windows;
|
||||
|
||||
namespace Microsoft.PowerToys.UITest
|
||||
{
|
||||
/// <summary>
|
||||
/// Nested class for test initialization.
|
||||
/// </summary>
|
||||
internal class SessionHelper
|
||||
{
|
||||
// Default session path is PowerToys settings dashboard
|
||||
private readonly string sessionPath = ModuleConfigData.Instance.GetModulePath(PowerToysModule.PowerToysSettings);
|
||||
|
||||
private WindowsDriver<WindowsElement> Root { get; set; }
|
||||
|
||||
private WindowsDriver<WindowsElement>? Driver { get; set; }
|
||||
|
||||
private Process? appDriver;
|
||||
|
||||
public SessionHelper(PowerToysModule scope)
|
||||
{
|
||||
this.sessionPath = ModuleConfigData.Instance.GetModulePath(scope);
|
||||
|
||||
var winAppDriverProcessInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "C:\\Program Files (x86)\\Windows Application Driver\\WinAppDriver.exe",
|
||||
Verb = "runas",
|
||||
};
|
||||
|
||||
this.appDriver = Process.Start(winAppDriverProcessInfo);
|
||||
|
||||
var desktopCapabilities = new AppiumOptions();
|
||||
desktopCapabilities.AddAdditionalCapability("app", "Root");
|
||||
this.Root = new WindowsDriver<WindowsElement>(new Uri(ModuleConfigData.Instance.GetWindowsApplicationDriverUrl()), desktopCapabilities);
|
||||
|
||||
// Set default timeout to 5 seconds
|
||||
this.Root.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(5);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the test environment.
|
||||
/// </summary>
|
||||
/// <param name="scope">The PowerToys module to start.</param>
|
||||
[UnconditionalSuppressMessage("SingleFile", "IL3000:Avoid accessing Assembly file path when publishing as a single file", Justification = "<Pending>")]
|
||||
public SessionHelper Init()
|
||||
{
|
||||
string? path = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
|
||||
this.StartExe(path + this.sessionPath);
|
||||
|
||||
Assert.IsNotNull(this.Driver, $"Failed to initialize the test environment. Driver is null.");
|
||||
|
||||
// Set default timeout to 5 seconds
|
||||
this.Driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(5);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cleans up the test environment.
|
||||
/// </summary>
|
||||
public void Cleanup()
|
||||
{
|
||||
try
|
||||
{
|
||||
appDriver?.Kill();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Handle exceptions if needed
|
||||
Debug.WriteLine($"Exception during Cleanup: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts a new exe and takes control of it.
|
||||
/// </summary>
|
||||
/// <param name="appPath">The path to the application executable.</param>
|
||||
public void StartExe(string appPath)
|
||||
{
|
||||
var opts = new AppiumOptions();
|
||||
opts.AddAdditionalCapability("app", appPath);
|
||||
this.Driver = new WindowsDriver<WindowsElement>(new Uri(ModuleConfigData.Instance.GetWindowsApplicationDriverUrl()), opts);
|
||||
}
|
||||
|
||||
public WindowsDriver<WindowsElement> GetRoot() => this.Root;
|
||||
|
||||
public WindowsDriver<WindowsElement> GetDriver()
|
||||
{
|
||||
Assert.IsNotNull(this.Driver, $"Failed to get driver. Driver is null.");
|
||||
return this.Driver;
|
||||
}
|
||||
}
|
||||
}
|
||||
19
src/common/UITestAutomation/UITestAutomation.csproj
Normal file
19
src/common/UITestAutomation/UITestAutomation.csproj
Normal file
@@ -0,0 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<!-- Look at Directory.Build.props in root for common stuff as well -->
|
||||
<Import Project="..\..\Common.Dotnet.CsWinRT.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Library</OutputType>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<PublishAot>true</PublishAot>
|
||||
<InvariantGlobalization>true</InvariantGlobalization>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Appium.WebDriver" />
|
||||
<PackageReference Include="MSTest" />
|
||||
<PackageReference Include="System.IO.Abstractions" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
157
src/common/UITestAutomation/UITestBase.cs
Normal file
157
src/common/UITestAutomation/UITestBase.cs
Normal file
@@ -0,0 +1,157 @@
|
||||
// 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.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Reflection;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using OpenQA.Selenium.Appium;
|
||||
using OpenQA.Selenium.Appium.Windows;
|
||||
|
||||
namespace Microsoft.PowerToys.UITest
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class that should be inherited by all Test Classes.
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class UITestBase
|
||||
{
|
||||
public Session Session { get; set; }
|
||||
|
||||
private readonly SessionHelper sessionHelper;
|
||||
|
||||
private readonly PowerToysModule scope;
|
||||
|
||||
public UITestBase(PowerToysModule scope = PowerToysModule.PowerToysSettings)
|
||||
{
|
||||
this.scope = scope;
|
||||
this.sessionHelper = new SessionHelper(scope).Init();
|
||||
this.Session = new Session(this.sessionHelper.GetRoot(), this.sessionHelper.GetDriver());
|
||||
}
|
||||
|
||||
~UITestBase()
|
||||
{
|
||||
this.sessionHelper.Cleanup();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the test.
|
||||
/// </summary>
|
||||
[TestInitialize]
|
||||
public void TestInit()
|
||||
{
|
||||
if (this.scope == PowerToysModule.PowerToysSettings)
|
||||
{
|
||||
// close Debug warning dialog if any
|
||||
// Such debug warning dialog seems only appear in PowerToys Settings
|
||||
if (this.FindAll("DEBUG").Count > 0)
|
||||
{
|
||||
this.Find("DEBUG").Find<Button>("Close").Click();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds an element by selector.
|
||||
/// Shortcut for this.Session.Find<T>(by, timeoutMS)
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The class of the element, should be Element or its derived class.</typeparam>
|
||||
/// <param name="by">The selector to find the element.</param>
|
||||
/// <param name="timeoutMS">The timeout in milliseconds (default is 3000).</param>
|
||||
/// <returns>The found element.</returns>
|
||||
protected T Find<T>(By by, int timeoutMS = 3000)
|
||||
where T : Element, new()
|
||||
{
|
||||
return this.Session.Find<T>(by, timeoutMS);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shortcut for this.Session.Find<Element>(By.Name(name), timeoutMS)
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The class of the element, should be Element or its derived class.</typeparam>
|
||||
/// <param name="name">The name of the element.</param>
|
||||
/// <param name="timeoutMS">The timeout in milliseconds (default is 3000).</param>
|
||||
/// <returns>The found element.</returns>
|
||||
protected T Find<T>(string name, int timeoutMS = 3000)
|
||||
where T : Element, new()
|
||||
{
|
||||
return this.Session.Find<T>(By.Name(name), timeoutMS);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shortcut for this.Session.Find<Element>(by, timeoutMS)
|
||||
/// </summary>
|
||||
/// <param name="by">The selector to find the element.</param>
|
||||
/// <param name="timeoutMS">The timeout in milliseconds (default is 3000).</param>
|
||||
/// <returns>The found element.</returns>
|
||||
protected Element Find(By by, int timeoutMS = 3000)
|
||||
{
|
||||
return this.Session.Find(by, timeoutMS);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shortcut for this.Session.Find<Element>(By.Name(name), timeoutMS)
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the element.</param>
|
||||
/// <param name="timeoutMS">The timeout in milliseconds (default is 3000).</param>
|
||||
/// <returns>The found element.</returns>
|
||||
protected Element Find(string name, int timeoutMS = 3000)
|
||||
{
|
||||
return this.Session.Find(name, timeoutMS);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds all elements by selector.
|
||||
/// Shortcut for this.Session.FindAll<T>(by, timeoutMS)
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The class of the elements, should be Element or its derived class.</typeparam>
|
||||
/// <param name="by">The selector to find the elements.</param>
|
||||
/// <param name="timeoutMS">The timeout in milliseconds (default is 3000).</param>
|
||||
/// <returns>A read-only collection of the found elements.</returns>
|
||||
protected ReadOnlyCollection<T> FindAll<T>(By by, int timeoutMS = 3000)
|
||||
where T : Element, new()
|
||||
{
|
||||
return this.Session.FindAll<T>(by, timeoutMS);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds all elements by selector.
|
||||
/// Shortcut for this.Session.FindAll<Element>(By.Name(name), timeoutMS)
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The class of the elements, should be Element or its derived class.</typeparam>
|
||||
/// <param name="name">The name of the elements.</param>
|
||||
/// <param name="timeoutMS">The timeout in milliseconds (default is 3000).</param>
|
||||
/// <returns>A read-only collection of the found elements.</returns>
|
||||
protected ReadOnlyCollection<T> FindAll<T>(string name, int timeoutMS = 3000)
|
||||
where T : Element, new()
|
||||
{
|
||||
return this.Session.FindAll<T>(By.Name(name), timeoutMS);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds all elements by selector.
|
||||
/// Shortcut for this.Session.FindAll<Element>(by, timeoutMS)
|
||||
/// </summary>
|
||||
/// <param name="by">The selector to find the elements.</param>
|
||||
/// <param name="timeoutMS">The timeout in milliseconds (default is 3000).</param>
|
||||
/// <returns>A read-only collection of the found elements.</returns>
|
||||
protected ReadOnlyCollection<Element> FindAll(By by, int timeoutMS = 3000)
|
||||
{
|
||||
return this.Session.FindAll<Element>(by, timeoutMS);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds all elements by selector.
|
||||
/// Shortcut for this.Session.FindAll<Element>(By.Name(name), timeoutMS)
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the elements.</param>
|
||||
/// <param name="timeoutMS">The timeout in milliseconds (default is 3000).</param>
|
||||
/// <returns>A read-only collection of the found elements.</returns>
|
||||
protected ReadOnlyCollection<Element> FindAll(string name, int timeoutMS = 3000)
|
||||
{
|
||||
return this.Session.FindAll<Element>(By.Name(name), timeoutMS);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -82,12 +82,20 @@ namespace updating
|
||||
// prevent the warning that may show up depend on the value of the constants (#defines)
|
||||
#pragma warning(push)
|
||||
#pragma warning(disable : 4702)
|
||||
#if USE_STD_EXPECTED
|
||||
std::future<std::expected<github_version_info, std::wstring>> get_github_version_info_async(const bool prerelease)
|
||||
#else
|
||||
std::future<nonstd::expected<github_version_info, std::wstring>> get_github_version_info_async(const bool prerelease)
|
||||
#endif
|
||||
{
|
||||
// If the current version starts with 0.0.*, it means we're on a local build from a farm and shouldn't check for updates.
|
||||
if constexpr (VERSION_MAJOR == 0 && VERSION_MINOR == 0)
|
||||
{
|
||||
#if USE_STD_EXPECTED
|
||||
co_return std::unexpected(LOCAL_BUILD_ERROR);
|
||||
#else
|
||||
co_return nonstd::make_unexpected(LOCAL_BUILD_ERROR);
|
||||
#endif
|
||||
}
|
||||
|
||||
try
|
||||
@@ -139,7 +147,11 @@ namespace updating
|
||||
catch (...)
|
||||
{
|
||||
}
|
||||
#if USE_STD_EXPECTED
|
||||
co_return std::unexpected(NETWORK_ERROR);
|
||||
#else
|
||||
co_return nonstd::make_unexpected(NETWORK_ERROR);
|
||||
#endif
|
||||
}
|
||||
#pragma warning(pop)
|
||||
|
||||
|
||||
@@ -6,7 +6,14 @@
|
||||
#include <filesystem>
|
||||
#include <variant>
|
||||
#include <winrt/Windows.Foundation.h>
|
||||
//#if __MSVC_VERSION__ >= 1933 // MSVC begin to support std::unexpected in 19.33
|
||||
#if __has_include(<expected> ) // use the same way with excepted-lite to detect std::unexcepted, as using it as backup
|
||||
#include <expected>
|
||||
#define USE_STD_EXPECTED 1
|
||||
#else
|
||||
#include <expected.hpp>
|
||||
#define USE_STD_EXPECTED 0
|
||||
#endif
|
||||
|
||||
#include <common/version/helper.h>
|
||||
|
||||
@@ -27,7 +34,11 @@ namespace updating
|
||||
|
||||
std::future<std::optional<std::filesystem::path>> download_new_version(const new_version_download_info& new_version);
|
||||
std::filesystem::path get_pending_updates_path();
|
||||
#if USE_STD_EXPECTED
|
||||
std::future<std::expected<github_version_info, std::wstring>> get_github_version_info_async(const bool prerelease = false);
|
||||
#else
|
||||
std::future<nonstd::expected<github_version_info, std::wstring>> get_github_version_info_async(const bool prerelease = false);
|
||||
#endif
|
||||
void cleanup_updates();
|
||||
|
||||
// non-localized
|
||||
|
||||
@@ -73,12 +73,14 @@ namespace powertoys_gpo {
|
||||
// The registry value names for other PowerToys policies.
|
||||
const std::wstring POLICY_ALLOW_EXPERIMENTATION = L"AllowExperimentation";
|
||||
const std::wstring POLICY_ALLOW_DATA_DIAGNOSTICS = L"AllowDataDiagnostics";
|
||||
const std::wstring POLICY_CONFIGURE_RUN_AT_STARTUP = L"ConfigureRunAtStartup";
|
||||
const std::wstring POLICY_CONFIGURE_ENABLED_POWER_LAUNCHER_ALL_PLUGINS = L"PowerLauncherAllPluginsEnabledState";
|
||||
const std::wstring POLICY_ALLOW_ADVANCED_PASTE_ONLINE_AI_MODELS = L"AllowPowerToysAdvancedPasteOnlineAIModels";
|
||||
const std::wstring POLICY_MWB_CLIPBOARD_SHARING_ENABLED = L"MwbClipboardSharingEnabled";
|
||||
const std::wstring POLICY_MWB_FILE_TRANSFER_ENABLED = L"MwbFileTransferEnabled";
|
||||
const std::wstring POLICY_MWB_USE_ORIGINAL_USER_INTERFACE = L"MwbUseOriginalUserInterface";
|
||||
const std::wstring POLICY_MWB_DISALLOW_BLOCKING_SCREENSAVER = L"MwbDisallowBlockingScreensaver";
|
||||
const std::wstring POLICY_MWB_ALLOW_SERVICE_MODE = L"MwbAllowServiceMode";
|
||||
const std::wstring POLICY_MWB_SAME_SUBNET_ONLY = L"MwbSameSubnetOnly";
|
||||
const std::wstring POLICY_MWB_VALIDATE_REMOTE_IP = L"MwbValidateRemoteIp";
|
||||
const std::wstring POLICY_MWB_DISABLE_USER_DEFINED_IP_MAPPING_RULES = L"MwbDisableUserDefinedIpMappingRules";
|
||||
@@ -493,6 +495,11 @@ namespace powertoys_gpo {
|
||||
return getConfiguredValue(POLICY_ALLOW_DATA_DIAGNOSTICS);
|
||||
}
|
||||
|
||||
inline gpo_rule_configured_t getConfiguredRunAtStartupValue()
|
||||
{
|
||||
return getConfiguredValue(POLICY_CONFIGURE_RUN_AT_STARTUP);
|
||||
}
|
||||
|
||||
inline gpo_rule_configured_t getRunPluginEnabledValue(std::string pluginID)
|
||||
{
|
||||
if (pluginID == "" || pluginID == " ")
|
||||
@@ -558,6 +565,11 @@ namespace powertoys_gpo {
|
||||
return getConfiguredValue(POLICY_MWB_DISALLOW_BLOCKING_SCREENSAVER);
|
||||
}
|
||||
|
||||
inline gpo_rule_configured_t getConfiguredMwbAllowServiceModeValue()
|
||||
{
|
||||
return getConfiguredValue(POLICY_MWB_ALLOW_SERVICE_MODE);
|
||||
}
|
||||
|
||||
inline gpo_rule_configured_t getConfiguredMwbSameSubnetOnlyValue()
|
||||
{
|
||||
return getConfiguredValue(POLICY_MWB_SAME_SUBNET_ONLY);
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <winrt/Windows.Management.Deployment.h>
|
||||
|
||||
#include "../logger/logger.h"
|
||||
#include "../version/version.h"
|
||||
|
||||
namespace package {
|
||||
inline BOOL IsWin11OrGreater()
|
||||
@@ -47,10 +48,14 @@ namespace package {
|
||||
for (auto const& package : packageManager.FindPackagesForUser({}))
|
||||
{
|
||||
const auto& packageFullName = std::wstring{ package.Id().FullName() };
|
||||
const auto& packageVersion = package.Id().Version();
|
||||
|
||||
if (packageFullName.contains(packageDisplayName))
|
||||
{
|
||||
return true;
|
||||
if (packageVersion.Major == VERSION_MAJOR && packageVersion.Minor == VERSION_MINOR && packageVersion.Revision == VERSION_REVISION)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +77,7 @@ namespace package {
|
||||
// Declare use of an external location
|
||||
AddPackageOptions options;
|
||||
options.ExternalLocationUri(externalUri);
|
||||
options.ForceUpdateFromAnyVersion(true);
|
||||
|
||||
IAsyncOperationWithProgress<DeploymentResult, DeploymentProgress> deploymentOperation = packageManager.AddPackageByUriAsync(packageUri, options);
|
||||
deploymentOperation.get();
|
||||
|
||||
@@ -5,22 +5,22 @@ This folder contains samples of DSC files that can be used to configure PowerToy
|
||||
> [!NOTE]
|
||||
> PowerToys DSC user documentation can be found on [Learn Microsoft PowerToys documentation](https://aka.ms/powertoys-docs-dsc-configure).
|
||||
|
||||
### [configuration.dsc.yaml](./configuration.dsc.yaml)
|
||||
### [configuration.winget](./configuration.winget)
|
||||
|
||||
Sample configuration file that changes the enabled state of some modules, changes an integer setting and a hotkey setting.
|
||||
|
||||
### [installAndConfiguration.dsc.yaml](./installAndConfiguration.dsc.yaml)
|
||||
### [installAndConfiguration.winget](./installAndConfiguration.winget)
|
||||
|
||||
Installs PowerToys and applies configuration.dsc.yaml to it.
|
||||
Installs PowerToys and applies configuration.winget to it.
|
||||
|
||||
### [enableAllModules.dsc.yaml](./enableAllModules.dsc.yaml)
|
||||
### [enableAllModules.winget](./enableAllModules.winget)
|
||||
|
||||
Enables all PowerToys utilities.
|
||||
|
||||
### [disableAllModules.dsc.yaml](./disableAllModules.dsc.yaml)
|
||||
### [disableAllModules.winget](./disableAllModules.winget)
|
||||
|
||||
Disables all PowerToys utilities.
|
||||
|
||||
### [configureLauncherPlugins.dsc.yaml](./configureLauncherPlugins.dsc.yaml)
|
||||
### [configureLauncherPlugins.winget](./configureLauncherPlugins.winget)
|
||||
|
||||
Enables PowerToys Run and all its plugins and changes action keyword and global state for the Program plugin.
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (c) Microsoft Corporation.
|
||||
Licensed under the MIT License. -->
|
||||
<policyDefinitions xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" revision="1.15" schemaVersion="1.0" xmlns="http://schemas.microsoft.com/GroupPolicy/2006/07/PolicyDefinitions">
|
||||
<policyDefinitions xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" revision="1.16" schemaVersion="1.0" xmlns="http://schemas.microsoft.com/GroupPolicy/2006/07/PolicyDefinitions">
|
||||
<policyNamespaces>
|
||||
<target prefix="powertoys" namespace="Microsoft.Policies.PowerToys" />
|
||||
</policyNamespaces>
|
||||
<resources minRequiredRevision="1.15"/><!-- Last changed with PowerToys v0.88.0 -->
|
||||
<resources minRequiredRevision="1.16"/><!-- Last changed with PowerToys v0.89.0 -->
|
||||
<supportedOn>
|
||||
<definitions>
|
||||
<definition name="SUPPORTED_POWERTOYS_0_64_0" displayName="$(string.SUPPORTED_POWERTOYS_0_64_0)"/>
|
||||
@@ -24,6 +24,7 @@
|
||||
<definition name="SUPPORTED_POWERTOYS_0_85_0" displayName="$(string.SUPPORTED_POWERTOYS_0_85_0)"/>
|
||||
<definition name="SUPPORTED_POWERTOYS_0_86_0" displayName="$(string.SUPPORTED_POWERTOYS_0_86_0)"/>
|
||||
<definition name="SUPPORTED_POWERTOYS_0_88_0" displayName="$(string.SUPPORTED_POWERTOYS_0_88_0)"/>
|
||||
<definition name="SUPPORTED_POWERTOYS_0_89_0" displayName="$(string.SUPPORTED_POWERTOYS_0_89_0)"/>
|
||||
<definition name="SUPPORTED_POWERTOYS_0_64_0_TO_0_87_1" displayName="$(string.SUPPORTED_POWERTOYS_0_64_0_TO_0_87_1)"/>
|
||||
</definitions>
|
||||
</supportedOn>
|
||||
@@ -535,6 +536,16 @@
|
||||
<decimal value="0" />
|
||||
</disabledValue>
|
||||
</policy>
|
||||
<policy name="ConfigureRunAtStartup" class="Both" displayName="$(string.ConfigureRunAtStartup)" explainText="$(string.ConfigureRunAtStartupDescription)" key="Software\Policies\PowerToys" valueName="ConfigureRunAtStartup">
|
||||
<parentCategory ref="GeneralSettings" />
|
||||
<supportedOn ref="SUPPORTED_POWERTOYS_0_89_0" />
|
||||
<enabledValue>
|
||||
<decimal value="1" />
|
||||
</enabledValue>
|
||||
<disabledValue>
|
||||
<decimal value="0" />
|
||||
</disabledValue>
|
||||
</policy>
|
||||
<policy name="PowerToysRunAllPluginsEnabledState" class="Both" displayName="$(string.PowerToysRunAllPluginsEnabledState)" explainText="$(string.PowerToysRunAllPluginsEnabledStateDescription)" key="Software\Policies\PowerToys" valueName="PowerLauncherAllPluginsEnabledState">
|
||||
<parentCategory ref="PowerToysRun" />
|
||||
<supportedOn ref="SUPPORTED_POWERTOYS_0_75_0" />
|
||||
@@ -602,6 +613,16 @@
|
||||
<decimal value="0" />
|
||||
</disabledValue>
|
||||
</policy>
|
||||
<policy name="MwbAllowServiceMode" class="Machine" displayName="$(string.MwbAllowServiceMode)" explainText="$(string.MwbAllowServiceModeDescription)" key="Software\Policies\PowerToys" valueName="MwbAllowServiceMode">
|
||||
<parentCategory ref="MouseWithoutBorders" />
|
||||
<supportedOn ref="SUPPORTED_POWERTOYS_0_89_0" />
|
||||
<enabledValue>
|
||||
<decimal value="1" />
|
||||
</enabledValue>
|
||||
<disabledValue>
|
||||
<decimal value="0" />
|
||||
</disabledValue>
|
||||
</policy>
|
||||
<policy name="MwbSameSubnetOnly" class="Both" displayName="$(string.MwbSameSubnetOnly)" explainText="$(string.MwbSameSubnetOnlyDescription)" key="Software\Policies\PowerToys" valueName="MwbSameSubnetOnly">
|
||||
<parentCategory ref="MouseWithoutBorders" />
|
||||
<supportedOn ref="SUPPORTED_POWERTOYS_0_83_0" />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (c) Microsoft Corporation.
|
||||
Licensed under the MIT License. -->
|
||||
<policyDefinitionResources xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" revision="1.15" schemaVersion="1.0" xmlns="http://schemas.microsoft.com/GroupPolicy/2006/07/PolicyDefinitions">
|
||||
<policyDefinitionResources xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" revision="1.16" schemaVersion="1.0" xmlns="http://schemas.microsoft.com/GroupPolicy/2006/07/PolicyDefinitions">
|
||||
<displayName>PowerToys</displayName>
|
||||
<description>PowerToys</description>
|
||||
<resources>
|
||||
@@ -31,6 +31,7 @@
|
||||
<string id="SUPPORTED_POWERTOYS_0_85_0">PowerToys version 0.85.0 or later</string>
|
||||
<string id="SUPPORTED_POWERTOYS_0_86_0">PowerToys version 0.86.0 or later</string>
|
||||
<string id="SUPPORTED_POWERTOYS_0_88_0">PowerToys version 0.88.0 or later</string>
|
||||
<string id="SUPPORTED_POWERTOYS_0_89_0">PowerToys version 0.89.0 or later</string>
|
||||
<string id="SUPPORTED_POWERTOYS_0_64_0_TO_0_87_1">From PowerToys version 0.64.0 until PowerToys version 0.87.1</string>
|
||||
|
||||
<string id="ConfigureAllUtilityGlobalEnabledStateDescription">This policy configures the enabled state for all PowerToys utilities.
|
||||
@@ -111,6 +112,16 @@ If this setting is disabled, experimentation is not allowed.
|
||||
If this setting is enabled or not configured, the user can control diagnostic data sending in the PowerToys settings menu.
|
||||
|
||||
If this setting is disabled, diagnostic data sending is not allowed.
|
||||
</string>
|
||||
<string id="ConfigureRunAtStartupDescription">This policy configures the "run at startup" setting of PowerToys.
|
||||
|
||||
If you enable this setting, the "run at startup" setting will be always enabled and the user won't be able to disable it.
|
||||
|
||||
If you disable this setting, the "run at startup" setting will be always disabled and the user won't be able to enable it.
|
||||
|
||||
If you don't configure this setting, users are able to enable or disable "run at startup" at will.
|
||||
|
||||
Note: This only controls the PowerToys method that creates a scheduled task to start PowerToys at login. It doesn't control other custom auto-start methods that the user might try to use outside of PowerToys or manually creating/deleting a scheduled task.
|
||||
</string>
|
||||
<string id="PowerToysRunAllPluginsEnabledStateDescription">This policy configures the enabled state for all PowerToys Run plugins. All plugins will have the same state.
|
||||
|
||||
@@ -169,7 +180,14 @@ If you enable this policy, the user won't be able to enable the "block screensav
|
||||
|
||||
If you disable or don't configure this policy, the user takes control over the setting and can block the screensaver.
|
||||
</string>
|
||||
<string id="MwbAllowServiceModeDescription">This policy configures if the user is allowed to use Mouse Without Borders in Service Mode.
|
||||
|
||||
If this setting is enabled or not configured, the user can enable and use Mouse Without Borders in Service Mode.
|
||||
|
||||
If this setting is disabled, the user won't be able to enable or use Mouse Without Borders in Service Mode.
|
||||
|
||||
Note: As most other PowerToys policies, a restart of PowerToys is required for a change in this policy to take full effect.
|
||||
</string>
|
||||
<string id="MwbSameSubnetOnlyDescription">This policy configures if connections are only allowed in the same subnet.
|
||||
|
||||
If you enable this policy, the setting is enabled and only connections in the same subnet are allowed.
|
||||
@@ -264,12 +282,14 @@ If you don't configure this policy, the user takes control over the setting and
|
||||
<string id="MwbFileTransferEnabled">File transfer enabled</string>
|
||||
<string id="MwbUseOriginalUserInterface">Original user interface is available</string>
|
||||
<string id="MwbDisallowBlockingScreensaver">Disallow blocking screensaver on other machines</string>
|
||||
<string id="MwbAllowServiceMode">Allow Service Mode</string>
|
||||
<string id="MwbSameSubnetOnly">Connect only in same subnet</string>
|
||||
<string id="MwbValidateRemoteIp">Validate remote machine IP Address</string>
|
||||
<string id="MwbDisableUserDefinedIpMappingRules">Disable user defined IP Address mapping rules</string>
|
||||
<string id="MwbPolicyDefinedIpMappingRules">Predefined IP Address mapping rules</string>
|
||||
<string id="NewPlusHideTemplateFilenameExtension">Hide template filename extension</string>
|
||||
<string id="AllowDiagnosticData">Allow sending diagnostic data</string>
|
||||
<string id="ConfigureRunAtStartup">Configure the run at startup setting</string>
|
||||
</stringTable>
|
||||
|
||||
<presentationTable>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// 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.Tasks;
|
||||
|
||||
using AdvancedPaste.Models.KernelQueryCache;
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
|
||||
namespace AdvancedPaste.UnitTests.Mocks;
|
||||
|
||||
internal sealed class NoOpProgress : IProgress<double>
|
||||
{
|
||||
public void Report(double value)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using AdvancedPaste.Helpers;
|
||||
@@ -131,17 +132,18 @@ public sealed class AIServiceBatchIntegrationTests
|
||||
{
|
||||
VaultCredentialsProvider credentialsProvider = new();
|
||||
PromptModerationService promptModerationService = new(credentialsProvider);
|
||||
NoOpProgress progress = new();
|
||||
CustomTextTransformService customTextTransformService = new(credentialsProvider, promptModerationService);
|
||||
|
||||
switch (format)
|
||||
{
|
||||
case PasteFormats.CustomTextTransformation:
|
||||
return DataPackageHelpers.CreateFromText(await customTextTransformService.TransformTextAsync(batchTestInput.Prompt, batchTestInput.Clipboard));
|
||||
return DataPackageHelpers.CreateFromText(await customTextTransformService.TransformTextAsync(batchTestInput.Prompt, batchTestInput.Clipboard, CancellationToken.None, progress));
|
||||
|
||||
case PasteFormats.KernelQuery:
|
||||
var clipboardData = DataPackageHelpers.CreateFromText(batchTestInput.Clipboard).GetView();
|
||||
KernelService kernelService = new(new NoOpKernelQueryCacheService(), credentialsProvider, promptModerationService, customTextTransformService);
|
||||
return await kernelService.TransformClipboardAsync(batchTestInput.Prompt, clipboardData, isSavedQuery: false);
|
||||
return await kernelService.TransformClipboardAsync(batchTestInput.Prompt, clipboardData, isSavedQuery: false, CancellationToken.None, progress);
|
||||
|
||||
default:
|
||||
throw new InvalidOperationException($"Unexpected format {format}");
|
||||
|
||||
@@ -6,6 +6,7 @@ using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using AdvancedPaste.Helpers;
|
||||
@@ -130,7 +131,7 @@ public sealed class KernelServiceIntegrationTests : IDisposable
|
||||
|
||||
private async Task<DataPackageView> GetKernelOutputAsync(string prompt, DataPackage input)
|
||||
{
|
||||
var output = await _kernelService.TransformClipboardAsync(prompt, input.GetView(), isSavedQuery: false);
|
||||
var output = await _kernelService.TransformClipboardAsync(prompt, input.GetView(), isSavedQuery: false, CancellationToken.None, new NoOpProgress());
|
||||
|
||||
Assert.AreEqual(1, _eventListener.SemanticKernelEvents.Count);
|
||||
Assert.IsTrue(_eventListener.SemanticKernelTokens > 0);
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
// 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.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using AdvancedPaste.Helpers;
|
||||
using AdvancedPaste.Models;
|
||||
using AdvancedPaste.UnitTests.Mocks;
|
||||
using ManagedCommon;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Windows.Storage;
|
||||
using Windows.Storage.FileProperties;
|
||||
|
||||
namespace AdvancedPaste.UnitTests.ServicesTests;
|
||||
|
||||
[TestClass]
|
||||
public sealed class TranscodeHelperIntegrationTests
|
||||
{
|
||||
private sealed record class MediaProperties(BasicProperties Basic, MusicProperties Music, VideoProperties Video);
|
||||
|
||||
private const string InputRootFolder = @"%USERPROFILE%\AdvancedPasteTranscodeMediaTestData";
|
||||
|
||||
/// <summary> Tests transforming a folder of media files.
|
||||
/// - Verifies that the output file has the same basic properties (e.g. duration) as the input file.
|
||||
/// - Copies the output file to a subfolder of the input folder for manual inspection.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
[DataRow(@"audio", PasteFormats.TranscodeToMp3)]
|
||||
[DataRow(@"video", PasteFormats.TranscodeToMp4)]
|
||||
public async Task TestTransformFolder(string inputSubfolder, PasteFormats format)
|
||||
{
|
||||
var inputFolder = Environment.ExpandEnvironmentVariables(Path.Combine(InputRootFolder, inputSubfolder));
|
||||
|
||||
if (!Directory.Exists(inputFolder))
|
||||
{
|
||||
Assert.Inconclusive($"Skipping tests for {inputFolder} as it does not exist");
|
||||
}
|
||||
|
||||
var outputPath = Path.Combine(inputFolder, $"test_output_{format}");
|
||||
|
||||
foreach (var inputPath in Directory.EnumerateFiles(inputFolder))
|
||||
{
|
||||
await RunTestTransformFileAsync(inputPath, outputPath, format);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RunTestTransformFileAsync(string inputPath, string finalOutputPath, PasteFormats format)
|
||||
{
|
||||
Logger.LogDebug($"Running {nameof(RunTestTransformFileAsync)} for {inputPath}/{format}");
|
||||
|
||||
Directory.CreateDirectory(finalOutputPath);
|
||||
|
||||
var inputPackage = await DataPackageHelpers.CreateFromFileAsync(inputPath);
|
||||
var inputProperties = await GetPropertiesAsync(await StorageFile.GetFileFromPathAsync(inputPath));
|
||||
|
||||
var outputPackage = await TransformHelpers.TransformAsync(format, inputPackage.GetView(), CancellationToken.None, new NoOpProgress());
|
||||
|
||||
var outputItems = await outputPackage.GetView().GetStorageItemsAsync();
|
||||
Assert.AreEqual(1, outputItems.Count);
|
||||
var outputFile = outputItems.Single() as StorageFile;
|
||||
Assert.IsNotNull(outputFile);
|
||||
var outputProperties = await GetPropertiesAsync(outputFile);
|
||||
AssertPropertiesMatch(format, inputProperties, outputProperties);
|
||||
|
||||
await outputFile.CopyAsync(await StorageFolder.GetFolderFromPathAsync(finalOutputPath), outputFile.Name, NameCollisionOption.ReplaceExisting);
|
||||
await outputPackage.GetView().TryCleanupAfterDelayAsync(TimeSpan.Zero);
|
||||
}
|
||||
|
||||
private static void AssertPropertiesMatch(PasteFormats format, MediaProperties inputProperties, MediaProperties outputProperties)
|
||||
{
|
||||
Assert.IsTrue(outputProperties.Basic.Size > 0);
|
||||
|
||||
Assert.AreEqual(inputProperties.Music.Title, outputProperties.Music.Title);
|
||||
Assert.AreEqual(inputProperties.Music.Album, outputProperties.Music.Album);
|
||||
Assert.AreEqual(inputProperties.Music.Artist, outputProperties.Music.Artist);
|
||||
AssertDurationsApproxEqual(inputProperties.Music.Duration, outputProperties.Music.Duration);
|
||||
|
||||
if (format == PasteFormats.TranscodeToMp4)
|
||||
{
|
||||
Assert.AreEqual(inputProperties.Video.Title, outputProperties.Video.Title);
|
||||
AssertDurationsApproxEqual(inputProperties.Video.Duration, outputProperties.Video.Duration);
|
||||
|
||||
var inputVideoDimensions = GetNormalizedDimensions(inputProperties.Video);
|
||||
if (inputVideoDimensions != null)
|
||||
{
|
||||
Assert.AreEqual(inputVideoDimensions, GetNormalizedDimensions(outputProperties.Video));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<MediaProperties> GetPropertiesAsync(StorageFile file) =>
|
||||
new(await file.GetBasicPropertiesAsync(), await file.Properties.GetMusicPropertiesAsync(), await file.Properties.GetVideoPropertiesAsync());
|
||||
|
||||
private static void AssertDurationsApproxEqual(TimeSpan expected, TimeSpan actual) =>
|
||||
Assert.AreEqual(expected.Ticks, actual.Ticks, delta: TimeSpan.FromSeconds(1).Ticks);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the dimensions of a video, if available. Accounts for the fact that the dimensions may sometimes be swapped.
|
||||
/// </summary>
|
||||
private static (uint Width, uint Height)? GetNormalizedDimensions(VideoProperties properties) =>
|
||||
properties.Width == 0 || properties.Height == 0
|
||||
? null
|
||||
: (Math.Max(properties.Width, properties.Height), Math.Min(properties.Width, properties.Height));
|
||||
}
|
||||
@@ -28,6 +28,7 @@
|
||||
Background="Transparent"
|
||||
BorderThickness="4"
|
||||
CornerRadius="{TemplateBinding CornerRadius}"
|
||||
IsHitTestVisible="False"
|
||||
Visibility="Collapsed">
|
||||
<!-- CornerRadius needs to be > 0 -->
|
||||
<Grid.BorderBrush>
|
||||
|
||||
@@ -178,17 +178,36 @@
|
||||
Padding="0"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch">
|
||||
<Image
|
||||
x:Name="AIGlyphImage"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
Source="/Assets/AdvancedPaste/SemanticKernel.svg"
|
||||
Visibility="{Binding DataContext.IsAdvancedAIEnabled, Mode=OneWay, RelativeSource={RelativeSource Mode=TemplatedParent}, Converter={StaticResource BoolToVisibilityConverter}}" />
|
||||
<PathIcon
|
||||
x:Name="AIGlyph"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
Data="M128 766q0-42 24-77t65-48l178-57q32-11 61-30t52-42q50-50 71-114l58-179q13-40 48-65t78-26q42 0 77 24t50 65l58 177q21 66 72 117 49 50 117 72l176 58q43 14 69 48t26 80q0 41-25 76t-64 49l-178 58q-66 21-117 72-32 32-51 73t-33 84-26 83-30 73-45 51-71 20q-42 0-77-24t-49-65l-58-178q-8-25-19-47t-28-43q-34-43-77-68t-89-41-89-27-78-29-55-45-21-75zm1149 7q-76-29-145-53t-129-60-104-88-73-138l-57-176-67 176q-18 48-42 89t-60 78q-34 34-76 61t-89 43l-177 57q75 29 144 53t127 60 103 89 73 137l57 176 67-176q37-97 103-168t168-103l177-57zm-125 759q0-31 20-57t49-36l99-32q34-11 53-34t30-51 20-59 20-54 33-41 58-16q32 0 59 19t38 50q6 20 11 40t13 40 17 38 25 34q16 17 39 26t48 18 49 16 44 20 31 32 12 50q0 33-18 60t-51 38q-19 6-39 11t-41 13-39 17-34 25q-24 25-35 62t-24 73-35 61-68 25q-32 0-59-19t-38-50q-6-18-11-39t-13-41-17-40-24-33q-18-17-41-27t-47-17-49-15-43-20-30-33-12-54zm583 4q-43-13-74-30t-55-41-40-55-32-74q-12 41-29 72t-42 55-55 42-71 31q81 23 128 71t71 129q15-43 31-74t40-54 53-40 75-32z"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Visibility="{Binding DataContext.IsAdvancedAIEnabled, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource BoolToInvertedVisibilityConverter}}" />
|
||||
<ProgressRing
|
||||
Width="30"
|
||||
Height="30"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
IsActive="{Binding DataContext.IsBusy, Mode=OneWay, RelativeSource={RelativeSource Mode=TemplatedParent}}"
|
||||
IsIndeterminate="{Binding DataContext.HasIndeterminateTransformProgress, Mode=OneWay, RelativeSource={RelativeSource Mode=TemplatedParent}}"
|
||||
Maximum="100"
|
||||
Minimum="0"
|
||||
Visibility="{Binding DataContext.IsBusy, Mode=OneWay, RelativeSource={RelativeSource Mode=TemplatedParent}, Converter={StaticResource BoolToVisibilityConverter}}"
|
||||
Value="{Binding DataContext.TransformProgress, Mode=OneWay, RelativeSource={RelativeSource Mode=TemplatedParent}}" />
|
||||
|
||||
<StackPanel
|
||||
Margin="0"
|
||||
Padding="0"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Visibility="{Binding DataContext.IsBusy, Mode=OneWay, RelativeSource={RelativeSource Mode=TemplatedParent}, Converter={StaticResource BoolToInvertedVisibilityConverter}}">
|
||||
<Image
|
||||
x:Name="AIGlyphImage"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
Source="/Assets/AdvancedPaste/SemanticKernel.svg"
|
||||
Visibility="{Binding DataContext.IsAdvancedAIEnabled, Mode=OneWay, RelativeSource={RelativeSource Mode=TemplatedParent}, Converter={StaticResource BoolToVisibilityConverter}}" />
|
||||
<PathIcon
|
||||
x:Name="AIGlyph"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
Data="M128 766q0-42 24-77t65-48l178-57q32-11 61-30t52-42q50-50 71-114l58-179q13-40 48-65t78-26q42 0 77 24t50 65l58 177q21 66 72 117 49 50 117 72l176 58q43 14 69 48t26 80q0 41-25 76t-64 49l-178 58q-66 21-117 72-32 32-51 73t-33 84-26 83-30 73-45 51-71 20q-42 0-77-24t-49-65l-58-178q-8-25-19-47t-28-43q-34-43-77-68t-89-41-89-27-78-29-55-45-21-75zm1149 7q-76-29-145-53t-129-60-104-88-73-138l-57-176-67 176q-18 48-42 89t-60 78q-34 34-76 61t-89 43l-177 57q75 29 144 53t127 60 103 89 73 137l57 176 67-176q37-97 103-168t168-103l177-57zm-125 759q0-31 20-57t49-36l99-32q34-11 53-34t30-51 20-59 20-54 33-41 58-16q32 0 59 19t38 50q6 20 11 40t13 40 17 38 25 34q16 17 39 26t48 18 49 16 44 20 31 32 12 50q0 33-18 60t-51 38q-19 6-39 11t-41 13-39 17-34 25q-24 25-35 62t-24 73-35 61-68 25q-32 0-59-19t-38-50q-6-18-11-39t-13-41-17-40-24-33q-18-17-41-27t-47-17-49-15-43-20-30-33-12-54zm583 4q-43-13-74-30t-55-41-40-55-32-74q-12 41-29 72t-42 55-55 42-71 31q81 23 128 71t71 129q15-43 31-74t40-54 53-40 75-32z"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Visibility="{Binding DataContext.IsAdvancedAIEnabled, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource BoolToInvertedVisibilityConverter}}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Viewbox>
|
||||
<ScrollViewer
|
||||
@@ -572,6 +591,24 @@
|
||||
Duration="0:0:0.167" />
|
||||
</animations:Implicit.HideAnimations>
|
||||
</Button>
|
||||
<Button
|
||||
x:Name="CancelBtn"
|
||||
x:Uid="CancelBtnAutomation"
|
||||
Padding="0"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
ui:VisualExtensions.NormalizedCenterPoint="0.5,0.5"
|
||||
Command="{x:Bind CancelPasteActionCommand}"
|
||||
Content="{ui:FontIcon Glyph=,
|
||||
FontSize=16}"
|
||||
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}"
|
||||
IsEnabled="False"
|
||||
Style="{StaticResource SubtleButtonStyle}"
|
||||
Visibility="Collapsed">
|
||||
<ToolTipService.ToolTip>
|
||||
<TextBlock x:Uid="CancelBtnToolTip" TextWrapping="WrapWholeWords" />
|
||||
</ToolTipService.ToolTip>
|
||||
</Button>
|
||||
<!-- Transparent overlay to show tooltip -->
|
||||
<Grid
|
||||
x:Name="SendBtnOverlay"
|
||||
@@ -679,6 +716,10 @@
|
||||
<Setter Target="Loader.IsLoading" Value="True" />
|
||||
<Setter Target="InputTxtBox.IsEnabled" Value="False" />
|
||||
<Setter Target="SendBtn.IsEnabled" Value="False" />
|
||||
<Setter Target="SendBtn.Visibility" Value="Collapsed" />
|
||||
<Setter Target="SendBtnOverlay.Visibility" Value="Collapsed" />
|
||||
<Setter Target="CancelBtn.IsEnabled" Value="True" />
|
||||
<Setter Target="CancelBtn.Visibility" Value="Visible" />
|
||||
<Setter Target="DisclaimerPresenter.Visibility" Value="Collapsed" />
|
||||
<Setter Target="LoadingText.Visibility" Value="Visible" />
|
||||
</VisualState.Setters>
|
||||
|
||||
@@ -55,9 +55,9 @@ namespace AdvancedPaste.Controls
|
||||
|
||||
private void ViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(ViewModel.Busy) || e.PropertyName == nameof(ViewModel.PasteActionError))
|
||||
if (e.PropertyName is nameof(ViewModel.IsBusy) or nameof(ViewModel.PasteActionError))
|
||||
{
|
||||
var state = ViewModel.Busy ? "LoadingState" : ViewModel.PasteActionError.HasText ? "ErrorState" : "DefaultState";
|
||||
var state = ViewModel.IsBusy ? "LoadingState" : ViewModel.PasteActionError.HasText ? "ErrorState" : "DefaultState";
|
||||
VisualStateManager.GoToState(this, state, true);
|
||||
}
|
||||
}
|
||||
@@ -78,6 +78,9 @@ namespace AdvancedPaste.Controls
|
||||
[RelayCommand]
|
||||
private async Task GenerateCustomAIAsync() => await ViewModel.ExecuteCustomAIFormatFromCurrentQueryAsync(PasteActionSource.PromptBox);
|
||||
|
||||
[RelayCommand]
|
||||
private async Task CancelPasteActionAsync() => await ViewModel.CancelPasteActionAsync();
|
||||
|
||||
private async void InputTxtBox_KeyDown(object sender, Microsoft.UI.Xaml.Input.KeyRoutedEventArgs e)
|
||||
{
|
||||
if (e.Key == Windows.System.VirtualKey.Enter && InputTxtBox.Text.Length > 0 && ViewModel.IsCustomAIAvailable)
|
||||
|
||||
@@ -24,6 +24,7 @@ namespace AdvancedPaste
|
||||
{
|
||||
private readonly WindowMessageMonitor _msgMonitor;
|
||||
private readonly IUserSettings _userSettings;
|
||||
private readonly OptionsViewModel _optionsViewModel;
|
||||
|
||||
private bool _disposedValue;
|
||||
|
||||
@@ -32,8 +33,7 @@ namespace AdvancedPaste
|
||||
InitializeComponent();
|
||||
|
||||
_userSettings = App.GetService<IUserSettings>();
|
||||
|
||||
var optionsViewModel = App.GetService<OptionsViewModel>();
|
||||
_optionsViewModel = App.GetService<OptionsViewModel>();
|
||||
|
||||
var baseHeight = MinHeight;
|
||||
var coreActionCount = PasteFormat.MetadataDict.Values.Count(metadata => metadata.IsCoreAction);
|
||||
@@ -43,7 +43,7 @@ namespace AdvancedPaste
|
||||
double GetHeight(int maxCustomActionCount) =>
|
||||
baseHeight +
|
||||
new PasteFormatsToHeightConverter().GetHeight(coreActionCount + _userSettings.AdditionalActions.Count) +
|
||||
new PasteFormatsToHeightConverter() { MaxItems = maxCustomActionCount }.GetHeight(optionsViewModel.IsCustomAIServiceEnabled ? _userSettings.CustomActions.Count : 0);
|
||||
new PasteFormatsToHeightConverter() { MaxItems = maxCustomActionCount }.GetHeight(_optionsViewModel.IsCustomAIServiceEnabled ? _userSettings.CustomActions.Count : 0);
|
||||
|
||||
MinHeight = GetHeight(1);
|
||||
Height = GetHeight(5);
|
||||
@@ -52,9 +52,9 @@ namespace AdvancedPaste
|
||||
UpdateHeight();
|
||||
|
||||
_userSettings.Changed += (_, _) => UpdateHeight();
|
||||
optionsViewModel.PropertyChanged += (_, e) =>
|
||||
_optionsViewModel.PropertyChanged += (_, e) =>
|
||||
{
|
||||
if (e.PropertyName == nameof(optionsViewModel.IsCustomAIServiceEnabled))
|
||||
if (e.PropertyName == nameof(_optionsViewModel.IsCustomAIServiceEnabled))
|
||||
{
|
||||
UpdateHeight();
|
||||
}
|
||||
@@ -111,8 +111,9 @@ namespace AdvancedPaste
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private void WindowEx_Closed(object sender, Microsoft.UI.Xaml.WindowEventArgs args)
|
||||
private async void WindowEx_Closed(object sender, Microsoft.UI.Xaml.WindowEventArgs args)
|
||||
{
|
||||
await _optionsViewModel.CancelPasteActionAsync();
|
||||
Hide();
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
@@ -4,10 +4,14 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using AdvancedPaste.Models;
|
||||
using ManagedCommon;
|
||||
using Microsoft.Win32;
|
||||
using Windows.ApplicationModel.DataTransfer;
|
||||
using Windows.Data.Html;
|
||||
using Windows.Graphics.Imaging;
|
||||
@@ -18,8 +22,6 @@ namespace AdvancedPaste.Helpers;
|
||||
|
||||
internal static class DataPackageHelpers
|
||||
{
|
||||
private static readonly Lazy<HashSet<string>> ImageFileTypes = new(GetImageFileTypes());
|
||||
|
||||
private static readonly (string DataFormat, ClipboardFormat ClipboardFormat)[] DataFormats =
|
||||
[
|
||||
(StandardDataFormats.Text, ClipboardFormat.Text),
|
||||
@@ -27,6 +29,14 @@ internal static class DataPackageHelpers
|
||||
(StandardDataFormats.Bitmap, ClipboardFormat.Image),
|
||||
];
|
||||
|
||||
private static readonly Lazy<(ClipboardFormat Format, HashSet<string> FileTypes)[]> SupportedFileTypes =
|
||||
new(() =>
|
||||
[
|
||||
(ClipboardFormat.Image, GetImageFileTypes()),
|
||||
(ClipboardFormat.Audio, GetMediaFileTypes("audio")),
|
||||
(ClipboardFormat.Video, GetMediaFileTypes("video")),
|
||||
]);
|
||||
|
||||
internal static DataPackage CreateFromText(string text)
|
||||
{
|
||||
DataPackage dataPackage = new();
|
||||
@@ -57,9 +67,12 @@ internal static class DataPackageHelpers
|
||||
{
|
||||
availableFormats |= ClipboardFormat.File;
|
||||
|
||||
if (ImageFileTypes.Value.Contains(file.FileType))
|
||||
foreach (var (format, fileTypes) in SupportedFileTypes.Value)
|
||||
{
|
||||
availableFormats |= ClipboardFormat.Image;
|
||||
if (fileTypes.Contains(file.FileType))
|
||||
{
|
||||
availableFormats |= format;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -93,6 +106,60 @@ internal static class DataPackageHelpers
|
||||
return availableFormats == ClipboardFormat.Text ? !string.IsNullOrEmpty(await dataPackageView.GetTextAsync()) : availableFormats != ClipboardFormat.None;
|
||||
}
|
||||
|
||||
internal static async Task TryCleanupAfterDelayAsync(this DataPackageView dataPackageView, TimeSpan delay)
|
||||
{
|
||||
try
|
||||
{
|
||||
var tempFile = await GetSingleTempFileOrNullAsync(dataPackageView);
|
||||
|
||||
if (tempFile != null)
|
||||
{
|
||||
await Task.Delay(delay);
|
||||
|
||||
Logger.LogDebug($"Cleaning up temporary file with extension [{tempFile.Extension}] from data package after delay");
|
||||
|
||||
tempFile.Delete();
|
||||
if (NormalizeDirectoryPath(tempFile.Directory?.Parent?.FullName) == NormalizeDirectoryPath(Path.GetTempPath()))
|
||||
{
|
||||
tempFile.Directory?.Delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to clean up temporary files", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<FileInfo> GetSingleTempFileOrNullAsync(this DataPackageView dataPackageView)
|
||||
{
|
||||
if (!dataPackageView.Contains(StandardDataFormats.StorageItems))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var storageItems = await dataPackageView.GetStorageItemsAsync();
|
||||
|
||||
if (storageItems.Count != 1 || storageItems.Single() is not StorageFile file)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
FileInfo fileInfo = new(file.Path);
|
||||
var tempPathDirectory = NormalizeDirectoryPath(Path.GetTempPath());
|
||||
|
||||
var directoryPaths = new[] { fileInfo.Directory, fileInfo.Directory?.Parent }
|
||||
.Where(directory => directory != null)
|
||||
.Select(directory => NormalizeDirectoryPath(directory.FullName));
|
||||
|
||||
return directoryPaths.Contains(NormalizeDirectoryPath(Path.GetTempPath())) ? fileInfo : null;
|
||||
}
|
||||
|
||||
private static string NormalizeDirectoryPath(string path) =>
|
||||
Path.GetFullPath(new Uri(path).LocalPath)
|
||||
.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)
|
||||
.ToUpperInvariant();
|
||||
|
||||
internal static async Task<string> GetTextOrEmptyAsync(this DataPackageView dataPackageView) =>
|
||||
dataPackageView.Contains(StandardDataFormats.Text) ? await dataPackageView.GetTextAsync() : string.Empty;
|
||||
|
||||
@@ -153,4 +220,27 @@ internal static class DataPackageHelpers
|
||||
BitmapDecoder.GetDecoderInformationEnumerator()
|
||||
.SelectMany(di => di.FileExtensions)
|
||||
.ToHashSet(StringComparer.InvariantCultureIgnoreCase);
|
||||
|
||||
private static HashSet<string> GetMediaFileTypes(string mediaKind)
|
||||
{
|
||||
static string AssocQueryString(NativeMethods.AssocStr assocStr, string extension)
|
||||
{
|
||||
uint pcchOut = 0;
|
||||
|
||||
NativeMethods.AssocQueryString(NativeMethods.AssocF.None, assocStr, extension, null, null, ref pcchOut);
|
||||
|
||||
StringBuilder pszOut = new((int)pcchOut);
|
||||
var hResult = NativeMethods.AssocQueryString(NativeMethods.AssocF.None, assocStr, extension, null, pszOut, ref pcchOut);
|
||||
return hResult == NativeMethods.HResult.Ok ? pszOut.ToString() : string.Empty;
|
||||
}
|
||||
|
||||
var comparison = StringComparison.OrdinalIgnoreCase;
|
||||
var extensions = from extension in Registry.ClassesRoot.GetSubKeyNames()
|
||||
where extension.StartsWith('.')
|
||||
where AssocQueryString(NativeMethods.AssocStr.PerceivedType, extension).Equals(mediaKind, comparison) ||
|
||||
AssocQueryString(NativeMethods.AssocStr.ContentType, extension).StartsWith($"{mediaKind}/", comparison)
|
||||
select extension;
|
||||
|
||||
return extensions.ToHashSet(StringComparer.InvariantCultureIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using AdvancedPaste.Models;
|
||||
@@ -17,6 +18,8 @@ internal static class KernelExtensions
|
||||
private const string DataPackageKey = "DataPackage";
|
||||
private const string LastErrorKey = "LastError";
|
||||
private const string ActionChainKey = "ActionChain";
|
||||
private const string CancellationTokenKey = "CancellationToken";
|
||||
private const string ProgressKey = "Progress";
|
||||
|
||||
internal static DataPackageView GetDataPackageView(this Kernel kernel)
|
||||
{
|
||||
@@ -40,6 +43,14 @@ internal static class KernelExtensions
|
||||
|
||||
internal static void SetDataPackageView(this Kernel kernel, DataPackageView dataPackageView) => kernel.Data[DataPackageKey] = dataPackageView;
|
||||
|
||||
internal static CancellationToken GetCancellationToken(this Kernel kernel) => kernel.Data.TryGetValue(CancellationTokenKey, out object value) ? (CancellationToken)value : CancellationToken.None;
|
||||
|
||||
internal static void SetCancellationToken(this Kernel kernel, CancellationToken cancellationToken) => kernel.Data[CancellationTokenKey] = cancellationToken;
|
||||
|
||||
internal static IProgress<double> GetProgress(this Kernel kernel) => kernel.Data.TryGetValue(ProgressKey, out object obj) ? obj as IProgress<double> : null;
|
||||
|
||||
internal static void SetProgress(this Kernel kernel, IProgress<double> progress) => kernel.Data[ProgressKey] = progress;
|
||||
|
||||
internal static Exception GetLastError(this Kernel kernel) => kernel.Data.TryGetValue(LastErrorKey, out object obj) ? obj as Exception : null;
|
||||
|
||||
internal static void SetLastError(this Kernel kernel, Exception error) => kernel.Data[LastErrorKey] = error;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace AdvancedPaste.Helpers
|
||||
{
|
||||
@@ -83,6 +84,68 @@ namespace AdvancedPaste.Helpers
|
||||
Scancode = 0x0008,
|
||||
}
|
||||
|
||||
public enum HResult
|
||||
{
|
||||
Ok = 0x0000,
|
||||
False = 0x0001,
|
||||
InvalidArguments = unchecked((int)0x80070057),
|
||||
OutOfMemory = unchecked((int)0x8007000E),
|
||||
NoInterface = unchecked((int)0x80004002),
|
||||
Fail = unchecked((int)0x80004005),
|
||||
ExtractionFailed = unchecked((int)0x8004B200),
|
||||
ElementNotFound = unchecked((int)0x80070490),
|
||||
TypeElementNotFound = unchecked((int)0x8002802B),
|
||||
NoObject = unchecked((int)0x800401E5),
|
||||
Win32ErrorCanceled = 1223,
|
||||
Canceled = unchecked((int)0x800704C7),
|
||||
ResourceInUse = unchecked((int)0x800700AA),
|
||||
AccessDenied = unchecked((int)0x80030005),
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum AssocF
|
||||
{
|
||||
None = 0,
|
||||
Init_NoRemapCLSID = 0x1,
|
||||
Init_ByExeName = 0x2,
|
||||
Open_ByExeName = 0x3,
|
||||
Init_DefaultToStar = 0x4,
|
||||
Init_DefaultToFolder = 0x8,
|
||||
NoUserSettings = 0x10,
|
||||
NoTruncate = 0x20,
|
||||
Verify = 0x40,
|
||||
RemapRunDll = 0x80,
|
||||
NoFixUps = 0x100,
|
||||
IgnoreBaseClass = 0x200,
|
||||
}
|
||||
|
||||
public enum AssocStr
|
||||
{
|
||||
Command = 1,
|
||||
Executable,
|
||||
FriendlyDocName,
|
||||
FriendlyAppName,
|
||||
NoOpen,
|
||||
ShellNewValue,
|
||||
DDECommand,
|
||||
DDEIfExec,
|
||||
DDEApplication,
|
||||
DDETopic,
|
||||
InfoTip,
|
||||
QuickTip,
|
||||
TileInfo,
|
||||
ContentType,
|
||||
DefaultIcon,
|
||||
ShellExtension,
|
||||
PerceivedType,
|
||||
DelegateExecute,
|
||||
SupportedUriProtocols,
|
||||
ProgId,
|
||||
AppId,
|
||||
AppPublisher,
|
||||
AppIconReference,
|
||||
}
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
internal static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize);
|
||||
|
||||
@@ -100,5 +163,8 @@ namespace AdvancedPaste.Helpers
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
internal static extern bool GetCursorPos(out PointInter lpPoint);
|
||||
|
||||
[DllImport("Shlwapi.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
internal static extern HResult AssocQueryString(AssocF flags, AssocStr str, string pszAssoc, string pszExtra, [Out] StringBuilder pszOut, [In][Out] ref uint pcchOut);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Windows.Globalization;
|
||||
@@ -15,11 +16,14 @@ namespace AdvancedPaste.Helpers;
|
||||
|
||||
public static class OcrHelpers
|
||||
{
|
||||
public static async Task<string> ExtractTextAsync(SoftwareBitmap bitmap)
|
||||
public static async Task<string> ExtractTextAsync(SoftwareBitmap bitmap, CancellationToken cancellationToken)
|
||||
{
|
||||
var ocrLanguage = GetOCRLanguage() ?? throw new InvalidOperationException("Unable to determine OCR language");
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var ocrEngine = OcrEngine.TryCreateFromLanguage(ocrLanguage) ?? throw new InvalidOperationException("Unable to create OCR engine");
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var ocrResult = await ocrEngine.RecognizeAsync(bitmap);
|
||||
|
||||
return string.IsNullOrWhiteSpace(ocrResult.Text)
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
// 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.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using AdvancedPaste.Models;
|
||||
using ManagedCommon;
|
||||
using Windows.ApplicationModel.DataTransfer;
|
||||
using Windows.Media.MediaProperties;
|
||||
using Windows.Media.Transcoding;
|
||||
using Windows.Storage;
|
||||
|
||||
namespace AdvancedPaste.Helpers;
|
||||
|
||||
internal static class TranscodeHelpers
|
||||
{
|
||||
public static async Task<DataPackage> TranscodeToMp3Async(DataPackageView clipboardData, CancellationToken cancellationToken, IProgress<double> progress) =>
|
||||
await TranscodeMediaAsync(clipboardData, MediaEncodingProfile.CreateMp3(AudioEncodingQuality.High), ".mp3", cancellationToken, progress);
|
||||
|
||||
public static async Task<DataPackage> TranscodeToMp4Async(DataPackageView clipboardData, CancellationToken cancellationToken, IProgress<double> progress) =>
|
||||
await TranscodeMediaAsync(clipboardData, MediaEncodingProfile.CreateMp4(VideoEncodingQuality.HD1080p), ".mp4", cancellationToken, progress);
|
||||
|
||||
private static async Task<DataPackage> TranscodeMediaAsync(DataPackageView clipboardData, MediaEncodingProfile baseOutputProfile, string extension, CancellationToken cancellationToken, IProgress<double> progress)
|
||||
{
|
||||
Logger.LogTrace();
|
||||
|
||||
var inputFiles = await clipboardData.GetStorageItemsAsync();
|
||||
|
||||
if (inputFiles.Count != 1)
|
||||
{
|
||||
throw new InvalidOperationException($"{nameof(TranscodeMediaAsync)} does not support multiple files");
|
||||
}
|
||||
|
||||
var inputFile = inputFiles.Single() as StorageFile ?? throw new InvalidOperationException($"{nameof(TranscodeMediaAsync)} only supports files");
|
||||
var inputFileNameWithoutExtension = Path.GetFileNameWithoutExtension(inputFile.Path);
|
||||
|
||||
var inputProfile = await MediaEncodingProfile.CreateFromFileAsync(inputFile);
|
||||
var outputProfile = CreateOutputProfile(inputProfile, baseOutputProfile);
|
||||
|
||||
#if DEBUG
|
||||
static string ProfileToString(MediaEncodingProfile profile) => System.Text.Json.JsonSerializer.Serialize(profile, options: new() { WriteIndented = true });
|
||||
Logger.LogDebug($"{nameof(inputProfile)}: {ProfileToString(inputProfile)}");
|
||||
Logger.LogDebug($"{nameof(outputProfile)}: {ProfileToString(outputProfile)}");
|
||||
#endif
|
||||
|
||||
var outputFolder = await Task.Run(() => Directory.CreateTempSubdirectory("PowerToys_AdvancedPaste_"), cancellationToken);
|
||||
var outputFileName = StringComparer.OrdinalIgnoreCase.Equals(Path.GetExtension(inputFile.Path), extension) ? inputFileNameWithoutExtension + "_1" : inputFileNameWithoutExtension;
|
||||
var outputFilePath = Path.Combine(outputFolder.FullName, Path.ChangeExtension(outputFileName, extension));
|
||||
await File.WriteAllBytesAsync(outputFilePath, [], cancellationToken); // TranscodeAsync seems to require the output file to exist
|
||||
|
||||
await TranscodeMediaAsync(inputFile, await StorageFile.GetFileFromPathAsync(outputFilePath), outputProfile, cancellationToken, progress);
|
||||
|
||||
return await DataPackageHelpers.CreateFromFileAsync(outputFilePath);
|
||||
}
|
||||
|
||||
private static MediaEncodingProfile CreateOutputProfile(MediaEncodingProfile inputProfile, MediaEncodingProfile baseOutputProfile)
|
||||
{
|
||||
MediaEncodingProfile outputProfile = new()
|
||||
{
|
||||
Video = null,
|
||||
Audio = null,
|
||||
};
|
||||
|
||||
outputProfile.Container = baseOutputProfile.Container.Copy();
|
||||
|
||||
if (inputProfile.Video != null && baseOutputProfile.Video != null)
|
||||
{
|
||||
outputProfile.Video = baseOutputProfile.Video.Copy();
|
||||
|
||||
if (inputProfile.Video.Bitrate != 0)
|
||||
{
|
||||
outputProfile.Video.Bitrate = inputProfile.Video.Bitrate;
|
||||
}
|
||||
|
||||
if (inputProfile.Video.FrameRate.Numerator != 0)
|
||||
{
|
||||
outputProfile.Video.FrameRate.Numerator = inputProfile.Video.FrameRate.Numerator;
|
||||
}
|
||||
|
||||
if (inputProfile.Video.FrameRate.Denominator != 0)
|
||||
{
|
||||
outputProfile.Video.FrameRate.Denominator = inputProfile.Video.FrameRate.Denominator;
|
||||
}
|
||||
|
||||
if (inputProfile.Video.PixelAspectRatio.Numerator != 0)
|
||||
{
|
||||
outputProfile.Video.PixelAspectRatio.Numerator = inputProfile.Video.PixelAspectRatio.Numerator;
|
||||
}
|
||||
|
||||
if (inputProfile.Video.PixelAspectRatio.Denominator != 0)
|
||||
{
|
||||
outputProfile.Video.PixelAspectRatio.Denominator = inputProfile.Video.PixelAspectRatio.Denominator;
|
||||
}
|
||||
|
||||
outputProfile.Video.Width = inputProfile.Video.Width;
|
||||
outputProfile.Video.Height = inputProfile.Video.Height;
|
||||
}
|
||||
|
||||
if (inputProfile.Audio != null && baseOutputProfile.Audio != null)
|
||||
{
|
||||
outputProfile.Audio = baseOutputProfile.Audio.Copy();
|
||||
|
||||
if (inputProfile.Audio.Bitrate != 0)
|
||||
{
|
||||
outputProfile.Audio.Bitrate = inputProfile.Audio.Bitrate;
|
||||
}
|
||||
|
||||
if (inputProfile.Audio.BitsPerSample != 0)
|
||||
{
|
||||
outputProfile.Audio.BitsPerSample = inputProfile.Audio.BitsPerSample;
|
||||
}
|
||||
|
||||
if (inputProfile.Audio.ChannelCount != 0)
|
||||
{
|
||||
outputProfile.Audio.ChannelCount = inputProfile.Audio.ChannelCount;
|
||||
}
|
||||
|
||||
if (inputProfile.Audio.SampleRate != 0)
|
||||
{
|
||||
outputProfile.Audio.SampleRate = inputProfile.Audio.SampleRate;
|
||||
}
|
||||
}
|
||||
|
||||
return outputProfile;
|
||||
}
|
||||
|
||||
private static async Task TranscodeMediaAsync(StorageFile inputFile, StorageFile outputFile, MediaEncodingProfile outputProfile, CancellationToken cancellationToken, IProgress<double> progress)
|
||||
{
|
||||
if (outputProfile.Video == null && outputProfile.Audio == null)
|
||||
{
|
||||
throw new InvalidOperationException("Target profile does not contain media");
|
||||
}
|
||||
|
||||
async Task<PrepareTranscodeResult> GetPrepareResult(bool hardwareAccelerationEnabled)
|
||||
{
|
||||
MediaTranscoder transcoder = new()
|
||||
{
|
||||
AlwaysReencode = false,
|
||||
HardwareAccelerationEnabled = hardwareAccelerationEnabled,
|
||||
};
|
||||
|
||||
return await transcoder.PrepareFileTranscodeAsync(inputFile, outputFile, outputProfile);
|
||||
}
|
||||
|
||||
var prepareResult = await GetPrepareResult(hardwareAccelerationEnabled: true);
|
||||
|
||||
if (!prepareResult.CanTranscode)
|
||||
{
|
||||
Logger.LogWarning($"Unable to transcode with hardware acceleration enabled, falling back to software; {nameof(prepareResult.FailureReason)}={prepareResult.FailureReason}");
|
||||
|
||||
prepareResult = await GetPrepareResult(hardwareAccelerationEnabled: false);
|
||||
}
|
||||
|
||||
if (!prepareResult.CanTranscode)
|
||||
{
|
||||
var message = ResourceLoaderInstance.ResourceLoader.GetString(prepareResult.FailureReason == TranscodeFailureReason.CodecNotFound ? "TranscodeErrorUnsupportedCodec" : "TranscodeErrorGeneral");
|
||||
throw new PasteActionException(message, new InvalidOperationException($"Error transcoding; {nameof(prepareResult.FailureReason)}={prepareResult.FailureReason}"));
|
||||
}
|
||||
|
||||
await prepareResult.TranscodeAsync().AsTask(cancellationToken, progress);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using AdvancedPaste.Models;
|
||||
@@ -17,17 +18,19 @@ namespace AdvancedPaste.Helpers;
|
||||
|
||||
public static class TransformHelpers
|
||||
{
|
||||
public static async Task<DataPackage> TransformAsync(PasteFormats format, DataPackageView clipboardData)
|
||||
public static async Task<DataPackage> TransformAsync(PasteFormats format, DataPackageView clipboardData, CancellationToken cancellationToken, IProgress<double> progress)
|
||||
{
|
||||
return format switch
|
||||
{
|
||||
PasteFormats.PlainText => await ToPlainTextAsync(clipboardData),
|
||||
PasteFormats.Markdown => await ToMarkdownAsync(clipboardData),
|
||||
PasteFormats.Json => await ToJsonAsync(clipboardData),
|
||||
PasteFormats.ImageToText => await ImageToTextAsync(clipboardData),
|
||||
PasteFormats.PasteAsTxtFile => await ToTxtFileAsync(clipboardData),
|
||||
PasteFormats.PasteAsPngFile => await ToPngFileAsync(clipboardData),
|
||||
PasteFormats.PasteAsHtmlFile => await ToHtmlFileAsync(clipboardData),
|
||||
PasteFormats.ImageToText => await ImageToTextAsync(clipboardData, cancellationToken),
|
||||
PasteFormats.PasteAsTxtFile => await ToTxtFileAsync(clipboardData, cancellationToken),
|
||||
PasteFormats.PasteAsPngFile => await ToPngFileAsync(clipboardData, cancellationToken),
|
||||
PasteFormats.PasteAsHtmlFile => await ToHtmlFileAsync(clipboardData, cancellationToken),
|
||||
PasteFormats.TranscodeToMp3 => await TranscodeHelpers.TranscodeToMp3Async(clipboardData, cancellationToken, progress),
|
||||
PasteFormats.TranscodeToMp4 => await TranscodeHelpers.TranscodeToMp4Async(clipboardData, cancellationToken, progress),
|
||||
PasteFormats.KernelQuery => throw new ArgumentException($"Unsupported format {format}", nameof(format)),
|
||||
PasteFormats.CustomTextTransformation => throw new ArgumentException($"Unsupported format {format}", nameof(format)),
|
||||
_ => throw new ArgumentException($"Unknown value {format}", nameof(format)),
|
||||
@@ -52,16 +55,16 @@ public static class TransformHelpers
|
||||
return CreateDataPackageFromText(await JsonHelper.ToJsonFromXmlOrCsvAsync(clipboardData));
|
||||
}
|
||||
|
||||
private static async Task<DataPackage> ImageToTextAsync(DataPackageView clipboardData)
|
||||
private static async Task<DataPackage> ImageToTextAsync(DataPackageView clipboardData, CancellationToken cancellationToken)
|
||||
{
|
||||
Logger.LogTrace();
|
||||
|
||||
var bitmap = await clipboardData.GetImageContentAsync() ?? throw new ArgumentException("No image content found in clipboard", nameof(clipboardData));
|
||||
var text = await OcrHelpers.ExtractTextAsync(bitmap);
|
||||
var text = await OcrHelpers.ExtractTextAsync(bitmap, cancellationToken);
|
||||
return CreateDataPackageFromText(text);
|
||||
}
|
||||
|
||||
private static async Task<DataPackage> ToPngFileAsync(DataPackageView clipboardData)
|
||||
private static async Task<DataPackage> ToPngFileAsync(DataPackageView clipboardData, CancellationToken cancellationToken)
|
||||
{
|
||||
Logger.LogTrace();
|
||||
|
||||
@@ -72,25 +75,25 @@ public static class TransformHelpers
|
||||
encoder.SetSoftwareBitmap(clipboardBitmap);
|
||||
await encoder.FlushAsync();
|
||||
|
||||
return await CreateDataPackageFromFileContentAsync(pngStream.AsStreamForRead(), "png");
|
||||
return await CreateDataPackageFromFileContentAsync(pngStream.AsStreamForRead(), "png", cancellationToken);
|
||||
}
|
||||
|
||||
private static async Task<DataPackage> ToTxtFileAsync(DataPackageView clipboardData)
|
||||
private static async Task<DataPackage> ToTxtFileAsync(DataPackageView clipboardData, CancellationToken cancellationToken)
|
||||
{
|
||||
Logger.LogTrace();
|
||||
|
||||
var text = await clipboardData.GetTextOrHtmlTextAsync();
|
||||
return await CreateDataPackageFromFileContentAsync(text, "txt");
|
||||
return await CreateDataPackageFromFileContentAsync(text, "txt", cancellationToken);
|
||||
}
|
||||
|
||||
private static async Task<DataPackage> ToHtmlFileAsync(DataPackageView clipboardData)
|
||||
private static async Task<DataPackage> ToHtmlFileAsync(DataPackageView clipboardData, CancellationToken cancellationToken)
|
||||
{
|
||||
Logger.LogTrace();
|
||||
|
||||
var cfHtml = await clipboardData.GetHtmlContentAsync();
|
||||
var html = RemoveHtmlMetadata(cfHtml);
|
||||
|
||||
return await CreateDataPackageFromFileContentAsync(html, "html");
|
||||
return await CreateDataPackageFromFileContentAsync(html, "html", cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -114,7 +117,7 @@ public static class TransformHelpers
|
||||
return (startFragmentIndex == null || endFragmentIndex == null) ? cfHtml : cfHtml[startFragmentIndex.Value..endFragmentIndex.Value];
|
||||
}
|
||||
|
||||
private static async Task<DataPackage> CreateDataPackageFromFileContentAsync(string data, string fileExtension)
|
||||
private static async Task<DataPackage> CreateDataPackageFromFileContentAsync(string data, string fileExtension, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrEmpty(data))
|
||||
{
|
||||
@@ -123,16 +126,16 @@ public static class TransformHelpers
|
||||
|
||||
var path = GetPasteAsFileTempFilePath(fileExtension);
|
||||
|
||||
await File.WriteAllTextAsync(path, data);
|
||||
await File.WriteAllTextAsync(path, data, cancellationToken);
|
||||
return await DataPackageHelpers.CreateFromFileAsync(path);
|
||||
}
|
||||
|
||||
private static async Task<DataPackage> CreateDataPackageFromFileContentAsync(Stream stream, string fileExtension)
|
||||
private static async Task<DataPackage> CreateDataPackageFromFileContentAsync(Stream stream, string fileExtension, CancellationToken cancellationToken)
|
||||
{
|
||||
var path = GetPasteAsFileTempFilePath(fileExtension);
|
||||
|
||||
using var fileStream = File.Create(path);
|
||||
await stream.CopyToAsync(fileStream);
|
||||
await stream.CopyToAsync(fileStream, cancellationToken);
|
||||
|
||||
return await DataPackageHelpers.CreateFromFileAsync(path);
|
||||
}
|
||||
|
||||
@@ -108,7 +108,9 @@ namespace AdvancedPaste.Settings
|
||||
(PasteFormats.ImageToText, [sourceAdditionalActions.ImageToText]),
|
||||
(PasteFormats.PasteAsTxtFile, [sourceAdditionalActions.PasteAsFile, sourceAdditionalActions.PasteAsFile.PasteAsTxtFile]),
|
||||
(PasteFormats.PasteAsPngFile, [sourceAdditionalActions.PasteAsFile, sourceAdditionalActions.PasteAsFile.PasteAsPngFile]),
|
||||
(PasteFormats.PasteAsHtmlFile, [sourceAdditionalActions.PasteAsFile, sourceAdditionalActions.PasteAsFile.PasteAsHtmlFile])
|
||||
(PasteFormats.PasteAsHtmlFile, [sourceAdditionalActions.PasteAsFile, sourceAdditionalActions.PasteAsFile.PasteAsHtmlFile]),
|
||||
(PasteFormats.TranscodeToMp3, [sourceAdditionalActions.Transcode, sourceAdditionalActions.Transcode.TranscodeToMp3]),
|
||||
(PasteFormats.TranscodeToMp4, [sourceAdditionalActions.Transcode, sourceAdditionalActions.Transcode.TranscodeToMp4]),
|
||||
];
|
||||
|
||||
_additionalActions.Clear();
|
||||
|
||||
@@ -13,6 +13,7 @@ public enum ClipboardFormat
|
||||
Text = 1 << 0,
|
||||
Html = 1 << 1,
|
||||
Audio = 1 << 2,
|
||||
Image = 1 << 3,
|
||||
File = 1 << 4, // output only for now
|
||||
Video = 1 << 3,
|
||||
Image = 1 << 4,
|
||||
File = 1 << 5, // output only for now
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ public sealed class PasteActionError
|
||||
public static PasteActionError FromException(Exception ex) =>
|
||||
new()
|
||||
{
|
||||
Text = ex is PasteActionException ? ex.Message : ResourceLoaderInstance.ResourceLoader.GetString("PasteError"),
|
||||
Text = ex is PasteActionException ? ex.Message : ResourceLoaderInstance.ResourceLoader.GetString(ex is OperationCanceledException ? "PasteActionCanceled" : "PasteError"),
|
||||
Details = (ex as PasteActionException)?.AIServiceMessage ?? string.Empty,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -82,12 +82,34 @@ public enum PasteFormats
|
||||
KernelFunctionDescription = "Takes HTML data in the clipboard and transforms it to an HTML file.")]
|
||||
PasteAsHtmlFile,
|
||||
|
||||
[PasteFormatMetadata(
|
||||
IsCoreAction = false,
|
||||
ResourceId = "TranscodeToMp3",
|
||||
IconGlyph = "\uE8D6",
|
||||
RequiresAIService = false,
|
||||
CanPreview = false,
|
||||
SupportedClipboardFormats = ClipboardFormat.Audio | ClipboardFormat.Video,
|
||||
IPCKey = AdvancedPasteTranscodeAction.PropertyNames.TranscodeToMp3,
|
||||
KernelFunctionDescription = "Takes an audio or video file in the clipboard and transcodes it to MP3.")]
|
||||
TranscodeToMp3,
|
||||
|
||||
[PasteFormatMetadata(
|
||||
IsCoreAction = false,
|
||||
ResourceId = "TranscodeToMp4",
|
||||
IconGlyph = "\uE714",
|
||||
RequiresAIService = false,
|
||||
CanPreview = false,
|
||||
SupportedClipboardFormats = ClipboardFormat.Video,
|
||||
IPCKey = AdvancedPasteTranscodeAction.PropertyNames.TranscodeToMp4,
|
||||
KernelFunctionDescription = "Takes a video file in the clipboard and transcodes it to MP4 (H.264/AAC).")]
|
||||
TranscodeToMp4,
|
||||
|
||||
[PasteFormatMetadata(
|
||||
IsCoreAction = false,
|
||||
IconGlyph = "\uE945",
|
||||
RequiresAIService = true,
|
||||
CanPreview = true,
|
||||
SupportedClipboardFormats = ClipboardFormat.Text | ClipboardFormat.Html | ClipboardFormat.Audio | ClipboardFormat.Image,
|
||||
SupportedClipboardFormats = ClipboardFormat.Text | ClipboardFormat.Html | ClipboardFormat.Audio | ClipboardFormat.Video | ClipboardFormat.Image,
|
||||
RequiresPrompt = true)]
|
||||
KernelQuery,
|
||||
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
// 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;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace AdvancedPaste.Services;
|
||||
|
||||
public interface ICustomTextTransformService
|
||||
{
|
||||
Task<string> TransformTextAsync(string prompt, string inputText);
|
||||
Task<string> TransformTextAsync(string prompt, string inputText, CancellationToken cancellationToken, IProgress<double> progress);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
// 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;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Windows.ApplicationModel.DataTransfer;
|
||||
@@ -10,5 +12,5 @@ namespace AdvancedPaste.Services;
|
||||
|
||||
public interface IKernelService
|
||||
{
|
||||
Task<DataPackage> TransformClipboardAsync(string prompt, DataPackageView clipboardData, bool isSavedQuery);
|
||||
Task<DataPackage> TransformClipboardAsync(string prompt, DataPackageView clipboardData, bool isSavedQuery, CancellationToken cancellationToken, IProgress<double> progress);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
// 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;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using AdvancedPaste.Models;
|
||||
@@ -11,5 +13,5 @@ namespace AdvancedPaste.Services;
|
||||
|
||||
public interface IPasteFormatExecutor
|
||||
{
|
||||
Task<DataPackage> ExecutePasteFormatAsync(PasteFormat pasteFormat, PasteActionSource source);
|
||||
Task<DataPackage> ExecutePasteFormatAsync(PasteFormat pasteFormat, PasteActionSource source, CancellationToken cancellationToken, IProgress<double> progress);
|
||||
}
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
// 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.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace AdvancedPaste.Services;
|
||||
|
||||
public interface IPromptModerationService
|
||||
{
|
||||
Task ValidateAsync(string fullPrompt);
|
||||
Task ValidateAsync(string fullPrompt, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using AdvancedPaste.Helpers;
|
||||
@@ -36,12 +37,14 @@ public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheServi
|
||||
|
||||
protected abstract AIServiceUsage GetAIServiceUsage(ChatMessageContent chatMessage);
|
||||
|
||||
public async Task<DataPackage> TransformClipboardAsync(string prompt, DataPackageView clipboardData, bool isSavedQuery)
|
||||
public async Task<DataPackage> TransformClipboardAsync(string prompt, DataPackageView clipboardData, bool isSavedQuery, CancellationToken cancellationToken, IProgress<double> progress)
|
||||
{
|
||||
Logger.LogTrace();
|
||||
|
||||
var kernel = CreateKernel();
|
||||
kernel.SetDataPackageView(clipboardData);
|
||||
kernel.SetCancellationToken(cancellationToken);
|
||||
kernel.SetProgress(progress);
|
||||
|
||||
CacheKey cacheKey = new() { Prompt = prompt, AvailableFormats = await clipboardData.GetAvailableFormatsAsync() };
|
||||
var maybeCacheValue = _queryCacheService.ReadOrNull(cacheKey);
|
||||
@@ -51,7 +54,7 @@ public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheServi
|
||||
|
||||
try
|
||||
{
|
||||
(chatHistory, var usage) = cacheUsed ? await ExecuteCachedActionChain(kernel, maybeCacheValue.ActionChain) : await ExecuteAICompletion(kernel, prompt);
|
||||
(chatHistory, var usage) = cacheUsed ? await ExecuteCachedActionChain(kernel, maybeCacheValue.ActionChain) : await ExecuteAICompletion(kernel, prompt, cancellationToken);
|
||||
|
||||
LogResult(cacheUsed, isSavedQuery, kernel.GetOrAddActionChain(), usage);
|
||||
|
||||
@@ -84,7 +87,7 @@ public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheServi
|
||||
AdvancedPasteSemanticKernelErrorEvent errorEvent = new(ex is PasteActionModeratedException ? PasteActionModeratedException.ErrorDescription : ex.Message);
|
||||
PowerToysTelemetry.Log.WriteEvent(errorEvent);
|
||||
|
||||
if (ex is PasteActionException)
|
||||
if (ex is PasteActionException or OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
@@ -127,7 +130,7 @@ public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheServi
|
||||
return $"{combinedSystemMessage}{newLine}{newLine}User instructions:{newLine}{userPromptMessage.Content}";
|
||||
}
|
||||
|
||||
private async Task<(ChatHistory ChatHistory, AIServiceUsage Usage)> ExecuteAICompletion(Kernel kernel, string prompt)
|
||||
private async Task<(ChatHistory ChatHistory, AIServiceUsage Usage)> ExecuteAICompletion(Kernel kernel, string prompt, CancellationToken cancellationToken)
|
||||
{
|
||||
ChatHistory chatHistory = [];
|
||||
|
||||
@@ -141,10 +144,10 @@ public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheServi
|
||||
chatHistory.AddSystemMessage($"Available clipboard formats: {await kernel.GetDataFormatsAsync()}");
|
||||
chatHistory.AddUserMessage(prompt);
|
||||
|
||||
await _promptModerationService.ValidateAsync(GetFullPrompt(chatHistory));
|
||||
await _promptModerationService.ValidateAsync(GetFullPrompt(chatHistory), cancellationToken);
|
||||
|
||||
var chatResult = await kernel.GetRequiredService<IChatCompletionService>()
|
||||
.GetChatMessageContentAsync(chatHistory, PromptExecutionSettings, kernel);
|
||||
.GetChatMessageContentAsync(chatHistory, PromptExecutionSettings, kernel, cancellationToken);
|
||||
chatHistory.Add(chatResult);
|
||||
|
||||
var totalUsage = chatHistory.Select(GetAIServiceUsage)
|
||||
@@ -157,6 +160,8 @@ public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheServi
|
||||
{
|
||||
foreach (var item in actionChain)
|
||||
{
|
||||
kernel.GetCancellationToken().ThrowIfCancellationRequested();
|
||||
|
||||
if (item.Arguments.Count > 0)
|
||||
{
|
||||
await ExecutePromptTransformAsync(kernel, item.Format, item.Arguments[PromptParameterName]);
|
||||
@@ -208,14 +213,14 @@ public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheServi
|
||||
async dataPackageView =>
|
||||
{
|
||||
var input = await dataPackageView.GetTextAsync();
|
||||
string output = await GetPromptBasedOutput(format, prompt, input);
|
||||
string output = await GetPromptBasedOutput(format, prompt, input, kernel.GetCancellationToken(), kernel.GetProgress());
|
||||
return DataPackageHelpers.CreateFromText(output);
|
||||
});
|
||||
|
||||
private async Task<string> GetPromptBasedOutput(PasteFormats format, string prompt, string input) =>
|
||||
private async Task<string> GetPromptBasedOutput(PasteFormats format, string prompt, string input, CancellationToken cancellationToken, IProgress<double> progress) =>
|
||||
format switch
|
||||
{
|
||||
PasteFormats.CustomTextTransformation => await _customTextTransformService.TransformTextAsync(prompt, input),
|
||||
PasteFormats.CustomTextTransformation => await _customTextTransformService.TransformTextAsync(prompt, input, cancellationToken, progress),
|
||||
_ => throw new ArgumentException($"Unsupported format {format} for prompt transform", nameof(format)),
|
||||
};
|
||||
|
||||
@@ -223,7 +228,7 @@ public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheServi
|
||||
ExecuteTransformAsync(
|
||||
kernel,
|
||||
new ActionChainItem(format, Arguments: []),
|
||||
async dataPackageView => await TransformHelpers.TransformAsync(format, dataPackageView));
|
||||
async dataPackageView => await TransformHelpers.TransformAsync(format, dataPackageView, kernel.GetCancellationToken(), kernel.GetProgress()));
|
||||
|
||||
private static async Task<string> ExecuteTransformAsync(Kernel kernel, ActionChainItem actionChainItem, Func<DataPackageView, Task<DataPackage>> transformFunc)
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using AdvancedPaste.Helpers;
|
||||
@@ -23,11 +24,11 @@ public sealed class CustomTextTransformService(IAICredentialsProvider aiCredenti
|
||||
private readonly IAICredentialsProvider _aiCredentialsProvider = aiCredentialsProvider;
|
||||
private readonly IPromptModerationService _promptModerationService = promptModerationService;
|
||||
|
||||
private async Task<Completions> GetAICompletionAsync(string systemInstructions, string userMessage)
|
||||
private async Task<Completions> GetAICompletionAsync(string systemInstructions, string userMessage, CancellationToken cancellationToken)
|
||||
{
|
||||
var fullPrompt = systemInstructions + "\n\n" + userMessage;
|
||||
|
||||
await _promptModerationService.ValidateAsync(fullPrompt);
|
||||
await _promptModerationService.ValidateAsync(fullPrompt, cancellationToken);
|
||||
|
||||
OpenAIClient azureAIClient = new(_aiCredentialsProvider.Key);
|
||||
|
||||
@@ -41,7 +42,8 @@ public sealed class CustomTextTransformService(IAICredentialsProvider aiCredenti
|
||||
},
|
||||
Temperature = 0.01F,
|
||||
MaxTokens = 2000,
|
||||
});
|
||||
},
|
||||
cancellationToken);
|
||||
|
||||
if (response.Value.Choices[0].FinishReason == "length")
|
||||
{
|
||||
@@ -51,7 +53,7 @@ public sealed class CustomTextTransformService(IAICredentialsProvider aiCredenti
|
||||
return response;
|
||||
}
|
||||
|
||||
public async Task<string> TransformTextAsync(string prompt, string inputText)
|
||||
public async Task<string> TransformTextAsync(string prompt, string inputText, CancellationToken cancellationToken, IProgress<double> progress)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(prompt))
|
||||
{
|
||||
@@ -80,7 +82,7 @@ Output:
|
||||
|
||||
try
|
||||
{
|
||||
var response = await GetAICompletionAsync(systemInstructions, userMessage);
|
||||
var response = await GetAICompletionAsync(systemInstructions, userMessage, cancellationToken);
|
||||
|
||||
var usage = response.Usage;
|
||||
AdvancedPasteGenerateCustomFormatEvent telemetryEvent = new(usage.PromptTokens, usage.CompletionTokens, ModelName);
|
||||
@@ -98,7 +100,7 @@ Output:
|
||||
AdvancedPasteGenerateCustomErrorEvent errorEvent = new(ex is PasteActionModeratedException ? PasteActionModeratedException.ErrorDescription : ex.Message);
|
||||
PowerToysTelemetry.Log.WriteEvent(errorEvent);
|
||||
|
||||
if (ex is PasteActionException)
|
||||
if (ex is PasteActionException or OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.ClientModel;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using AdvancedPaste.Helpers;
|
||||
@@ -18,12 +19,12 @@ public sealed class PromptModerationService(IAICredentialsProvider aiCredentials
|
||||
|
||||
private readonly IAICredentialsProvider _aiCredentialsProvider = aiCredentialsProvider;
|
||||
|
||||
public async Task ValidateAsync(string fullPrompt)
|
||||
public async Task ValidateAsync(string fullPrompt, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
ModerationClient moderationClient = new(ModelName, _aiCredentialsProvider.Key);
|
||||
var moderationClientResult = await moderationClient.ClassifyTextAsync(fullPrompt);
|
||||
var moderationClientResult = await moderationClient.ClassifyTextAsync(fullPrompt, cancellationToken);
|
||||
var moderationResult = moderationClientResult.Value;
|
||||
|
||||
Logger.LogDebug($"{nameof(PromptModerationService)}.{nameof(ValidateAsync)} complete; {nameof(moderationResult.Flagged)}={moderationResult.Flagged}");
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using AdvancedPaste.Helpers;
|
||||
@@ -17,7 +18,7 @@ public sealed class PasteFormatExecutor(IKernelService kernelService, ICustomTex
|
||||
private readonly IKernelService _kernelService = kernelService;
|
||||
private readonly ICustomTextTransformService _customTextTransformService = customTextTransformService;
|
||||
|
||||
public async Task<DataPackage> ExecutePasteFormatAsync(PasteFormat pasteFormat, PasteActionSource source)
|
||||
public async Task<DataPackage> ExecutePasteFormatAsync(PasteFormat pasteFormat, PasteActionSource source, CancellationToken cancellationToken, IProgress<double> progress)
|
||||
{
|
||||
if (!pasteFormat.IsEnabled)
|
||||
{
|
||||
@@ -34,9 +35,9 @@ public sealed class PasteFormatExecutor(IKernelService kernelService, ICustomTex
|
||||
return await Task.Run(async () =>
|
||||
pasteFormat.Format switch
|
||||
{
|
||||
PasteFormats.KernelQuery => await _kernelService.TransformClipboardAsync(pasteFormat.Prompt, clipboardData, pasteFormat.IsSavedQuery),
|
||||
PasteFormats.CustomTextTransformation => DataPackageHelpers.CreateFromText(await _customTextTransformService.TransformTextAsync(pasteFormat.Prompt, await clipboardData.GetTextAsync())),
|
||||
_ => await TransformHelpers.TransformAsync(format, clipboardData),
|
||||
PasteFormats.KernelQuery => await _kernelService.TransformClipboardAsync(pasteFormat.Prompt, clipboardData, pasteFormat.IsSavedQuery, cancellationToken, progress),
|
||||
PasteFormats.CustomTextTransformation => DataPackageHelpers.CreateFromText(await _customTextTransformService.TransformTextAsync(pasteFormat.Prompt, await clipboardData.GetTextAsync(), cancellationToken, progress)),
|
||||
_ => await TransformHelpers.TransformAsync(format, clipboardData, cancellationToken, progress),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -135,6 +135,9 @@
|
||||
<data name="OpenAIApiKeyError" xml:space="preserve">
|
||||
<value>OpenAI request failed with status code: </value>
|
||||
</data>
|
||||
<data name="PasteActionCanceled" xml:space="preserve">
|
||||
<value>The paste operation was canceled</value>
|
||||
</data>
|
||||
<data name="PasteError" xml:space="preserve">
|
||||
<value>An error occurred during the paste operation</value>
|
||||
</data>
|
||||
@@ -188,7 +191,19 @@
|
||||
</data>
|
||||
<data name="PasteAsHtmlFile" xml:space="preserve">
|
||||
<value>Paste as .html file</value>
|
||||
</data>
|
||||
<data name="TranscodeToMp3" xml:space="preserve">
|
||||
<value>Transcode to .mp3</value>
|
||||
</data>
|
||||
<data name="TranscodeToMp4" xml:space="preserve">
|
||||
<value>Transcode to .mp4 (H.264/AAC)</value>
|
||||
</data>
|
||||
<data name="TranscodeErrorGeneral" xml:space="preserve">
|
||||
<value>An error occurred while transcoding media file</value>
|
||||
</data>
|
||||
<data name="TranscodeErrorUnsupportedCodec" xml:space="preserve">
|
||||
<value>The media file contains an unsupported codec</value>
|
||||
</data>
|
||||
<data name="PasteButtonAutomation.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Paste</value>
|
||||
</data>
|
||||
@@ -207,6 +222,9 @@
|
||||
<data name="SendBtnToolTip.Text" xml:space="preserve">
|
||||
<value>Generate and paste data</value>
|
||||
</data>
|
||||
<data name="CancelBtnToolTip.Text" xml:space="preserve">
|
||||
<value>Cancel paste operation</value>
|
||||
</data>
|
||||
<data name="RegenerateBtnToolTip.Text" xml:space="preserve">
|
||||
<value>Regenerate</value>
|
||||
</data>
|
||||
@@ -216,6 +234,9 @@
|
||||
<data name="SendButtonAutomation.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Generate and paste data</value>
|
||||
</data>
|
||||
<data name="CancelBtnAutomation.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Cancel paste operation</value>
|
||||
</data>
|
||||
<data name="SettingsBtn.Content" xml:space="preserve">
|
||||
<value>Open settings</value>
|
||||
</data>
|
||||
|
||||
@@ -8,6 +8,8 @@ using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO.Abstractions;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using AdvancedPaste.Helpers;
|
||||
@@ -29,7 +31,7 @@ using DispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue;
|
||||
|
||||
namespace AdvancedPaste.ViewModels
|
||||
{
|
||||
public sealed partial class OptionsViewModel : ObservableObject, IDisposable
|
||||
public sealed partial class OptionsViewModel : ObservableObject, IProgress<double>, IDisposable
|
||||
{
|
||||
private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
||||
private readonly DispatcherTimer _clipboardTimer;
|
||||
@@ -37,6 +39,8 @@ namespace AdvancedPaste.ViewModels
|
||||
private readonly IPasteFormatExecutor _pasteFormatExecutor;
|
||||
private readonly IAICredentialsProvider _aiCredentialsProvider;
|
||||
|
||||
private CancellationTokenSource _pasteActionCancellationTokenSource;
|
||||
|
||||
public DataPackageView ClipboardData { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
@@ -65,7 +69,11 @@ namespace AdvancedPaste.ViewModels
|
||||
private bool _pasteFormatsDirty;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _busy;
|
||||
private bool _isBusy;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(HasIndeterminateTransformProgress))]
|
||||
private double _transformProgress = double.NaN;
|
||||
|
||||
public ObservableCollection<PasteFormat> StandardPasteFormats { get; } = [];
|
||||
|
||||
@@ -81,9 +89,24 @@ namespace AdvancedPaste.ViewModels
|
||||
|
||||
public bool ClipboardHasDataForCustomAI => PasteFormat.SupportsClipboardFormats(CustomAIFormat, AvailableClipboardFormats);
|
||||
|
||||
public bool HasIndeterminateTransformProgress => double.IsNaN(TransformProgress);
|
||||
|
||||
private PasteFormats CustomAIFormat => _userSettings.IsAdvancedAIEnabled ? PasteFormats.KernelQuery : PasteFormats.CustomTextTransformation;
|
||||
|
||||
private bool Visible => GetMainWindow()?.Visible is true;
|
||||
private bool Visible
|
||||
{
|
||||
get
|
||||
{
|
||||
try
|
||||
{
|
||||
return GetMainWindow()?.Visible is true;
|
||||
}
|
||||
catch (COMException)
|
||||
{
|
||||
return false; // window is closed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public event EventHandler PreviewRequested;
|
||||
|
||||
@@ -189,7 +212,12 @@ namespace AdvancedPaste.ViewModels
|
||||
|
||||
void UpdateFormats(ObservableCollection<PasteFormat> collection, IEnumerable<PasteFormat> pasteFormats)
|
||||
{
|
||||
collection.Clear();
|
||||
// Hack: Clear collection via repeated RemoveAt to avoid this crash, which seems to occasionally occur when using Clear:
|
||||
// https://github.com/microsoft/microsoft-ui-xaml/issues/8684
|
||||
while (collection.Count > 0)
|
||||
{
|
||||
collection.RemoveAt(collection.Count - 1);
|
||||
}
|
||||
|
||||
foreach (var format in FilterAndSort(pasteFormats))
|
||||
{
|
||||
@@ -214,12 +242,13 @@ namespace AdvancedPaste.ViewModels
|
||||
public void Dispose()
|
||||
{
|
||||
_clipboardTimer.Stop();
|
||||
_pasteActionCancellationTokenSource?.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
public async Task ReadClipboardAsync()
|
||||
{
|
||||
if (Busy)
|
||||
if (IsBusy)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -324,6 +353,10 @@ namespace AdvancedPaste.ViewModels
|
||||
{
|
||||
await ClipboardHelper.TryCopyPasteAsync(package, HideWindow);
|
||||
Query = string.Empty;
|
||||
|
||||
// Delete any temp files created. A delay is needed to ensure the file is not in use by the target application -
|
||||
// for example, when pasting onto File Explorer, the paste operation will trigger a file copy.
|
||||
_ = Task.Run(() => package.GetView().TryCleanupAfterDelayAsync(TimeSpan.FromSeconds(30)));
|
||||
}
|
||||
|
||||
// Command to select the previous custom format
|
||||
@@ -362,7 +395,7 @@ namespace AdvancedPaste.ViewModels
|
||||
|
||||
internal async Task ExecutePasteFormatAsync(PasteFormat pasteFormat, PasteActionSource source)
|
||||
{
|
||||
if (Busy)
|
||||
if (IsBusy)
|
||||
{
|
||||
Logger.LogWarning($"Execution of {pasteFormat.Format} from {source} suppressed as busy");
|
||||
return;
|
||||
@@ -377,16 +410,18 @@ namespace AdvancedPaste.ViewModels
|
||||
var elapsedWatch = Stopwatch.StartNew();
|
||||
Logger.LogDebug($"Started executing {pasteFormat.Format} from source {source}");
|
||||
|
||||
Busy = true;
|
||||
IsBusy = true;
|
||||
_pasteActionCancellationTokenSource = new();
|
||||
TransformProgress = double.NaN;
|
||||
PasteActionError = PasteActionError.None;
|
||||
Query = pasteFormat.Query;
|
||||
|
||||
try
|
||||
{
|
||||
// Minimum time to show busy spinner for AI actions when triggered by global keyboard shortcut.
|
||||
var aiActionMinTaskTime = TimeSpan.FromSeconds(2);
|
||||
var aiActionMinTaskTime = TimeSpan.FromSeconds(1.5);
|
||||
var delayTask = (Visible && source == PasteActionSource.GlobalKeyboardShortcut) ? Task.Delay(aiActionMinTaskTime) : Task.CompletedTask;
|
||||
var dataPackage = await _pasteFormatExecutor.ExecutePasteFormatAsync(pasteFormat, source);
|
||||
var dataPackage = await _pasteFormatExecutor.ExecutePasteFormatAsync(pasteFormat, source, _pasteActionCancellationTokenSource.Token, this);
|
||||
|
||||
await delayTask;
|
||||
|
||||
@@ -410,7 +445,9 @@ namespace AdvancedPaste.ViewModels
|
||||
PasteActionError = PasteActionError.FromException(ex);
|
||||
}
|
||||
|
||||
Busy = false;
|
||||
IsBusy = false;
|
||||
_pasteActionCancellationTokenSource?.Dispose();
|
||||
_pasteActionCancellationTokenSource = null;
|
||||
elapsedWatch.Stop();
|
||||
Logger.LogDebug($"Finished executing {pasteFormat.Format} from source {source}; timeTakenMs={elapsedWatch.ElapsedMilliseconds}");
|
||||
}
|
||||
@@ -484,5 +521,26 @@ namespace AdvancedPaste.ViewModels
|
||||
|
||||
return IsAllowedByGPO && _aiCredentialsProvider.Refresh();
|
||||
}
|
||||
|
||||
public async Task CancelPasteActionAsync()
|
||||
{
|
||||
if (_pasteActionCancellationTokenSource != null)
|
||||
{
|
||||
await _pasteActionCancellationTokenSource.CancelAsync();
|
||||
}
|
||||
}
|
||||
|
||||
void IProgress<double>.Report(double value)
|
||||
{
|
||||
ReportProgress(value);
|
||||
}
|
||||
|
||||
private void ReportProgress(double value)
|
||||
{
|
||||
_dispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
TransformProgress = value;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,6 +88,8 @@ void Trace::AdvancedPaste_SettingsTelemetry(const PowertoyModuleIface::Hotkey& p
|
||||
TraceLoggingWideString(getAdditionalActionHotkeyCStr(L"ImageToText"), "ImageToTextHotkey"),
|
||||
TraceLoggingWideString(getAdditionalActionHotkeyCStr(L"PasteAsTxtFile"), "PasteAsTxtFileHotkey"),
|
||||
TraceLoggingWideString(getAdditionalActionHotkeyCStr(L"PasteAsPngFile"), "PasteAsPngFileHotkey"),
|
||||
TraceLoggingWideString(getAdditionalActionHotkeyCStr(L"PasteAsHtmlFile"), "PasteAsHtmlFileHotkey")
|
||||
TraceLoggingWideString(getAdditionalActionHotkeyCStr(L"PasteAsHtmlFile"), "PasteAsHtmlFileHotkey"),
|
||||
TraceLoggingWideString(getAdditionalActionHotkeyCStr(L"TranscodeToMp3"), "TranscodeToMp3Hotkey"),
|
||||
TraceLoggingWideString(getAdditionalActionHotkeyCStr(L"TranscodeToMp4"), "TranscodeToMp4Hotkey")
|
||||
);
|
||||
}
|
||||
|
||||
35
src/modules/Hosts/Hosts.FuzzTests/Fuzz.md
Normal file
35
src/modules/Hosts/Hosts.FuzzTests/Fuzz.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Create Fuzzing Tests in your .NET Code Project
|
||||
|
||||
This document provides a step-by-step guide for integrating fuzzing tests into your .NET project.
|
||||
|
||||
### Step1: Add a Fuzzing Test Project
|
||||
Create a new test project within your module folder. Ensure the project name follows the format *.FuzzTests*.
|
||||
|
||||
### step2: Add FuzzTests and OneFuzzConfig.json to your fuzzing test project
|
||||
Follow the instructions in [Fuzz.md](https://github.com/microsoft/PowerToys/blob/main/src/modules/AdvancedPaste/AdvancedPaste.FuzzTests/Fuzz.md) from AdvancedPaste.FuzzTests to properly integrate fuzzing tests into your project.
|
||||
|
||||
Configuring **OneFuzzConfig.json**:
|
||||
1. Update the dll, class, method, and FuzzingTargetBinaries field in the fuzzers list.
|
||||
2. Modify the AssignedTo field in the adoTemplate list.
|
||||
3. Set the jobNotificationEmail to your Microsoft email account.
|
||||
4. Update the projectName and targetName fields in the oneFuzzJobs list.
|
||||
5. Define job dependencies in the following directory:
|
||||
Example:
|
||||
```PowerToys\x64\Debug\tests\Hosts.FuzzTests\net8.0-windows10.0.19041.0```
|
||||
|
||||
|
||||
# step3: Configure the OneFuzz Pipeline
|
||||
Modify the patterns in the job steps within [job-fuzz.yml](https://github.com/microsoft/PowerToys/blob/main/.pipelines/v2/templates/job-fuzz.yml) to match your fuzzing project name.
|
||||
|
||||
Example:
|
||||
```
|
||||
- download: current
|
||||
displayName: Download artifacts
|
||||
artifact: $(ArtifactName)
|
||||
patterns: |-
|
||||
**/tests/Hosts.FuzzTests/**
|
||||
```
|
||||
|
||||
|
||||
# step4: Submit OneFuzz Pipeline and Verify Results on the OneFuzz Platform
|
||||
After executing the tests, check your email for the job link. Click the link to review the fuzzing test results.
|
||||
101
src/modules/Hosts/Hosts.FuzzTests/FuzzTests.cs
Normal file
101
src/modules/Hosts/Hosts.FuzzTests/FuzzTests.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
// 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.IO;
|
||||
using System.IO.Abstractions.TestingHelpers;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Hosts.Tests.Mocks;
|
||||
using HostsUILib.Helpers;
|
||||
using HostsUILib.Models;
|
||||
using HostsUILib.Settings;
|
||||
using Moq;
|
||||
|
||||
namespace Hosts.FuzzTests
|
||||
{
|
||||
public class FuzzTests
|
||||
{
|
||||
private static Mock<IUserSettings> _userSettings;
|
||||
private static Mock<IElevationHelper> _elevationHelper;
|
||||
|
||||
// Case1: Fuzzing method for ValidIPv4
|
||||
public static void FuzzValidIPv4(ReadOnlySpan<byte> input)
|
||||
{
|
||||
try
|
||||
{
|
||||
string address = System.Text.Encoding.UTF8.GetString(input);
|
||||
bool isValid = ValidationHelper.ValidIPv4(address);
|
||||
}
|
||||
catch (Exception ex) when (ex is RegexMatchTimeoutException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
// Case2: fuzzing method for ValidIPv6
|
||||
public static void FuzzValidIPv6(ReadOnlySpan<byte> input)
|
||||
{
|
||||
try
|
||||
{
|
||||
string address = System.Text.Encoding.UTF8.GetString(input);
|
||||
bool isValid = ValidationHelper.ValidIPv6(address);
|
||||
}
|
||||
catch (Exception ex) when (ex is RegexMatchTimeoutException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
// Case3: fuzzing method for ValidHosts
|
||||
public static void FuzzValidHosts(ReadOnlySpan<byte> input)
|
||||
{
|
||||
try
|
||||
{
|
||||
string hosts = System.Text.Encoding.UTF8.GetString(input);
|
||||
bool isValid = ValidationHelper.ValidHosts(hosts, true);
|
||||
}
|
||||
catch (Exception ex) when (ex is RegexMatchTimeoutException)
|
||||
{
|
||||
// It's important to filter out any *expected* exceptions from our code here.
|
||||
// However, catching all exceptions is considered an anti-pattern because it may suppress legitimate
|
||||
// issues, such as a NullReferenceException thrown by our code. In this case, we still re-throw
|
||||
// the exception, as the ToJsonFromXmlOrCsvAsync method is not expected to throw any exceptions.
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public static void FuzzWriteAsync(ReadOnlySpan<byte> data)
|
||||
{
|
||||
try
|
||||
{
|
||||
_userSettings = new Mock<IUserSettings>();
|
||||
_elevationHelper = new Mock<IElevationHelper>();
|
||||
_elevationHelper.Setup(m => m.IsElevated).Returns(true);
|
||||
|
||||
var fileSystem = new CustomMockFileSystem();
|
||||
var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object);
|
||||
|
||||
string input = System.Text.Encoding.UTF8.GetString(data);
|
||||
|
||||
// Since the WriteAsync method does not involve content parsing, we won't fuzz the additionalLines in the hosts file.
|
||||
string additionalLines = " ";
|
||||
string hosts = input;
|
||||
string address = input;
|
||||
string comments = input;
|
||||
var entries = new List<Entry>
|
||||
{
|
||||
new Entry(1, hosts, address, comments, true),
|
||||
};
|
||||
|
||||
// fuzzing WriteAsync
|
||||
_ = Task.Run(async () => await service.WriteAsync(additionalLines, entries));
|
||||
}
|
||||
catch (Exception ex) when (ex is ArgumentException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
51
src/modules/Hosts/Hosts.FuzzTests/Hosts.FuzzTests.csproj
Normal file
51
src/modules/Hosts/Hosts.FuzzTests/Hosts.FuzzTests.csproj
Normal file
@@ -0,0 +1,51 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputPath>..\..\..\..\$(Platform)\$(Configuration)\tests\Hosts.FuzzTests\</OutputPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="..\Hosts.Tests\Mocks\CustomMockFileSystem.cs" Link="CustomMockFileSystem.cs" />
|
||||
<Compile Include="..\Hosts.Tests\Mocks\MockFileSystemWatcher.cs" Link="MockFileSystemWatcher.cs" />
|
||||
<Compile Include="..\Hosts.Tests\Mocks\MockFileSystemWatcherFactory.cs" Link="MockFileSystemWatcherFactory.cs" />
|
||||
<Compile Include="..\HostsUILib\Consts.cs" Link="Consts.cs" />
|
||||
<Compile Include="..\HostsUILib\Helpers\ValidationHelper.cs" Link="ValidationHelper.cs" />
|
||||
<Compile Include="..\HostsUILib\Exceptions\NotRunningElevatedException.cs" Link="NotRunningElevatedException.cs" />
|
||||
<Compile Include="..\HostsUILib\Exceptions\ReadOnlyHostsException.cs" Link="ReadOnlyHostsException.cs" />
|
||||
<Compile Include="..\HostsUILib\Helpers\HostsService.cs" Link="HostsService.cs" />
|
||||
<Compile Include="..\HostsUILib\Helpers\IElevationHelper.cs" Link="IElevationHelper.cs" />
|
||||
<Compile Include="..\HostsUILib\Helpers\IHostsService.cs" Link="IHostsService.cs" />
|
||||
<Compile Include="..\HostsUILib\Helpers\ILogger.cs" Link="ILogger.cs" />
|
||||
<Compile Include="..\HostsUILib\Helpers\LoggerInstance.cs" Link="LoggerInstance.cs" />
|
||||
<Compile Include="..\HostsUILib\Models\AddressType.cs" Link="AddressType.cs" />
|
||||
<Compile Include="..\HostsUILib\Models\Entry.cs" Link="Entry.cs" />
|
||||
<Compile Include="..\HostsUILib\Models\HostsData.cs" Link="HostsData.cs" />
|
||||
<Compile Include="..\HostsUILib\Settings\HostsAdditionalLinesPosition.cs" Link="HostsAdditionalLinesPosition.cs" />
|
||||
<Compile Include="..\HostsUILib\Settings\HostsEncoding.cs" Link="HostsEncoding.cs" />
|
||||
<Compile Include="..\HostsUILib\Settings\IUserSettings.cs" Link="IUserSettings.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Moq" />
|
||||
<PackageReference Include="MSTest" />
|
||||
<PackageReference Include="System.IO.Abstractions" />
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" />
|
||||
<PackageReference Include="System.IO.Abstractions.TestingHelpers" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Microsoft.VisualStudio.TestTools.UnitTesting" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="OneFuzzConfig.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
5
src/modules/Hosts/Hosts.FuzzTests/MSTestSettings.cs
Normal file
5
src/modules/Hosts/Hosts.FuzzTests/MSTestSettings.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
// 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.
|
||||
|
||||
[assembly: Parallelize(Scope = ExecutionScope.MethodLevel)]
|
||||
178
src/modules/Hosts/Hosts.FuzzTests/OneFuzzConfig.json
Normal file
178
src/modules/Hosts/Hosts.FuzzTests/OneFuzzConfig.json
Normal file
@@ -0,0 +1,178 @@
|
||||
{
|
||||
"configVersion": 3,
|
||||
"entries": [
|
||||
{
|
||||
"fuzzer": {
|
||||
"$type": "libfuzzerDotNet",
|
||||
"dll": "Hosts.FuzzTests.dll",
|
||||
"class": "Hosts.FuzzTests.FuzzTests",
|
||||
"method": "FuzzValidIPv4",
|
||||
"FuzzingTargetBinaries": [
|
||||
"PowerToys.Hosts.dll"
|
||||
]
|
||||
},
|
||||
"adoTemplate": {
|
||||
// supply the values appropriate to your
|
||||
// project, where bugs will be filed
|
||||
"org": "microsoft",
|
||||
"project": "OS",
|
||||
"AssignedTo": "mengyuanchen@microsoft.com",
|
||||
"AreaPath": "OS\\Windows Client and Services\\WinPD\\DEEP-Developer Experience, Ecosystem and Partnerships\\SHINE\\PowerToys",
|
||||
"IterationPath": "OS\\Future"
|
||||
},
|
||||
"jobNotificationEmail": "mengyuanchen@microsoft.com",
|
||||
"skip": false,
|
||||
"rebootAfterSetup": false,
|
||||
"oneFuzzJobs": [
|
||||
// at least one job is required
|
||||
{
|
||||
"projectName": "Hosts",
|
||||
"targetName": "Hosts-dotnet-fuzzer-Ipv4"
|
||||
}
|
||||
],
|
||||
"jobDependencies": [
|
||||
// this should contain, at minimum,
|
||||
// the DLL and PDB files
|
||||
// you will need to add any other files required
|
||||
// (globs are supported)
|
||||
"Hosts.FuzzTests.dll",
|
||||
"Hosts.FuzzTests.pdb",
|
||||
"Microsoft.Windows.SDK.NET.dll",
|
||||
"WinRT.Runtime.dll"
|
||||
],
|
||||
"SdlWorkItemId": 49911822
|
||||
},
|
||||
{
|
||||
"fuzzer": {
|
||||
"$type": "libfuzzerDotNet",
|
||||
"dll": "Hosts.FuzzTests.dll",
|
||||
"class": "Hosts.FuzzTests.FuzzTests",
|
||||
"method": "FuzzValidIPv6",
|
||||
"FuzzingTargetBinaries": [
|
||||
"PowerToys.Hosts.dll"
|
||||
]
|
||||
},
|
||||
"adoTemplate": {
|
||||
// supply the values appropriate to your
|
||||
// project, where bugs will be filed
|
||||
"org": "microsoft",
|
||||
"project": "OS",
|
||||
"AssignedTo": "mengyuanchen@microsoft.com",
|
||||
"AreaPath": "OS\\Windows Client and Services\\WinPD\\DEEP-Developer Experience, Ecosystem and Partnerships\\SHINE\\PowerToys",
|
||||
"IterationPath": "OS\\Future"
|
||||
},
|
||||
"jobNotificationEmail": "mengyuanchen@microsoft.com",
|
||||
"skip": false,
|
||||
"rebootAfterSetup": false,
|
||||
"oneFuzzJobs": [
|
||||
// at least one job is required
|
||||
{
|
||||
"projectName": "Hosts",
|
||||
"targetName": "Hosts-dotnet-fuzzer-Ipv6"
|
||||
}
|
||||
],
|
||||
"jobDependencies": [
|
||||
// this should contain, at minimum,
|
||||
// the DLL and PDB files
|
||||
// you will need to add any other files required
|
||||
// (globs are supported)
|
||||
"Hosts.FuzzTests.dll",
|
||||
"Hosts.FuzzTests.pdb",
|
||||
"Microsoft.Windows.SDK.NET.dll",
|
||||
"WinRT.Runtime.dll"
|
||||
],
|
||||
"SdlWorkItemId": 49911822
|
||||
},
|
||||
{
|
||||
"fuzzer": {
|
||||
"$type": "libfuzzerDotNet",
|
||||
"dll": "Hosts.FuzzTests.dll",
|
||||
"class": "Hosts.FuzzTests.FuzzTests",
|
||||
"method": "FuzzValidHosts",
|
||||
"FuzzingTargetBinaries": [
|
||||
"PowerToys.Hosts.dll"
|
||||
]
|
||||
},
|
||||
"adoTemplate": {
|
||||
// supply the values appropriate to your
|
||||
// project, where bugs will be filed
|
||||
"org": "microsoft",
|
||||
"project": "OS",
|
||||
"AssignedTo": "mengyuanchen@microsoft.com",
|
||||
"AreaPath": "OS\\Windows Client and Services\\WinPD\\DEEP-Developer Experience, Ecosystem and Partnerships\\SHINE\\PowerToys",
|
||||
"IterationPath": "OS\\Future"
|
||||
},
|
||||
"jobNotificationEmail": "mengyuanchen@microsoft.com",
|
||||
"skip": false,
|
||||
"rebootAfterSetup": false,
|
||||
"oneFuzzJobs": [
|
||||
// at least one job is required
|
||||
{
|
||||
"projectName": "Hosts",
|
||||
"targetName": "Hosts-dotnet-fuzzer-hosts"
|
||||
}
|
||||
],
|
||||
"jobDependencies": [
|
||||
// this should contain, at minimum,
|
||||
// the DLL and PDB files
|
||||
// you will need to add any other files required
|
||||
// (globs are supported)
|
||||
"Hosts.FuzzTests.dll",
|
||||
"Hosts.FuzzTests.pdb",
|
||||
"Microsoft.Windows.SDK.NET.dll",
|
||||
"WinRT.Runtime.dll"
|
||||
],
|
||||
"SdlWorkItemId": 49911822
|
||||
},
|
||||
{
|
||||
"fuzzer": {
|
||||
"$type": "libfuzzerDotNet",
|
||||
"dll": "Hosts.FuzzTests.dll",
|
||||
"class": "Hosts.FuzzTests.FuzzTests",
|
||||
"method": "FuzzWriteAsync",
|
||||
"FuzzingTargetBinaries": [
|
||||
"PowerToys.Hosts.dll"
|
||||
]
|
||||
},
|
||||
"adoTemplate": {
|
||||
// supply the values appropriate to your
|
||||
// project, where bugs will be filed
|
||||
"org": "microsoft",
|
||||
"project": "OS",
|
||||
"AssignedTo": "mengyuanchen@microsoft.com",
|
||||
"AreaPath": "OS\\Windows Client and Services\\WinPD\\DEEP-Developer Experience, Ecosystem and Partnerships\\SHINE\\PowerToys",
|
||||
"IterationPath": "OS\\Future"
|
||||
},
|
||||
"jobNotificationEmail": "mengyuanchen@microsoft.com",
|
||||
"skip": false,
|
||||
"rebootAfterSetup": false,
|
||||
"oneFuzzJobs": [
|
||||
// at least one job is required
|
||||
{
|
||||
"projectName": "Hosts",
|
||||
"targetName": "Hosts-dotnet-fuzzer-WriteAsync"
|
||||
}
|
||||
],
|
||||
"jobDependencies": [
|
||||
// this should contain, at minimum,
|
||||
// the DLL and PDB files
|
||||
// you will need to add any other files required
|
||||
// (globs are supported)
|
||||
"Hosts.FuzzTests.dll",
|
||||
"Hosts.FuzzTests.pdb",
|
||||
"Microsoft.Windows.SDK.NET.dll",
|
||||
"WinRT.Runtime.dll",
|
||||
"Moq.dll",
|
||||
"testhost.dll",
|
||||
"Castle.Core.dll",
|
||||
"System.IO.Abstractions.dll",
|
||||
"CommunityToolkit.Mvvm.dll",
|
||||
"System.IO.Abstractions.TestingHelpers.dll",
|
||||
"TestableIO.System.IO.Abstractions.dll",
|
||||
"TestableIO.System.IO.Abstractions.TestingHelpers.dll",
|
||||
"TestableIO.System.IO.Abstractions.Wrappers.dll"
|
||||
],
|
||||
"SdlWorkItemId": 49911822
|
||||
}
|
||||
]
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user