mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-01-10 14:26:47 +01:00
Compare commits
35 Commits
dev/vanzue
...
leilzh/reg
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5386572092 | ||
|
|
0f1ccaee2d | ||
|
|
68b708f2fd | ||
|
|
3088908202 | ||
|
|
a5b9a38517 | ||
|
|
7a3616e996 | ||
|
|
b36530bf87 | ||
|
|
e260c01553 | ||
|
|
7f4a97cac5 | ||
|
|
ab76dd1255 | ||
|
|
911989bac1 | ||
|
|
c690cb1bb8 | ||
|
|
c23dcb0c5a | ||
|
|
3682f186e3 | ||
|
|
1eae1d9a12 | ||
|
|
bc134b344b | ||
|
|
04b8234192 | ||
|
|
d72e0ab20d | ||
|
|
062234c295 | ||
|
|
0d4f3d851e | ||
|
|
e93b044f39 | ||
|
|
fed6e523b6 | ||
|
|
0997c1a013 | ||
|
|
fa55cdb67f | ||
|
|
a889f4d4bd | ||
|
|
281c88a620 | ||
|
|
7bcddfeb09 | ||
|
|
7fb4ac2dcd | ||
|
|
c91bef1517 | ||
|
|
fdd1f47d85 | ||
|
|
9a998b2056 | ||
|
|
d26ef36e31 | ||
|
|
ee6336c47d | ||
|
|
46d380c2b6 | ||
|
|
decb947283 |
2
.github/actions/spell-check/expect.txt
vendored
2
.github/actions/spell-check/expect.txt
vendored
@@ -1440,6 +1440,7 @@ secpol
|
||||
securestring
|
||||
SEEMASKINVOKEIDLIST
|
||||
SELCHANGE
|
||||
selfhost
|
||||
SENDCHANGE
|
||||
sendvirtualinput
|
||||
serverside
|
||||
@@ -1879,6 +1880,7 @@ winexe
|
||||
winforms
|
||||
winget
|
||||
wingetcreate
|
||||
wingetpkgs
|
||||
Winhook
|
||||
WINL
|
||||
winlogon
|
||||
|
||||
2
.github/workflows/msstore-submissions.yml
vendored
2
.github/workflows/msstore-submissions.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
steps:
|
||||
- name: BODGY - Set up Gnome Keyring for future Cert Auth
|
||||
run: |-
|
||||
sudo apt-get install -y gnome-keyring
|
||||
sudo apt-get update && sudo apt-get install -y gnome-keyring
|
||||
export $(dbus-launch --sh-syntax)
|
||||
export $(echo 'anypass_just_to_unlock' | gnome-keyring-daemon --unlock)
|
||||
export $(echo 'anypass_just_to_unlock' | gnome-keyring-daemon --start --components=gpg,pkcs11,secrets,ssh)
|
||||
|
||||
@@ -123,7 +123,7 @@ jobs:
|
||||
displayName: Stage UI Test Build Outputs
|
||||
inputs:
|
||||
sourceFolder: '$(Build.SourcesDirectory)'
|
||||
contents: '$(BuildPlatform)/$(BuildConfiguration)/**/*'
|
||||
contents: '**/$(BuildPlatform)/$(BuildConfiguration)/tests/**/*'
|
||||
targetFolder: '$(JobOutputDirectory)\$(BuildPlatform)\$(BuildConfiguration)'
|
||||
|
||||
- publish: $(JobOutputDirectory)
|
||||
|
||||
@@ -11,12 +11,14 @@ parameters:
|
||||
- name: useLatestWebView2
|
||||
type: boolean
|
||||
default: false
|
||||
- name: useLatestOfficialBuild
|
||||
type: boolean
|
||||
default: true
|
||||
- name: useCurrentBranchBuild
|
||||
type: boolean
|
||||
default: false
|
||||
- name: buildSource
|
||||
type: string
|
||||
default: "latestMainOfficialBuild"
|
||||
displayName: "Build Source"
|
||||
- name: specificBuildId
|
||||
type: string
|
||||
default: "xxxx"
|
||||
displayName: "Build ID (for specific builds)"
|
||||
- name: uiTestModules
|
||||
type: object
|
||||
default: []
|
||||
@@ -113,16 +115,17 @@ jobs:
|
||||
& '$(build.sourcesdirectory)\.pipelines\InstallWinAppDriver.ps1'
|
||||
displayName: Download and install WinAppDriver
|
||||
|
||||
- ${{ if eq(parameters.useLatestOfficialBuild, true) }}:
|
||||
- ${{ if ne(parameters.buildSource, 'buildNow') }}:
|
||||
- task: DownloadPipelineArtifact@2
|
||||
inputs:
|
||||
buildType: 'specific'
|
||||
project: 'Dart'
|
||||
definition: '76541'
|
||||
buildVersionToDownload: 'latestFromBranch'
|
||||
${{ if eq(parameters.useCurrentBranchBuild, true) }}:
|
||||
branchName: '$(Build.SourceBranch)'
|
||||
${{ if eq(parameters.buildSource, 'specificBuildId') }}:
|
||||
buildVersionToDownload: 'specific'
|
||||
buildId: '${{ parameters.specificBuildId }}'
|
||||
${{ else }}:
|
||||
buildVersionToDownload: 'latestFromBranch'
|
||||
branchName: 'refs/heads/main'
|
||||
artifactName: 'build-$(BuildPlatform)-Release'
|
||||
targetPath: '$(Build.ArtifactStagingDirectory)'
|
||||
@@ -133,7 +136,7 @@ jobs:
|
||||
patterns: |
|
||||
**/PowerToysSetup*.exe
|
||||
|
||||
- ${{ if eq(parameters.useLatestOfficialBuild, true) }}:
|
||||
- ${{ if ne(parameters.buildSource, 'buildNow') }}:
|
||||
- ${{ if eq(parameters.installMode, 'peruser') }}:
|
||||
- pwsh: |-
|
||||
& "$(build.sourcesdirectory)\.pipelines\installPowerToys.ps1" -InstallMode "PerUser"
|
||||
@@ -169,7 +172,7 @@ jobs:
|
||||
!**\UITests-FancyZones\**\UITests-FancyZonesEditor.dll
|
||||
env:
|
||||
platform: '$(TestPlatform)'
|
||||
useInstallerForTest: ${{ parameters.useLatestOfficialBuild }}
|
||||
useInstallerForTest: ${{ ne(parameters.buildSource, 'buildNow') }}
|
||||
|
||||
- ${{ if ne(length(parameters.uiTestModules), 0) }}:
|
||||
- ${{ each module in parameters.uiTestModules }}:
|
||||
@@ -191,4 +194,4 @@ jobs:
|
||||
!**\UITests-FancyZones\**\UITests-FancyZonesEditor.dll
|
||||
env:
|
||||
platform: '$(TestPlatform)'
|
||||
useInstallerForTest: ${{ parameters.useLatestOfficialBuild }}
|
||||
useInstallerForTest: ${{ ne(parameters.buildSource, 'buildNow') }}
|
||||
|
||||
@@ -19,155 +19,40 @@ parameters:
|
||||
- name: useLatestWebView2
|
||||
type: boolean
|
||||
default: false
|
||||
- name: useLatestOfficialBuild
|
||||
type: boolean
|
||||
default: true
|
||||
- name: testBothInstallModes
|
||||
type: boolean
|
||||
default: true
|
||||
- name: useCurrentBranchBuild
|
||||
type: boolean
|
||||
default: false
|
||||
- name: buildSource
|
||||
type: string
|
||||
default: "latestMainOfficialBuild"
|
||||
displayName: "Build Source"
|
||||
values:
|
||||
- latestMainOfficialBuild
|
||||
- buildNow
|
||||
- specificBuildId
|
||||
- name: specificBuildId
|
||||
type: string
|
||||
default: 'xxxx'
|
||||
displayName: "Build ID (only used when Build Source = specificBuildId)"
|
||||
- name: uiTestModules
|
||||
type: object
|
||||
default: []
|
||||
|
||||
stages:
|
||||
- ${{ each platform in parameters.buildPlatforms }}:
|
||||
- ${{ if eq(parameters.useLatestOfficialBuild, false) }}:
|
||||
- stage: Build_${{ platform }}
|
||||
displayName: Build ${{ platform }}
|
||||
dependsOn: []
|
||||
jobs:
|
||||
- template: job-build-project.yml
|
||||
parameters:
|
||||
pool:
|
||||
${{ if eq(variables['System.CollectionId'], 'cb55739e-4afe-46a3-970f-1b49d8ee7564') }}:
|
||||
name: SHINE-INT-L
|
||||
${{ else }}:
|
||||
name: SHINE-OSS-L
|
||||
${{ if eq(parameters.useVSPreview, true) }}:
|
||||
demands: ImageOverride -equals SHINE-VS17-Preview
|
||||
buildPlatforms:
|
||||
- ${{ platform }}
|
||||
buildConfigurations: [Release]
|
||||
enablePackageCaching: true
|
||||
enableMsBuildCaching: ${{ parameters.enableMsBuildCaching }}
|
||||
runTests: false
|
||||
buildTests: true
|
||||
useVSPreview: ${{ parameters.useVSPreview }}
|
||||
timeoutInMinutes: 90
|
||||
# Full build path: build PowerToys + UI tests + run tests
|
||||
- ${{ if eq(parameters.buildSource, 'buildNow') }}:
|
||||
- template: pipeline-ui-tests-full-build.yml
|
||||
parameters:
|
||||
platform: ${{ platform }}
|
||||
enableMsBuildCaching: ${{ parameters.enableMsBuildCaching }}
|
||||
useVSPreview: ${{ parameters.useVSPreview }}
|
||||
useLatestWebView2: ${{ parameters.useLatestWebView2 }}
|
||||
uiTestModules: ${{ parameters.uiTestModules }}
|
||||
|
||||
- ${{ if eq(parameters.useLatestOfficialBuild, true) }}:
|
||||
- stage: BuildUITests_${{ platform }}
|
||||
displayName: Build UI Tests Only
|
||||
dependsOn: []
|
||||
jobs:
|
||||
- template: job-build-ui-tests.yml
|
||||
parameters:
|
||||
pool:
|
||||
${{ if eq(variables['System.CollectionId'], 'cb55739e-4afe-46a3-970f-1b49d8ee7564') }}:
|
||||
name: SHINE-INT-L
|
||||
${{ else }}:
|
||||
name: SHINE-OSS-L
|
||||
${{ if eq(parameters.useVSPreview, true) }}:
|
||||
demands: ImageOverride -equals SHINE-VS17-Preview
|
||||
buildPlatforms:
|
||||
- ${{ platform }}
|
||||
uiTestModules: ${{ parameters.uiTestModules }}
|
||||
|
||||
- ${{ if eq(platform, 'x64') }}:
|
||||
- stage: Test_x64Win10
|
||||
displayName: Test x64Win10
|
||||
${{ if eq(parameters.useLatestOfficialBuild, true) }}:
|
||||
dependsOn:
|
||||
- BuildUITests_${{ platform }}
|
||||
${{ else }}:
|
||||
dependsOn:
|
||||
- Build_${{ platform }}
|
||||
jobs:
|
||||
- template: job-test-project.yml
|
||||
parameters:
|
||||
platform: x64Win10
|
||||
configuration: Release
|
||||
useLatestWebView2: ${{ parameters.useLatestWebView2 }}
|
||||
useLatestOfficialBuild: ${{ parameters.useLatestOfficialBuild }}
|
||||
useCurrentBranchBuild: ${{ parameters.useCurrentBranchBuild }}
|
||||
uiTestModules: ${{ parameters.uiTestModules }}
|
||||
|
||||
# Additional per-user installation test (when both modes are enabled)
|
||||
- ${{ if and(eq(parameters.useLatestOfficialBuild, true), eq(parameters.testBothInstallModes, true)) }}:
|
||||
- template: job-test-project.yml
|
||||
parameters:
|
||||
platform: x64Win10
|
||||
configuration: Release
|
||||
useLatestWebView2: ${{ parameters.useLatestWebView2 }}
|
||||
useLatestOfficialBuild: ${{ parameters.useLatestOfficialBuild }}
|
||||
useCurrentBranchBuild: ${{ parameters.useCurrentBranchBuild }}
|
||||
uiTestModules: ${{ parameters.uiTestModules }}
|
||||
installMode: 'peruser'
|
||||
jobSuffix: '_PerUser'
|
||||
|
||||
- ${{ if eq(platform, 'x64') }}:
|
||||
- stage: Test_x64Win11
|
||||
displayName: Test x64Win11
|
||||
${{ if eq(parameters.useLatestOfficialBuild, true) }}:
|
||||
dependsOn:
|
||||
- BuildUITests_${{ platform }}
|
||||
${{ else }}:
|
||||
dependsOn:
|
||||
- Build_${{ platform }}
|
||||
jobs:
|
||||
- template: job-test-project.yml
|
||||
parameters:
|
||||
platform: x64Win11
|
||||
configuration: Release
|
||||
useLatestWebView2: ${{ parameters.useLatestWebView2 }}
|
||||
useLatestOfficialBuild: ${{ parameters.useLatestOfficialBuild }}
|
||||
useCurrentBranchBuild: ${{ parameters.useCurrentBranchBuild }}
|
||||
uiTestModules: ${{ parameters.uiTestModules }}
|
||||
|
||||
# Additional per-user installation test (when both modes are enabled)
|
||||
- ${{ if and(eq(parameters.useLatestOfficialBuild, true), eq(parameters.testBothInstallModes, true)) }}:
|
||||
- template: job-test-project.yml
|
||||
parameters:
|
||||
platform: x64Win11
|
||||
configuration: Release
|
||||
useLatestWebView2: ${{ parameters.useLatestWebView2 }}
|
||||
useLatestOfficialBuild: ${{ parameters.useLatestOfficialBuild }}
|
||||
useCurrentBranchBuild: ${{ parameters.useCurrentBranchBuild }}
|
||||
uiTestModules: ${{ parameters.uiTestModules }}
|
||||
installMode: 'peruser'
|
||||
jobSuffix: '_PerUser'
|
||||
|
||||
- ${{ if ne(platform, 'x64') }}:
|
||||
- stage: Test_${{ platform }}
|
||||
displayName: Test ${{ platform }}
|
||||
${{ if eq(parameters.useLatestOfficialBuild, true) }}:
|
||||
dependsOn:
|
||||
- BuildUITests_${{ platform }}
|
||||
${{ else }}:
|
||||
dependsOn:
|
||||
- Build_${{ platform }}
|
||||
jobs:
|
||||
- template: job-test-project.yml
|
||||
parameters:
|
||||
platform: ${{ platform }}
|
||||
configuration: Release
|
||||
useLatestWebView2: ${{ parameters.useLatestWebView2 }}
|
||||
useLatestOfficialBuild: ${{ parameters.useLatestOfficialBuild }}
|
||||
useCurrentBranchBuild: ${{ parameters.useCurrentBranchBuild }}
|
||||
uiTestModules: ${{ parameters.uiTestModules }}
|
||||
|
||||
# Additional per-user installation test (when both modes are enabled)
|
||||
- ${{ if and(eq(parameters.useLatestOfficialBuild, true), eq(parameters.testBothInstallModes, true)) }}:
|
||||
- template: job-test-project.yml
|
||||
parameters:
|
||||
platform: ${{ platform }}
|
||||
configuration: Release
|
||||
useLatestWebView2: ${{ parameters.useLatestWebView2 }}
|
||||
useLatestOfficialBuild: ${{ parameters.useLatestOfficialBuild }}
|
||||
useCurrentBranchBuild: ${{ parameters.useCurrentBranchBuild }}
|
||||
uiTestModules: ${{ parameters.uiTestModules }}
|
||||
installMode: 'peruser'
|
||||
jobSuffix: '_PerUser'
|
||||
# Official build path: build UI tests only + download official build + run tests
|
||||
- ${{ if ne(parameters.buildSource, 'buildNow') }}:
|
||||
- template: pipeline-ui-tests-official-build.yml
|
||||
parameters:
|
||||
platform: ${{ platform }}
|
||||
buildSource: ${{ parameters.buildSource }}
|
||||
specificBuildId: ${{ parameters.specificBuildId }}
|
||||
useLatestWebView2: ${{ parameters.useLatestWebView2 }}
|
||||
uiTestModules: ${{ parameters.uiTestModules }}
|
||||
|
||||
80
.pipelines/v2/templates/pipeline-ui-tests-full-build.yml
Normal file
80
.pipelines/v2/templates/pipeline-ui-tests-full-build.yml
Normal file
@@ -0,0 +1,80 @@
|
||||
# Template for full build path: Build PowerToys + Build UI Tests + Run Tests
|
||||
parameters:
|
||||
- name: platform
|
||||
type: string
|
||||
- name: enableMsBuildCaching
|
||||
type: boolean
|
||||
default: false
|
||||
- name: useVSPreview
|
||||
type: boolean
|
||||
default: false
|
||||
- name: useLatestWebView2
|
||||
type: boolean
|
||||
default: false
|
||||
- name: uiTestModules
|
||||
type: object
|
||||
default: []
|
||||
|
||||
stages:
|
||||
# Stage 1: Build full PowerToys project
|
||||
- stage: Build_${{ parameters.platform }}
|
||||
displayName: Build PowerToys ${{ parameters.platform }}
|
||||
dependsOn: []
|
||||
jobs:
|
||||
- template: job-build-project.yml
|
||||
parameters:
|
||||
pool:
|
||||
${{ if eq(variables['System.CollectionId'], 'cb55739e-4afe-46a3-970f-1b49d8ee7564') }}:
|
||||
name: SHINE-INT-L
|
||||
${{ else }}:
|
||||
name: SHINE-OSS-L
|
||||
${{ if eq(parameters.useVSPreview, true) }}:
|
||||
demands: ImageOverride -equals SHINE-VS17-Preview
|
||||
buildPlatforms:
|
||||
- ${{ parameters.platform }}
|
||||
buildConfigurations: [Release]
|
||||
enablePackageCaching: true
|
||||
enableMsBuildCaching: ${{ parameters.enableMsBuildCaching }}
|
||||
runTests: false
|
||||
buildTests: true
|
||||
useVSPreview: ${{ parameters.useVSPreview }}
|
||||
timeoutInMinutes: 90
|
||||
|
||||
# Stage 2: Run UI Tests
|
||||
- ${{ if eq(parameters.platform, 'x64') }}:
|
||||
- stage: Test_x64Win10_FullBuild
|
||||
displayName: Test x64Win10 (Full Build)
|
||||
dependsOn: Build_${{ parameters.platform }}
|
||||
jobs:
|
||||
- template: job-test-project.yml
|
||||
parameters:
|
||||
platform: x64Win10
|
||||
configuration: Release
|
||||
useLatestWebView2: ${{ parameters.useLatestWebView2 }}
|
||||
buildSource: 'buildNow'
|
||||
uiTestModules: ${{ parameters.uiTestModules }}
|
||||
|
||||
- stage: Test_x64Win11_FullBuild
|
||||
displayName: Test x64Win11 (Full Build)
|
||||
dependsOn: Build_${{ parameters.platform }}
|
||||
jobs:
|
||||
- template: job-test-project.yml
|
||||
parameters:
|
||||
platform: x64Win11
|
||||
configuration: Release
|
||||
useLatestWebView2: ${{ parameters.useLatestWebView2 }}
|
||||
buildSource: 'buildNow'
|
||||
uiTestModules: ${{ parameters.uiTestModules }}
|
||||
|
||||
- ${{ if ne(parameters.platform, 'x64') }}:
|
||||
- stage: Test_${{ parameters.platform }}_FullBuild
|
||||
displayName: Test ${{ parameters.platform }} (Full Build)
|
||||
dependsOn: Build_${{ parameters.platform }}
|
||||
jobs:
|
||||
- template: job-test-project.yml
|
||||
parameters:
|
||||
platform: ${{ parameters.platform }}
|
||||
configuration: Release
|
||||
useLatestWebView2: ${{ parameters.useLatestWebView2 }}
|
||||
buildSource: 'buildNow'
|
||||
uiTestModules: ${{ parameters.uiTestModules }}
|
||||
110
.pipelines/v2/templates/pipeline-ui-tests-official-build.yml
Normal file
110
.pipelines/v2/templates/pipeline-ui-tests-official-build.yml
Normal file
@@ -0,0 +1,110 @@
|
||||
# Template for official build path: Download Official Build + Build UI Tests Only + Run Tests
|
||||
parameters:
|
||||
- name: platform
|
||||
type: string
|
||||
- name: buildSource
|
||||
type: string
|
||||
- name: specificBuildId
|
||||
type: string
|
||||
default: 'xxxx'
|
||||
- name: useLatestWebView2
|
||||
type: boolean
|
||||
default: false
|
||||
- name: uiTestModules
|
||||
type: object
|
||||
default: []
|
||||
|
||||
stages:
|
||||
# Stage 1: Build UI Tests Only
|
||||
- stage: BuildUITests_${{ parameters.platform }}
|
||||
displayName: Build UI Tests Only ${{ parameters.platform }}
|
||||
dependsOn: []
|
||||
jobs:
|
||||
- template: job-build-ui-tests.yml
|
||||
parameters:
|
||||
pool:
|
||||
${{ if eq(variables['System.CollectionId'], 'cb55739e-4afe-46a3-970f-1b49d8ee7564') }}:
|
||||
name: SHINE-INT-L
|
||||
${{ else }}:
|
||||
name: SHINE-OSS-L
|
||||
buildPlatforms:
|
||||
- ${{ parameters.platform }}
|
||||
uiTestModules: ${{ parameters.uiTestModules }}
|
||||
|
||||
# Stage 2: Run UI Tests with Official Build
|
||||
- ${{ if eq(parameters.platform, 'x64') }}:
|
||||
- stage: Test_x64Win10_OfficialBuild
|
||||
displayName: Test x64Win10 (Official Build)
|
||||
dependsOn: BuildUITests_${{ parameters.platform }}
|
||||
jobs:
|
||||
- template: job-test-project.yml
|
||||
parameters:
|
||||
platform: x64Win10
|
||||
configuration: Release
|
||||
useLatestWebView2: ${{ parameters.useLatestWebView2 }}
|
||||
buildSource: ${{ parameters.buildSource }}
|
||||
specificBuildId: ${{ parameters.specificBuildId }}
|
||||
uiTestModules: ${{ parameters.uiTestModules }}
|
||||
|
||||
# Additional per-user installation test
|
||||
- template: job-test-project.yml
|
||||
parameters:
|
||||
platform: x64Win10
|
||||
configuration: Release
|
||||
useLatestWebView2: ${{ parameters.useLatestWebView2 }}
|
||||
buildSource: ${{ parameters.buildSource }}
|
||||
specificBuildId: ${{ parameters.specificBuildId }}
|
||||
uiTestModules: ${{ parameters.uiTestModules }}
|
||||
installMode: 'peruser'
|
||||
jobSuffix: '_PerUser'
|
||||
|
||||
- stage: Test_x64Win11_OfficialBuild
|
||||
displayName: Test x64Win11 (Official Build)
|
||||
dependsOn: BuildUITests_${{ parameters.platform }}
|
||||
jobs:
|
||||
- template: job-test-project.yml
|
||||
parameters:
|
||||
platform: x64Win11
|
||||
configuration: Release
|
||||
useLatestWebView2: ${{ parameters.useLatestWebView2 }}
|
||||
buildSource: ${{ parameters.buildSource }}
|
||||
specificBuildId: ${{ parameters.specificBuildId }}
|
||||
uiTestModules: ${{ parameters.uiTestModules }}
|
||||
|
||||
# Additional per-user installation test
|
||||
- template: job-test-project.yml
|
||||
parameters:
|
||||
platform: x64Win11
|
||||
configuration: Release
|
||||
useLatestWebView2: ${{ parameters.useLatestWebView2 }}
|
||||
buildSource: ${{ parameters.buildSource }}
|
||||
specificBuildId: ${{ parameters.specificBuildId }}
|
||||
uiTestModules: ${{ parameters.uiTestModules }}
|
||||
installMode: 'peruser'
|
||||
jobSuffix: '_PerUser'
|
||||
|
||||
- ${{ if ne(parameters.platform, 'x64') }}:
|
||||
- stage: Test_${{ parameters.platform }}_OfficialBuild
|
||||
displayName: Test ${{ parameters.platform }} (Official Build)
|
||||
dependsOn: BuildUITests_${{ parameters.platform }}
|
||||
jobs:
|
||||
- template: job-test-project.yml
|
||||
parameters:
|
||||
platform: ${{ parameters.platform }}
|
||||
configuration: Release
|
||||
useLatestWebView2: ${{ parameters.useLatestWebView2 }}
|
||||
buildSource: ${{ parameters.buildSource }}
|
||||
specificBuildId: ${{ parameters.specificBuildId }}
|
||||
uiTestModules: ${{ parameters.uiTestModules }}
|
||||
|
||||
# Additional per-user installation test
|
||||
- template: job-test-project.yml
|
||||
parameters:
|
||||
platform: ${{ parameters.platform }}
|
||||
configuration: Release
|
||||
useLatestWebView2: ${{ parameters.useLatestWebView2 }}
|
||||
buildSource: ${{ parameters.buildSource }}
|
||||
specificBuildId: ${{ parameters.specificBuildId }}
|
||||
uiTestModules: ${{ parameters.uiTestModules }}
|
||||
installMode: 'peruser'
|
||||
jobSuffix: '_PerUser'
|
||||
@@ -34,22 +34,22 @@
|
||||
<!-- 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="3.1.3" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.8" />
|
||||
<!-- Including Microsoft.Bcl.AsyncInterfaces to force version, since it's used by Microsoft.SemanticKernel. -->
|
||||
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.8" />
|
||||
<PackageVersion Include="Microsoft.Diagnostics.Tracing.TraceEvent" Version="3.1.16" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.8" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.8" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.8" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="9.0.8" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.8" />
|
||||
<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.2903.40" />
|
||||
<!-- 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.7" />
|
||||
<PackageVersion Include="Microsoft.Win32.SystemEvents" Version="9.0.8" />
|
||||
<PackageVersion Include="Microsoft.WindowsPackageManager.ComInterop" Version="1.10.340" />
|
||||
<PackageVersion Include="Microsoft.Windows.Compatibility" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Windows.Compatibility" Version="9.0.8" />
|
||||
<PackageVersion Include="Microsoft.Windows.CsWin32" Version="0.3.183" />
|
||||
<!-- 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. -->
|
||||
<!--
|
||||
@@ -78,28 +78,28 @@
|
||||
<PackageVersion Include="StreamJsonRpc" Version="2.21.69" />
|
||||
<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.7" />
|
||||
<PackageVersion Include="System.CodeDom" Version="9.0.8" />
|
||||
<PackageVersion Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
|
||||
<PackageVersion Include="System.ComponentModel.Composition" Version="9.0.7" />
|
||||
<PackageVersion Include="System.Configuration.ConfigurationManager" Version="9.0.7" />
|
||||
<PackageVersion Include="System.Data.OleDb" Version="9.0.7" />
|
||||
<PackageVersion Include="System.ComponentModel.Composition" Version="9.0.8" />
|
||||
<PackageVersion Include="System.Configuration.ConfigurationManager" Version="9.0.8" />
|
||||
<PackageVersion Include="System.Data.OleDb" Version="9.0.8" />
|
||||
<!-- 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.9.0" />
|
||||
<!-- 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.7" />
|
||||
<PackageVersion Include="System.Diagnostics.EventLog" Version="9.0.8" />
|
||||
<!-- 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.7" />
|
||||
<PackageVersion Include="System.Drawing.Common" Version="9.0.7" />
|
||||
<PackageVersion Include="System.Diagnostics.PerformanceCounter" Version="9.0.8" />
|
||||
<PackageVersion Include="System.Drawing.Common" Version="9.0.8" />
|
||||
<PackageVersion Include="System.IO.Abstractions" Version="22.0.13" />
|
||||
<PackageVersion Include="System.IO.Abstractions.TestingHelpers" Version="22.0.13" />
|
||||
<PackageVersion Include="System.Management" Version="9.0.7" />
|
||||
<PackageVersion Include="System.Management" Version="9.0.8" />
|
||||
<PackageVersion Include="System.Net.Http" Version="4.3.4" />
|
||||
<PackageVersion Include="System.Private.Uri" Version="4.3.2" />
|
||||
<PackageVersion Include="System.Reactive" Version="6.0.1" />
|
||||
<PackageVersion Include="System.Runtime.Caching" Version="9.0.7" />
|
||||
<PackageVersion Include="System.ServiceProcess.ServiceController" Version="9.0.7" />
|
||||
<PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.7" />
|
||||
<PackageVersion Include="System.Text.Json" Version="9.0.7" />
|
||||
<PackageVersion Include="System.Runtime.Caching" Version="9.0.8" />
|
||||
<PackageVersion Include="System.ServiceProcess.ServiceController" Version="9.0.8" />
|
||||
<PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.8" />
|
||||
<PackageVersion Include="System.Text.Json" Version="9.0.8" />
|
||||
<PackageVersion Include="System.Text.RegularExpressions" Version="4.3.1" />
|
||||
<PackageVersion Include="UnicodeInformation" Version="2.6.0" />
|
||||
<PackageVersion Include="UnitsNet" Version="5.56.0" />
|
||||
|
||||
42
NOTICE.md
42
NOTICE.md
@@ -1519,23 +1519,23 @@ SOFTWARE.
|
||||
- Mages 3.0.0
|
||||
- Markdig.Signed 0.34.0
|
||||
- MessagePack 3.1.3
|
||||
- Microsoft.Bcl.AsyncInterfaces 9.0.7
|
||||
- Microsoft.Bcl.AsyncInterfaces 9.0.8
|
||||
- Microsoft.Bot.AdaptiveExpressions.Core 4.23.0
|
||||
- Microsoft.CodeAnalysis.NetAnalyzers 9.0.0
|
||||
- Microsoft.Data.Sqlite 9.0.7
|
||||
- Microsoft.Data.Sqlite 9.0.8
|
||||
- Microsoft.Diagnostics.Tracing.TraceEvent 3.1.16
|
||||
- Microsoft.DotNet.ILCompiler (A)
|
||||
- Microsoft.Extensions.DependencyInjection 9.0.7
|
||||
- Microsoft.Extensions.Hosting 9.0.7
|
||||
- Microsoft.Extensions.Hosting.WindowsServices 9.0.7
|
||||
- Microsoft.Extensions.Logging 9.0.7
|
||||
- Microsoft.Extensions.Logging.Abstractions 9.0.7
|
||||
- Microsoft.Extensions.DependencyInjection 9.0.8
|
||||
- Microsoft.Extensions.Hosting 9.0.8
|
||||
- Microsoft.Extensions.Hosting.WindowsServices 9.0.8
|
||||
- Microsoft.Extensions.Logging 9.0.8
|
||||
- Microsoft.Extensions.Logging.Abstractions 9.0.8
|
||||
- Microsoft.NET.ILLink.Tasks (A)
|
||||
- Microsoft.SemanticKernel 1.15.0
|
||||
- Microsoft.Toolkit.Uwp.Notifications 7.1.2
|
||||
- Microsoft.Web.WebView2 1.0.2903.40
|
||||
- Microsoft.Win32.SystemEvents 9.0.7
|
||||
- Microsoft.Windows.Compatibility 9.0.7
|
||||
- Microsoft.Win32.SystemEvents 9.0.8
|
||||
- Microsoft.Windows.Compatibility 9.0.8
|
||||
- Microsoft.Windows.CsWin32 0.3.183
|
||||
- Microsoft.Windows.CsWinRT 2.2.0
|
||||
- Microsoft.Windows.SDK.BuildTools 10.0.26100.4188
|
||||
@@ -1555,25 +1555,25 @@ SOFTWARE.
|
||||
- SkiaSharp.Views.WinUI 2.88.9
|
||||
- StreamJsonRpc 2.21.69
|
||||
- StyleCop.Analyzers 1.2.0-beta.556
|
||||
- System.CodeDom 9.0.7
|
||||
- System.CodeDom 9.0.8
|
||||
- System.CommandLine 2.0.0-beta4.22272.1
|
||||
- System.ComponentModel.Composition 9.0.7
|
||||
- System.Configuration.ConfigurationManager 9.0.7
|
||||
- System.Data.OleDb 9.0.7
|
||||
- System.ComponentModel.Composition 9.0.8
|
||||
- System.Configuration.ConfigurationManager 9.0.8
|
||||
- System.Data.OleDb 9.0.8
|
||||
- System.Data.SqlClient 4.9.0
|
||||
- System.Diagnostics.EventLog 9.0.7
|
||||
- System.Diagnostics.PerformanceCounter 9.0.7
|
||||
- System.Drawing.Common 9.0.7
|
||||
- System.Diagnostics.EventLog 9.0.8
|
||||
- System.Diagnostics.PerformanceCounter 9.0.8
|
||||
- System.Drawing.Common 9.0.8
|
||||
- System.IO.Abstractions 22.0.13
|
||||
- System.IO.Abstractions.TestingHelpers 22.0.13
|
||||
- System.Management 9.0.7
|
||||
- System.Management 9.0.8
|
||||
- System.Net.Http 4.3.4
|
||||
- System.Private.Uri 4.3.2
|
||||
- System.Reactive 6.0.1
|
||||
- System.Runtime.Caching 9.0.7
|
||||
- System.ServiceProcess.ServiceController 9.0.7
|
||||
- System.Text.Encoding.CodePages 9.0.7
|
||||
- System.Text.Json 9.0.7
|
||||
- System.Runtime.Caching 9.0.8
|
||||
- System.ServiceProcess.ServiceController 9.0.8
|
||||
- System.Text.Encoding.CodePages 9.0.8
|
||||
- System.Text.Json 9.0.8
|
||||
- System.Text.RegularExpressions 4.3.1
|
||||
- UnicodeInformation 2.6.0
|
||||
- UnitsNet 5.56.0
|
||||
|
||||
@@ -262,6 +262,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "utils", "utils", "{B39DC643
|
||||
src\common\utils\EventLocker.h = src\common\utils\EventLocker.h
|
||||
src\common\utils\EventWaiter.h = src\common\utils\EventWaiter.h
|
||||
src\common\utils\excluded_apps.h = src\common\utils\excluded_apps.h
|
||||
src\common\utils\shell_ext_registration.h = src\common\utils\shell_ext_registration.h
|
||||
src\common\utils\exec.h = src\common\utils\exec.h
|
||||
src\common\utils\game_mode.h = src\common\utils\game_mode.h
|
||||
src\common\utils\gpo.h = src\common\utils\gpo.h
|
||||
@@ -461,6 +462,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Peek.Common", "src\modules\
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Peek.FilePreviewer", "src\modules\peek\Peek.FilePreviewer\Peek.FilePreviewer.csproj", "{AA9F0AF8-7924-4D59-BAA1-E36F1304E0DC}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Peek.UITests", "src\modules\peek\Peek.UITests\Peek.UITests.csproj", "{4E0AE3A4-2EE0-44D7-A2D0-8769977254A5}"
|
||||
EndProject
|
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "MarkdownPreviewHandlerCpp", "src\modules\previewpane\MarkdownPreviewHandlerCpp\MarkdownPreviewHandlerCpp.vcxproj", "{ED9A1AC6-AEB0-4569-A6E9-E1696182B545}"
|
||||
EndProject
|
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "GcodePreviewHandlerCpp", "src\modules\previewpane\GcodePreviewHandlerCpp\GcodePreviewHandlerCpp.vcxproj", "{5A5DD09D-723A-44D3-8F2B-293584C3D731}"
|
||||
@@ -784,6 +787,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.TimeDa
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.WindowWalker.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.WindowWalker.UnitTests\Microsoft.CmdPal.Ext.WindowWalker.UnitTests.csproj", "{E816D7B0-4688-4ECB-97CC-3D8E798F3829}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.UnitTestBase", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.UnitTestsBase\Microsoft.CmdPal.Ext.UnitTestBase.csproj", "{00D8659C-2068-40B6-8B86-759CD6284BBB}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|ARM64 = Debug|ARM64
|
||||
@@ -1852,6 +1857,14 @@ Global
|
||||
{AA9F0AF8-7924-4D59-BAA1-E36F1304E0DC}.Release|ARM64.Build.0 = Release|ARM64
|
||||
{AA9F0AF8-7924-4D59-BAA1-E36F1304E0DC}.Release|x64.ActiveCfg = Release|x64
|
||||
{AA9F0AF8-7924-4D59-BAA1-E36F1304E0DC}.Release|x64.Build.0 = Release|x64
|
||||
{4E0AE3A4-2EE0-44D7-A2D0-8769977254A5}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||
{4E0AE3A4-2EE0-44D7-A2D0-8769977254A5}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||
{4E0AE3A4-2EE0-44D7-A2D0-8769977254A5}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{4E0AE3A4-2EE0-44D7-A2D0-8769977254A5}.Debug|x64.Build.0 = Debug|x64
|
||||
{4E0AE3A4-2EE0-44D7-A2D0-8769977254A5}.Release|ARM64.ActiveCfg = Release|ARM64
|
||||
{4E0AE3A4-2EE0-44D7-A2D0-8769977254A5}.Release|ARM64.Build.0 = Release|ARM64
|
||||
{4E0AE3A4-2EE0-44D7-A2D0-8769977254A5}.Release|x64.ActiveCfg = Release|x64
|
||||
{4E0AE3A4-2EE0-44D7-A2D0-8769977254A5}.Release|x64.Build.0 = Release|x64
|
||||
{ED9A1AC6-AEB0-4569-A6E9-E1696182B545}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||
{ED9A1AC6-AEB0-4569-A6E9-E1696182B545}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||
{ED9A1AC6-AEB0-4569-A6E9-E1696182B545}.Debug|x64.ActiveCfg = Debug|x64
|
||||
@@ -2830,6 +2843,14 @@ Global
|
||||
{E816D7B0-4688-4ECB-97CC-3D8E798F3829}.Release|ARM64.Build.0 = Release|ARM64
|
||||
{E816D7B0-4688-4ECB-97CC-3D8E798F3829}.Release|x64.ActiveCfg = Release|x64
|
||||
{E816D7B0-4688-4ECB-97CC-3D8E798F3829}.Release|x64.Build.0 = Release|x64
|
||||
{00D8659C-2068-40B6-8B86-759CD6284BBB}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||
{00D8659C-2068-40B6-8B86-759CD6284BBB}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||
{00D8659C-2068-40B6-8B86-759CD6284BBB}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{00D8659C-2068-40B6-8B86-759CD6284BBB}.Debug|x64.Build.0 = Debug|x64
|
||||
{00D8659C-2068-40B6-8B86-759CD6284BBB}.Release|ARM64.ActiveCfg = Release|ARM64
|
||||
{00D8659C-2068-40B6-8B86-759CD6284BBB}.Release|ARM64.Build.0 = Release|ARM64
|
||||
{00D8659C-2068-40B6-8B86-759CD6284BBB}.Release|x64.ActiveCfg = Release|x64
|
||||
{00D8659C-2068-40B6-8B86-759CD6284BBB}.Release|x64.Build.0 = Release|x64
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -2988,6 +3009,7 @@ Global
|
||||
{9D7A6DE0-7D27-424D-ABAE-41B2161F9A03} = {17B4FA70-001E-4D33-BBBB-0D142DBC2E20}
|
||||
{17A99C7C-0BFF-45BB-A9FD-63A0DDC105BB} = {17B4FA70-001E-4D33-BBBB-0D142DBC2E20}
|
||||
{AA9F0AF8-7924-4D59-BAA1-E36F1304E0DC} = {17B4FA70-001E-4D33-BBBB-0D142DBC2E20}
|
||||
{4E0AE3A4-2EE0-44D7-A2D0-8769977254A5} = {17B4FA70-001E-4D33-BBBB-0D142DBC2E20}
|
||||
{ED9A1AC6-AEB0-4569-A6E9-E1696182B545} = {2F305555-C296-497E-AC20-5FA1B237996A}
|
||||
{5A5DD09D-723A-44D3-8F2B-293584C3D731} = {2F305555-C296-497E-AC20-5FA1B237996A}
|
||||
{B3E869C4-8210-4EBD-A621-FF4C4AFCBFA9} = {2F305555-C296-497E-AC20-5FA1B237996A}
|
||||
@@ -3117,11 +3139,6 @@ Global
|
||||
{D9BD324E-1D80-44AA-8E7B-73EB00944434} = {38BDB927-829B-4C65-9CD9-93FB05D66D65}
|
||||
{8EF25507-2575-4ADE-BF7E-D23376903AB8} = {3846508C-77EB-4034-A702-F8BB263C4F79}
|
||||
{070AC093-C9F2-20AD-0BCD-F318FC2761EA} = {B1234567-1234-1234-1234-123456789ABC}
|
||||
{E816D7AC-4688-4ECB-97CC-3D8E798F3825} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
|
||||
{E816D7AD-4688-4ECB-97CC-3D8E798F3826} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
|
||||
{E816D7AE-4688-4ECB-97CC-3D8E798F3827} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
|
||||
{E816D7AF-4688-4ECB-97CC-3D8E798F3828} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
|
||||
{E816D7B0-4688-4ECB-97CC-3D8E798F3829} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
|
||||
{2C318EC3-BA86-4372-B1BC-DB0F33C208B2} = {322566EF-20DC-43A6-B9F8-616AF942579A}
|
||||
{BFFB607F-7C78-434B-86B9-DA4C8196A1B5} = {B6C42F16-73EB-477E-8B0D-4E6CF6C20AAC}
|
||||
{66E1534A-1587-42B2-912F-45C994D32904} = {89E20BCE-EB9C-46C8-8B50-E01A82E6FDC3}
|
||||
@@ -3139,6 +3156,12 @@ Global
|
||||
{806BF185-8B89-5BE1-9AA1-DA5BC9487DB9} = {264B412F-DB8B-4CF8-A74B-96998B183045}
|
||||
{F93C2817-C846-4259-84D8-B39A6B57C8DE} = {3527BF37-DFC5-4309-A032-29278CA21328}
|
||||
{8131151D-B0E9-4E18-84A5-E5F946C4480A} = {929C1324-22E8-4412-A9A8-80E85F3985A5}
|
||||
{E816D7AC-4688-4ECB-97CC-3D8E798F3825} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
|
||||
{E816D7AD-4688-4ECB-97CC-3D8E798F3826} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
|
||||
{E816D7AE-4688-4ECB-97CC-3D8E798F3827} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
|
||||
{E816D7AF-4688-4ECB-97CC-3D8E798F3828} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
|
||||
{E816D7B0-4688-4ECB-97CC-3D8E798F3829} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
|
||||
{00D8659C-2068-40B6-8B86-759CD6284BBB} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0}
|
||||
|
||||
202
README.md
202
README.md
@@ -14,7 +14,7 @@ Microsoft PowerToys is a set of utilities for power users to tune and streamline
|
||||
| [Color Picker](https://aka.ms/PowerToysOverview_ColorPicker) | [Command Not Found](https://aka.ms/PowerToysOverview_CmdNotFound) | [Command Palette](https://aka.ms/PowerToysOverview_CmdPal) |
|
||||
| [Crop And Lock](https://aka.ms/PowerToysOverview_CropAndLock) | [Environment Variables](https://aka.ms/PowerToysOverview_EnvironmentVariables) | [FancyZones](https://aka.ms/PowerToysOverview_FancyZones) |
|
||||
| [File Explorer Add-ons](https://aka.ms/PowerToysOverview_FileExplorerAddOns) | [File Locksmith](https://aka.ms/PowerToysOverview_FileLocksmith) | [Hosts File Editor](https://aka.ms/PowerToysOverview_HostsFileEditor) |
|
||||
| [Image Resizer](https://aka.ms/PowerToysOverview_ImageResizer) | [Keyboard Manager](https://aka.ms/PowerToysOverview_KeyboardManager) | [Mouse utilities](https://aka.ms/PowerToysOverview_MouseUtilities) |
|
||||
| [Image Resizer](https://aka.ms/PowerToysOverview_ImageResizer) | [Keyboard Manager](https://aka.ms/PowerToysOverview_KeyboardManager) | [Mouse Utilities](https://aka.ms/PowerToysOverview_MouseUtilities) |
|
||||
| [Mouse Without Borders](https://aka.ms/PowerToysOverview_MouseWithoutBorders) | [New+](https://aka.ms/PowerToysOverview_NewPlus) | [Paste as Plain Text](https://aka.ms/PowerToysOverview_PastePlain) |
|
||||
| [Peek](https://aka.ms/PowerToysOverview_Peek) | [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) |
|
||||
@@ -35,19 +35,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.93%22
|
||||
[github-current-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.92%22
|
||||
[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.92.1/PowerToysUserSetup-0.92.1-x64.exe
|
||||
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.92.1/PowerToysUserSetup-0.92.1-arm64.exe
|
||||
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.92.1/PowerToysSetup-0.92.1-x64.exe
|
||||
[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.92.1/PowerToysSetup-0.92.1-arm64.exe
|
||||
[github-next-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.94%22
|
||||
[github-current-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.93%22
|
||||
[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.93.0/PowerToysUserSetup-0.93.0-x64.exe
|
||||
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.93.0/PowerToysUserSetup-0.93.0-arm64.exe
|
||||
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.93.0/PowerToysSetup-0.93.0-x64.exe
|
||||
[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.93.0/PowerToysSetup-0.93.0-arm64.exe
|
||||
|
||||
| Description | Filename |
|
||||
|----------------|----------|
|
||||
| Per user - x64 | [PowerToysUserSetup-0.92.1-x64.exe][ptUserX64] |
|
||||
| Per user - ARM64 | [PowerToysUserSetup-0.92.1-arm64.exe][ptUserArm64] |
|
||||
| Machine wide - x64 | [PowerToysSetup-0.92.1-x64.exe][ptMachineX64] |
|
||||
| Machine wide - ARM64 | [PowerToysSetup-0.92.1-arm64.exe][ptMachineArm64] |
|
||||
| Per user - x64 | [PowerToysUserSetup-0.93.0-x64.exe][ptUserX64] |
|
||||
| Per user - ARM64 | [PowerToysUserSetup-0.93.0-arm64.exe][ptUserArm64] |
|
||||
| Machine wide - x64 | [PowerToysSetup-0.93.0-x64.exe][ptMachineX64] |
|
||||
| Machine wide - ARM64 | [PowerToysSetup-0.93.0-arm64.exe][ptMachineArm64] |
|
||||
|
||||
This is our preferred method.
|
||||
|
||||
@@ -93,139 +93,119 @@ 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.92 - June 2025 Update
|
||||
### 0.93 - Aug 2025 Update
|
||||
|
||||
In this release, we focused on new features, stability, optimization improvements, and automation.
|
||||
|
||||
**✨Highlights**
|
||||
|
||||
- PowerToys settings now has a toggle for the system tray icon, giving users control over its visibility based on personal preference. Thanks [@BLM16](https://github.com/BLM16)!
|
||||
- Command Palette now has Ahead-of-Time ([AOT](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot)) compatibility for all first-party extensions, improved extensibility, and core UX fixes, resulting in better performance and stability across commands.
|
||||
- Color Picker now has customizable mouse button actions, enabling more personalized workflows by assigning functions to left, right, and middle clicks. Thanks [@PesBandi](https://github.com/PesBandi)!
|
||||
- Bug Report Tool now has a faster and clearer reporting process, with progress indicators, improved compression, auto-cleanup of old trace logs, and inclusion of MSIX installer logs for more efficient diagnostics.
|
||||
- File Explorer add-ons now have improved rendering stability, resolving issues with PDF previews, blank thumbnails, and text file crashes during file browsing.
|
||||
|
||||
### Color Picker
|
||||
|
||||
- Added mouse button actions so you can choose what left, right, or middle click does. Thanks [@PesBandi](https://github.com/PesBandi)!
|
||||
|
||||
### Crop & Lock
|
||||
|
||||
- Aligned window styling with current Windows theme for a cleaner look. Thanks [@sadirano](https://github.com/sadirano)!
|
||||
- PowerToys settings debuts a modern, card-based dashboard with clearer descriptions and faster navigation for a streamlined user experience.
|
||||
- Command Palette had over 99 issues resolved, including bringing back Clipboard History, adding context menu shortcuts, pinning favorite apps, and supporting history in Run.
|
||||
- Command Palette reduced its startup memory usage by ~15%, load time by ~40%, built-in extensions loading time by ~70%, and installation size by ~55%—all due to using the full Ahead-of-Time (AOT) compilation mode in Windows App SDK.
|
||||
- Peek now supports instant previews and embedded thumbnails for Binary G-code (.bgcode) 3D printing files, making it easy to inspect models at a glance. Thanks [@pedrolamas](https://github.com/pedrolamas)!
|
||||
- Mouse Utilities introduces a new spotlight highlighting mode that dims the screen and draws attention to your cursor, perfect for presentations.
|
||||
- Test coverage improvements for multiple PowerToys modules including Command Palette, Advanced Paste, Peek, Text Extractor, and PowerRename — ensuring better reliability and quality, with over 600 new unit tests (mostly for Command Palette) and doubled UI automation coverage.
|
||||
|
||||
### Command Palette
|
||||
|
||||
- Enhanced performance by resolving a regression in page loading.
|
||||
- Applied consistent hotkey handling across all Command Palette commands for a smoother user experience.
|
||||
- Improved graceful closing of Command Palette. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
|
||||
- Fixed consistency issue for extensions' alias with "Direct" setting and enabled localization for "Direct" and "Indirect" for better user understanding. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
|
||||
- Improved visual clarity by styling critical context items correctly.
|
||||
- Automatically focused the field when only one is present on the content page.
|
||||
- Improved stability and efficiency when loading file icons in SDK ThumbnailHelper.cs by removing unnecessary operations. Thanks [@OldUser101](https://github.com/OldUser101)!
|
||||
- Enhanced details view with commands implementation. (See [Extension sample](./src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleListPageWithDetails.cs))
|
||||
- Ensured screen readers are notified when the selected item in the list changes for better accessibility.
|
||||
- Fixed command title changes not being properly notified to screen readers. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Made icon controls excluded from keyboard navigation by default for better accessibility. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Improved UI design with better text sizing and alignment.
|
||||
- Fixed keyboard shortcuts to work better in text boxes and context menus.
|
||||
- Added right-click context menus with critical command styling and separators.
|
||||
- Improved various context menu issues, improving item selection, handling of long titles, search bar text scaling, initial item behavior, and primary button functionality.
|
||||
- Fixed context menu crashes with better type handling.
|
||||
- Fixed "Reload" command to work with both uppercase and lowercase letters.
|
||||
- Added mouse back button support for easier navigation. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Fixed Alt+Left Arrow navigation not working when search box contains text. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Updated back button tooltip to show keyboard shortcut information. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Fixed Command Palette window not appearing properly when activated. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Fixed Command Palette window staying hidden from taskbar after File Explorer restarts. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Fixed window focus not returning to previous app properly.
|
||||
- Fixed Command Palette window to always appear on top when shown and move to bottom when hidden. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Fixed window hiding to properly work on UI thread. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Fixed crashes and improved stability with better synchronization of Command list updates. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Improved extension disposal with better error handling to prevent crashes. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Improved stability by fixing a UI threading issue when loading more results, preventing possible crashes and ensuring the loading state resets if loading fails. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Enhanced icon loading stability with better exception handling. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added thread safety to recent commands to prevent crashes. Thanks [@MaoShengelia](https://github.com/MaoShengelia)!
|
||||
- Fixed acrylic (frosted glass) system backdrop display issues by ensuring proper UI thread handling. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
|
||||
### Command Palette extensions
|
||||
|
||||
- Added "Copy Path" command to *App* search results for convenience. Thanks [@PesBandi](https://github.com/PesBandi)!
|
||||
- Improved *Calculator* input experience by ignoring leading equal signs. Thanks [@PesBandi](https://github.com/PesBandi)!
|
||||
- Corrected input handling in the *Calculator* extension to avoid showing errors for input with only leading whitespace.
|
||||
- Improved *New Extension* wizard by validating names to prevent namespace errors.
|
||||
- Ensured consistent context items display for the *Run* extension between fallback and top-level results.
|
||||
- Fixed missing *Time & Date* commands in fallback results. Thanks [@htcfreek](https://github.com/htcfreek)!
|
||||
- Fixed outdated results in the *Time & Date* extension. Thanks [@htcfreek](https://github.com/htcfreek)!
|
||||
- Fixed an issue where *Web Search* always opened Microsoft Edge instead of the user's default browser on Windows 11 24H2 and later. Thanks [@RuggMatt](https://github.com/RuggMatt)!
|
||||
- Improved ordering of *Windows Settings* extension search results from alphabetical to relevance-based for quicker access.
|
||||
- Added "Restart Windows Explorer" command to the *Windows System Commands* provider for gracefully terminate and relaunch explorer.exe. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added settings to each provider to control which fallback commands are enabled. Thanks [@jiripolasek](https://github.com/jiripolasek)! for fixing a regression in this feature.
|
||||
- Added sample code showing how Command Palette extensions can track when their pages are loaded or unloaded. [Check it out here](./src/modules/cmdpal/ext/SamplePagesExtension/OnLoadPage.cs).
|
||||
- Fixed *Calculator* to accept regular spaces in numbers that use space separators. Thanks [@PesBandi](https://github.com/PesBandi)!
|
||||
- Added a new setting to *Calculator* to make "Copy" the primary button (replacing “Save”) and enable "Close on Enter", streamlining the workflow. Thanks [@PesBandi](https://github.com/PesBandi)!
|
||||
- Improved *Apps* indexing error handling and removed obsolete code. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
|
||||
- Prevented apps from showing in search when the *Apps* extension is disabled. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added ability to pin/unpin *Apps* using Ctrl+P shortcut.
|
||||
- Added keyboard shortcuts to the *Apps* context menu items for faster access.
|
||||
- Added all file context menu options to the *Apps* items context menu, making all file actions available there for better functionality.
|
||||
- Streamlined All *Apps* extension settings by removing redundant descriptions, making the UI clearer.
|
||||
- Added command history to the *Run* page for easier access to previous commands.
|
||||
- Fixed directory path handling in *Run* fallback for better file navigation.
|
||||
- Fixed URL fallback item hiding properly in *Web Search* extension when search query becomes invalid. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added proper empty state message for *Web Search* extension when no results found. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added fallback command to *Windows Settings* extension for better search results.
|
||||
- Re-enabled *Clipboard History* feature with proper window handling.
|
||||
- Improved *Add Bookmark* extension to automatically detect file, folder, or URL types without manual input.
|
||||
- Updated terminology from "Kill process" to "End task" in *Window Walker* for consistency with Windows.
|
||||
- Fixed minor grammar error in SamplePagesExtension code comments. Thanks [@purofle](https://github.com/purofle)!
|
||||
|
||||
### Command Palette Ahead-of-Time (AOT) readiness
|
||||
### Mouse Utilities
|
||||
|
||||
- We’ve made foundational changes to prepare the Command Palette for future Ahead-of-Time (AOT) publishing. This includes replacing the calculator library with ExprTk, improving COM object handling, refining Win32 interop, and correcting trimming behavior—all to ensure compatibility, performance, and reliability under AOT constraints. All first-party extensions are now AOT-compatible. These improvements lay the groundwork for publishing Command Palette as an AOT application in the next release.
|
||||
- Special thanks to [@Sergio0694](https://github.com/Sergio0694) for guidance on making COM APIs AOT-compatible, [@jtschuster](https://github.com/jtschuster) for fixing COM object handling, [@ArashPartow](https://github.com/ArashPartow) from ExprTk for integration suggestions, and [@tian-lt](https://github.com/tian-lt) from the Windows Calculator team for valuable suggestion throughout the migration journey and review.
|
||||
- As part of the upcoming release, we’re also enabling AOT compatibility for key dependencies, including markdown rendering, Adaptive Cards, internal logging and telemetry library, and the core Command Palette UX.
|
||||
|
||||
### FancyZones
|
||||
|
||||
- Fixed DPI-scaling issues to ensure FancyZones Editor displays crisply on high-resolution monitors. Thanks [@HO-COOH](https://github.com/HO-COOH)! This inspired us a broader review across other PowerToys modules, leading to DPI display optimizations in Awake, Color Picker, PowerAccent, and more.
|
||||
|
||||
### File Explorer add-ons
|
||||
|
||||
- Fixed potential failures in PDF previewer and thumbnail generation, improving reliability when browsing PDF files. Thanks [@mohiuddin-khan-shiam](https://github.com/mohiuddin-khan-shiam)!
|
||||
- Prevented Monaco Preview Handler crash when opening UTF-8-BOM text files.
|
||||
|
||||
### Hosts File Editor
|
||||
|
||||
- Added an in-app *“Learn more”* link to warning dialogs for quick guidance. Thanks [@PesBandi](https://github.com/PesBandi)!
|
||||
|
||||
### Mouse Without Borders
|
||||
|
||||
- Fixed firewall rule so MWB now accepts connections from IPs outside your local subnet.
|
||||
- Cleaned legacy logs to reduce disk usage and noise.
|
||||
- Added a new spotlight highlighting mode that creates a large transparent circle around your cursor with a backdrop effect, providing an alternative to the traditional circle highlight. Perfect for presentations where you want to focus attention on a specific area while dimming the rest of the screen.
|
||||
|
||||
### Peek
|
||||
|
||||
- Updated QOI reader so 3-channel QOI images preview correctly in Peek and File Explorer. Thanks [@mbartlett21](https://github.com/mbartlett21)!
|
||||
- Added codec detection with a clear warning when a video can’t be previewed, along with a link to the Microsoft Store to download the required codec.
|
||||
- Added preview and thumbnail support for Binary G-code (.bgcode) files used in 3D printing. You can now see embedded thumbnails and preview these compressed 3D printing files directly in Peek and File Explorer. Thanks [@pedrolamas](https://github.com/pedrolamas)!
|
||||
|
||||
### PowerRename
|
||||
### Quick Accent
|
||||
|
||||
- Added support for $YY-$MM-$DD in ModificationTime and AccessTime to enable flexible date-based renaming.
|
||||
|
||||
### PowerToys Run
|
||||
|
||||
- Suppressed error UI for known WPF-related crashes to reduce user confusion, while retaining diagnostic logging for analysis. This targets COMException 0xD0000701 and 0x80263001 caused by temporary DWM unavailability.
|
||||
|
||||
### Registry Preview
|
||||
|
||||
- Added "Extended data preview" via magnifier icon and context menu in the Data Grid, enabled easier inspection of complex registry types like REG_BINARY, REG_EXPAND_SZ, and REG_MULTI_SZ, etc. Thanks [@htcfreek](https://github.com/htcfreek)!
|
||||
- Improved file-saving experience in Registry Preview by aligning with Notepad-like behavior, enhancing user prompts, error handling, and preventing crashes during unsaved or interrupted actions. Thanks [@htcfreek](https://github.com/htcfreek)!
|
||||
- Added Vietnamese language support to Quick Accent, mappings for Vietnamese vowels (a, e, i, o, u, y) and the letter d. Thanks [@octastylos-pseudodipteros](https://github.com/octastylos-pseudodipteros)!
|
||||
|
||||
### Settings
|
||||
|
||||
- Added an option to hide or show the PowerToys system tray icon. Thanks [@BLM16](https://github.com/BLM16)!
|
||||
- Improved settings to show progress while a bug report package is being generated.
|
||||
|
||||
### Workspaces
|
||||
|
||||
- Stored Workspaces icons in user AppData to ensure profile portability and prevent loss during temporary folder cleanup.
|
||||
- Enabled capture and launch of PWAs on non-default Edge or Chrome profiles, ensuring consistent behavior during creation and execution.
|
||||
- Completely redesigned the Settings dashboard with a modern card-based layout featuring organized sections for quick actions and shortcuts overview, replacing the old module list.
|
||||
- Rewrote setting descriptions to be more concise and follow Windows writing style guidelines, making them easier to understand.
|
||||
- Improved formatting and readability of release notes in the "What's New" section with better typography and spacing.
|
||||
- Added missing deep link support for various settings pages (Peek, Quick Accent, PowerToys Run, etc.) so you can jump directly to specific settings.
|
||||
- Resolved an issue where the settings page header would drift away from its position when resizing the settings window.
|
||||
- Resolved a settings crash related to incompatible property names in ZoomIt configuration.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Added SpeedTest and Dictionary Definition to the third-party plugins documentation for PowerToys Run. Thanks [@ruslanlap](https://github.com/ruslanlap)!
|
||||
- Corrected sample links and typo in Command Palette documentation. Thanks [@daverayment](https://github.com/daverayment) and [@roycewilliams](https://github.com/roycewilliams)!
|
||||
- Added detailed step-by-step instructions for first-time developers building the Command Palette module, including prerequisites and Visual Studio setup guidance. Thanks [@chatasweetie](https://github.com/chatasweetie)!
|
||||
- **Fixed Broken SDK Link**: Corrected a broken markdown link in the Command Palette SDK README that was pointing to an incorrect directory path. Thanks [@ChrisGuzak](https://github.com/ChrisGuzak)!
|
||||
- Added documentation for the "Open With Cursor" plugin that enables opening Visual Studio and VS Code recent files using Cursor AI. Thanks [@VictorNoxx](https://github.com/VictorNoxx)!
|
||||
- Added documentation for two new community plugins - Hotkeys plugin for creating custom keyboard shortcuts, and RandomGen plugin for generating random data like passwords, colors, and placeholder text. Thanks [@ruslanlap](https://github.com/ruslanlap)!
|
||||
|
||||
### Development
|
||||
|
||||
- Updated .NET libraries to 9.0.6 for performance and security. Thanks [@snickler](https://github.com/snickler)!
|
||||
- Updated WinAppSDK to 1.7.2 for better stability and Windows support.
|
||||
- Introduced a one-step local build script that generates a signed installer, enhancing developer productivity.
|
||||
- Generated portable PDBs so cross-platform debuggers can read symbol files, improving debugging experience in VSCode and other tools.
|
||||
- Simplified WinGet configuration files by using the [Microsoft.Windows.Settings](https://www.powershellgallery.com/packages/Microsoft.Windows.Settings) module to enable Developer Mode. Thanks [@mdanish-kh](https://github.com/mdanish-kh)!
|
||||
- Adjusted build scripts for the latest Az.Accounts module to keep CI green.
|
||||
- Streamlined release pipeline by removing hard-coded telemetry version numbers, and unified Command Palette versioning with Windows Terminal's versioning method for consistent updates.
|
||||
- Enhanced the build validation step to show detailed differences between NOTICE.md and actual package dependencies and versions.
|
||||
- Improved spell-checking accuracy across the repo. Thanks [@rovercoder](https://github.com/rovercoder)!
|
||||
- Upgraded CI to TouchdownBuild v5 for faster pipelines.
|
||||
- Added context comments to *Resources.resw* to help translators.
|
||||
- Expanded fuzz testing coverage to include FancyZones.
|
||||
- Integrated all unit tests into the CI pipeline, increasing from ~3,000 to ~5,000 tests.
|
||||
- Enabled daily UI test automation on the main branch, now covering over 370 UI tests for end-to-end validation.
|
||||
- Newly added unit tests for WorkspacesLib to improve reliability and maintainability.
|
||||
- Updated .NET libraries to 9.0.8 for performance and security. Thanks [@snickler](https://github.com/snickler)!
|
||||
- Updated the spell check system to version 0.0.25 with better GitHub integration and SARIF reporting, plus fixed numerous spelling errors throughout the codebase including property names and documentation. Thanks [@jsoref](https://github.com/jsoref)!
|
||||
- Cleaned up spelling check configuration to eliminate false positives and excessive noise that was appearing in every pull request, making the development process smoother.
|
||||
- Replaced NuGet feed with Azure Artifacts for better package management.
|
||||
- Implemented configurable UI test pipeline that can use pre-built official releases instead of building everything from scratch, reducing test execution time from 2+ hours.
|
||||
- Replaced brittle pixel-by-pixel image comparison with perceptual hash (pHash) technology that's more robust to minor rendering differences - no more test failures due to anti-aliasing or compression artifacts.
|
||||
- Reduced CI/fuzzing/UI test timeouts from 4 hours to 90 minutes, dramatically improving developer feedback loops and preventing long waits when builds get stuck.
|
||||
- Standardized test project naming across the entire codebase and improved pipeline result identification by adding platform/install mode context to test run titles. Thanks [@khmyznikov](https://github.com/khmyznikov)!
|
||||
- Added comprehensive UI test suites for multiple PowerToys modules including Command Palette, Advanced Paste, Peek, Text Extractor, and PowerRename - ensuring better reliability and quality.
|
||||
- Enhanced UI test automation with command-line argument support, better session management, and improved element location methods using pattern matching to avoid failures from minor differences in exact matches.
|
||||
|
||||
### General
|
||||
### What is being planned over the next few releases
|
||||
|
||||
- Updated bug report compression library (cziplib 0.3.3) for faster and more reliable package creation. Thanks [@Chubercik](https://github.com/Chubercik)!
|
||||
- Included App Installer (“AppX Deployment Server”) event logs in bug reports for more thorough diagnostics.
|
||||
|
||||
### What is being planned for version 0.93
|
||||
|
||||
For [v0.93][github-next-release-work], we'll work on the items below:
|
||||
For [v0.94][github-next-release-work], we'll work on the items below:
|
||||
|
||||
- Continued Command Palette polish
|
||||
- New UI automation tests
|
||||
- Working on installer upgrades
|
||||
- Working on Shortcut Guide v2 (Thanks [@noraa-junker](https://github.com/noraa-junker)!)
|
||||
- Working on upgrading the installer to WiX 5
|
||||
- Working on shortcut conflict detection
|
||||
- Working on setting search
|
||||
- Upgrading Keyboard Manager's editor UI
|
||||
- New UI automation tests
|
||||
- Stability, bug fixes
|
||||
|
||||
## PowerToys Community
|
||||
|
||||
@@ -22,23 +22,23 @@ The PowerToys UI test pipeline provides flexible options for building and testin
|
||||
|
||||
### Pipeline Options
|
||||
|
||||
- **useLatestOfficialBuild**: When checked, downloads the latest official PowerToys build and installs it for testing. This skips the full solution build and only builds UI test projects.
|
||||
- **buildSource**: Select the build type for testing:
|
||||
- `latestMainOfficialBuild`: Downloads and uses the latest official PowerToys build from main branch
|
||||
- `buildNow`: Builds PowerToys from current source code and uses it for testing
|
||||
- `specificBuildId`: Downloads a specific PowerToys build using the build ID specified in `specificBuildId` parameter
|
||||
|
||||
- **useCurrentBranchBuild**: When checked along with `useLatestOfficialBuild`, downloads the official build from the current branch instead of main.
|
||||
**Default value**: `latestMainOfficialBuild`
|
||||
|
||||
**Default value**: `false` (downloads from main branch)
|
||||
- **specificBuildId**: When `buildSource` is set to `specificBuildId`, specify the exact PowerToys build ID to download and test against.
|
||||
|
||||
**Default value**: `"xxxx"` (placeholder, enter actual build ID when using specificBuildId option)
|
||||
|
||||
**When to use this**:
|
||||
- **Default scenario**: The pipeline tests against the latest signed PowerToys build from the `main` branch, regardless of which branch your test code changes are from
|
||||
- **Custom branch testing**: Only specify `true` when:
|
||||
- Your branch has produced its own signed PowerToys build via the official build pipeline
|
||||
- You want to test against that specific branch's PowerToys build instead of main
|
||||
- You are testing PowerToys functionality changes that are only available in your branch's build
|
||||
- Testing against a specific known build for reproducibility
|
||||
- Regression testing against a particular build version
|
||||
- Validating fixes in a specific build before release
|
||||
|
||||
**Important notes**:
|
||||
- The test pipeline itself runs from your specified branch, but by default tests against the main branch's PowerToys build
|
||||
- Not all branches have signed builds available - only use this if you're certain your branch has a signed build
|
||||
- If enabled but no build exists for your branch, the pipeline may fail or fall back to main
|
||||
**Usage**: Enter the build ID number (e.g., `12345`) to download that specific build. Only used when `buildSource` is set to `specificBuildId`.
|
||||
|
||||
- **uiTestModules**: Specify which UI test modules to build and run. This parameter controls both the `.csproj` projects to build and the `.dll` test assemblies to execute. Examples:
|
||||
- `['UITests-FancyZones']` - Only FancyZones UI tests
|
||||
@@ -50,19 +50,19 @@ The PowerToys UI test pipeline provides flexible options for building and testin
|
||||
|
||||
### Build Modes
|
||||
|
||||
1. **Official Build + Selective Testing** (`useLatestOfficialBuild = true`)
|
||||
- Downloads and installs official PowerToys build
|
||||
- Builds only specified UI test projects
|
||||
- Runs specified UI tests against installed PowerToys
|
||||
- Controlled by `uiTestModules` parameter
|
||||
1. **Official Build Testing** (`buildSource = latestMainOfficialBuild` or `specificBuildId`)
|
||||
- Downloads and installs official PowerToys build (latest from main or specific build ID)
|
||||
- Builds only UI test projects (all or specific based on `uiTestModules`)
|
||||
- Runs UI tests against installed PowerToys
|
||||
- Tests both machine-level and per-user installation modes automatically
|
||||
|
||||
2. **Full Build + Testing** (`useLatestOfficialBuild = false`)
|
||||
- Builds entire PowerToys solution
|
||||
2. **Current Source Build Testing** (`buildSource = buildNow`)
|
||||
- Builds entire PowerToys solution from current source code
|
||||
- Builds UI test projects (all or specific based on `uiTestModules`)
|
||||
- Runs UI tests (all or specific based on `uiTestModules`)
|
||||
- Uses freshly built PowerToys for testing
|
||||
- Runs UI tests against freshly built PowerToys
|
||||
- Uses artifacts from current pipeline build
|
||||
|
||||
> **Note**: Both modes support the `uiTestModules` parameter to control which specific UI test modules to build and run.
|
||||
> **Note**: All modes support the `uiTestModules` parameter to control which specific UI test modules to build and run. Both machine-level and per-user installation modes are tested automatically when using official builds.
|
||||
|
||||
### Pipeline Access
|
||||
- Pipeline: https://microsoft.visualstudio.com/Dart/_build?definitionId=161438&_a=summary
|
||||
|
||||
@@ -87,6 +87,13 @@
|
||||
|
||||
### Building PowerToys Locally
|
||||
|
||||
#### One stop script for building installer
|
||||
1. Open developer powershell for vs 2022
|
||||
2. Run tools\build\build-installer.ps1
|
||||
> For the first-time setup, please run the installer as an administrator. This ensures that the Wix tool can move wix.target to the desired location and trust the certificate used to sign the MSIX packages.
|
||||
|
||||
The following manual steps will not install the MSIX apps (such as Command Palette) on your local installer.
|
||||
|
||||
#### Prerequisites for building the MSI installer
|
||||
|
||||
1. Install the [WiX Toolset Visual Studio 2022 Extension](https://marketplace.visualstudio.com/items?itemName=WixToolset.WixToolsetVisualStudio2022Extension).
|
||||
|
||||
33
doc/devdocs/development/test-winget-install-locally.md
Normal file
33
doc/devdocs/development/test-winget-install-locally.md
Normal file
@@ -0,0 +1,33 @@
|
||||
## If for any reason, you'd like to test winget install scenario, you can follow this doc:
|
||||
|
||||
### Powertoys winget manifest definition:
|
||||
[winget repository](https://github.com/microsoft/winget-pkgs/tree/master/manifests/m/Microsoft/PowerToys)
|
||||
|
||||
### How to test a winget installation locally:
|
||||
1. Get artifacts from release CI pipeline Pipelines - Runs for PowerToys Signed YAML Release Build, or you can build one yourself by execute the
|
||||
'tools\build\build-installer.ps1' script
|
||||
|
||||
2. Get the artifact hash, this is required to define winget manifest
|
||||
```powershell
|
||||
cd /path/to/your/directory/contains/installer
|
||||
Get-FileHash -Path ".\<Installer-name>.exe" -Algorithm SHA256
|
||||
```
|
||||
3. Host your installer.exe - Attention: staged github release artifacts or artifacts in release pipeline is not OK in this step
|
||||
You can self-host it or you can upload to a publicly available endpoint
|
||||
**How to selfhost it** (A extremely simple way):
|
||||
```powershell
|
||||
python -m http.server 8000
|
||||
```
|
||||
|
||||
4. Download a version folder from wingetpkgs like: [version 0.92.1](https://github.com/microsoft/winget-pkgs/tree/master/manifests/m/Microsoft/PowerToys/0.92.1)
|
||||
and you get **a folder contains 3 yml files**
|
||||
>note: Do not put any files other than these three in this folder
|
||||
|
||||
5. Modify the yml files based on your version and the self hosted artifact link, and modify the sha256 hash for the installer you'd like to use
|
||||
|
||||
6. Start winget install:
|
||||
```powershell
|
||||
#execute as admin
|
||||
winget settings --enable LocalManifestFiles
|
||||
winget install --manifest "<folder_path_of_manifest_files>" --architecture x64 --scope user
|
||||
```
|
||||
@@ -14,21 +14,6 @@
|
||||
<DirectoryRef Id="FileLocksmithAssetsInstallFolder" FileSource="$(var.FileLocksmithAssetsFilesPath)">
|
||||
<!-- Generated by generateFileComponents.ps1 -->
|
||||
<!--FileLocksmithAssetsFiles_Component_Def-->
|
||||
<!-- !Warning! Make sure to change Component Guid if you update something here -->
|
||||
<Component Id="Module_FileLocksmith" Guid="108D3EC1-E6E0-4E81-88EF-25966133CB41" Win64="yes">
|
||||
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\CLSID\{84D68575-E186-46AD-B0CB-BAEB45EE29C0}">
|
||||
<RegistryValue Type="string" Value="File Locksmith Shell Extension" />
|
||||
<RegistryValue Type="string" Name="ContextMenuOptIn" Value="" />
|
||||
<RegistryValue Type="string" Key="InprocServer32" Value="[WinUI3AppsInstallFolder]PowerToys.FileLocksmithExt.dll" />
|
||||
<RegistryValue Type="string" Key="InprocServer32" Name="ThreadingModel" Value="Apartment" />
|
||||
</RegistryKey>
|
||||
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\AllFileSystemObjects\ShellEx\ContextMenuHandlers\FileLocksmithExt">
|
||||
<RegistryValue Type="string" Value="{84D68575-E186-46AD-B0CB-BAEB45EE29C0}"/>
|
||||
</RegistryKey>
|
||||
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\Drive\ShellEx\ContextMenuHandlers\FileLocksmithExt">
|
||||
<RegistryValue Type="string" Value="{84D68575-E186-46AD-B0CB-BAEB45EE29C0}"/>
|
||||
</RegistryKey>
|
||||
</Component>
|
||||
</DirectoryRef>
|
||||
|
||||
<ComponentGroup Id="FileLocksmithComponentGroup">
|
||||
@@ -38,7 +23,6 @@
|
||||
</RegistryKey>
|
||||
<RemoveFolder Id="RemoveFolderFileLocksmithAssetsFolder" Directory="FileLocksmithAssetsInstallFolder" On="uninstall"/>
|
||||
</Component>
|
||||
<ComponentRef Id="Module_FileLocksmith" />
|
||||
</ComponentGroup>
|
||||
|
||||
</Fragment>
|
||||
|
||||
@@ -16,71 +16,6 @@
|
||||
<!-- Generated by generateFileComponents.ps1 -->
|
||||
<!--ImageResizerAssetsFiles_Component_Def-->
|
||||
|
||||
<Component Id="Module_ImageResizer_Registry" Win64="yes">
|
||||
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\CLSID\{51B4D7E5-7568-4234-B4BB-47FB3C016A69}\InprocServer32">
|
||||
<RegistryValue Value="[WinUI3AppsInstallFolder]PowerToys.ImageResizerExt.dll" Type="string" />
|
||||
<RegistryValue Name="ThreadingModel" Value="Apartment" Type="string" />
|
||||
</RegistryKey>
|
||||
|
||||
<RegistryValue Root="$(var.RegistryScope)"
|
||||
Key="SOFTWARE\Classes\Directory\ShellEx\DragDropHandlers\ImageResizer"
|
||||
Value="{51B4D7E5-7568-4234-B4BB-47FB3C016A69}"
|
||||
Type="string" />
|
||||
<!-- Registry Keys for the context menu handler for each of the following image formats: bmp, dib, gif, jfif, jpe, jpeg, jpg, jxr, png, rle, tif, tiff, wdp -->
|
||||
<RegistryValue Root="$(var.RegistryScope)"
|
||||
Key="SOFTWARE\Classes\SystemFileAssociations\.bmp\ShellEx\ContextMenuHandlers\ImageResizer"
|
||||
Value="{51B4D7E5-7568-4234-B4BB-47FB3C016A69}"
|
||||
Type="string" />
|
||||
<RegistryValue Root="$(var.RegistryScope)"
|
||||
Key="SOFTWARE\Classes\SystemFileAssociations\.dib\ShellEx\ContextMenuHandlers\ImageResizer"
|
||||
Value="{51B4D7E5-7568-4234-B4BB-47FB3C016A69}"
|
||||
Type="string" />
|
||||
<RegistryValue Root="$(var.RegistryScope)"
|
||||
Key="SOFTWARE\Classes\SystemFileAssociations\.gif\ShellEx\ContextMenuHandlers\ImageResizer"
|
||||
Value="{51B4D7E5-7568-4234-B4BB-47FB3C016A69}"
|
||||
Type="string" />
|
||||
<RegistryValue Root="$(var.RegistryScope)"
|
||||
Key="SOFTWARE\Classes\SystemFileAssociations\.jfif\ShellEx\ContextMenuHandlers\ImageResizer"
|
||||
Value="{51B4D7E5-7568-4234-B4BB-47FB3C016A69}"
|
||||
Type="string" />
|
||||
<RegistryValue Root="$(var.RegistryScope)"
|
||||
Key="SOFTWARE\Classes\SystemFileAssociations\.jpe\ShellEx\ContextMenuHandlers\ImageResizer"
|
||||
Value="{51B4D7E5-7568-4234-B4BB-47FB3C016A69}"
|
||||
Type="string" />
|
||||
<RegistryValue Root="$(var.RegistryScope)"
|
||||
Key="SOFTWARE\Classes\SystemFileAssociations\.jpeg\ShellEx\ContextMenuHandlers\ImageResizer"
|
||||
Value="{51B4D7E5-7568-4234-B4BB-47FB3C016A69}"
|
||||
Type="string" />
|
||||
<RegistryValue Root="$(var.RegistryScope)"
|
||||
Key="SOFTWARE\Classes\SystemFileAssociations\.jpg\ShellEx\ContextMenuHandlers\ImageResizer"
|
||||
Value="{51B4D7E5-7568-4234-B4BB-47FB3C016A69}"
|
||||
Type="string" />
|
||||
<RegistryValue Root="$(var.RegistryScope)"
|
||||
Key="SOFTWARE\Classes\SystemFileAssociations\.jxr\ShellEx\ContextMenuHandlers\ImageResizer"
|
||||
Value="{51B4D7E5-7568-4234-B4BB-47FB3C016A69}"
|
||||
Type="string" />
|
||||
<RegistryValue Root="$(var.RegistryScope)"
|
||||
Key="SOFTWARE\Classes\SystemFileAssociations\.png\ShellEx\ContextMenuHandlers\ImageResizer"
|
||||
Value="{51B4D7E5-7568-4234-B4BB-47FB3C016A69}"
|
||||
Type="string" />
|
||||
<RegistryValue Root="$(var.RegistryScope)"
|
||||
Key="SOFTWARE\Classes\SystemFileAssociations\.rle\ShellEx\ContextMenuHandlers\ImageResizer"
|
||||
Value="{51B4D7E5-7568-4234-B4BB-47FB3C016A69}"
|
||||
Type="string" />
|
||||
<RegistryValue Root="$(var.RegistryScope)"
|
||||
Key="SOFTWARE\Classes\SystemFileAssociations\.tif\ShellEx\ContextMenuHandlers\ImageResizer"
|
||||
Value="{51B4D7E5-7568-4234-B4BB-47FB3C016A69}"
|
||||
Type="string" />
|
||||
<RegistryValue Root="$(var.RegistryScope)"
|
||||
Key="SOFTWARE\Classes\SystemFileAssociations\.tiff\ShellEx\ContextMenuHandlers\ImageResizer"
|
||||
Value="{51B4D7E5-7568-4234-B4BB-47FB3C016A69}"
|
||||
Type="string" />
|
||||
<RegistryValue Root="$(var.RegistryScope)"
|
||||
Key="SOFTWARE\Classes\SystemFileAssociations\.wdp\ShellEx\ContextMenuHandlers\ImageResizer"
|
||||
Value="{51B4D7E5-7568-4234-B4BB-47FB3C016A69}"
|
||||
Type="string" />
|
||||
</Component>
|
||||
|
||||
</DirectoryRef>
|
||||
|
||||
<ComponentGroup Id="ImageResizerComponentGroup">
|
||||
@@ -90,7 +25,6 @@
|
||||
</RegistryKey>
|
||||
<RemoveFolder Id="RemoveFolderImageResizerAssetsFolder" Directory="ImageResizerAssetsFolder" On="uninstall"/>
|
||||
</Component>
|
||||
<ComponentRef Id="Module_ImageResizer_Registry" />
|
||||
</ComponentGroup>
|
||||
</Fragment>
|
||||
</Wix>
|
||||
|
||||
@@ -18,19 +18,6 @@
|
||||
<DirectoryRef Id="NewPlusAssetsInstallFolder" FileSource="$(var.NewPlusAssetsFilesPath)">
|
||||
<!-- Generated by generateFileComponents.ps1 -->
|
||||
<!--NewPlusAssetsFiles_Component_Def-->
|
||||
|
||||
<!-- NewPlus Shell Extension for Win10 registration -->
|
||||
<Component Id="NewPlus_ShellExtension_win10" Guid="D5456D4A-6EEC-4B85-944D-6A6A4A74FFA6" Win64="yes">
|
||||
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\CLSID\{FF90D477-E32A-4BE8-8CC5-A502A97F5401}">
|
||||
<RegistryValue Type="string" Value="NewPlus Shell Extension Win10" />
|
||||
<RegistryValue Type="string" Name="ContextMenuOptIn" Value="" />
|
||||
<RegistryValue Type="string" Key="InprocServer32" Value="[WinUI3AppsInstallFolder]PowerToys.NewPlus.ShellExtension.win10.dll" />
|
||||
<RegistryValue Type="string" Key="InprocServer32" Name="ThreadingModel" Value="Apartment" />
|
||||
</RegistryKey>
|
||||
<RegistryKey Root="$(var.RegistryScope)" Key="SOFTWARE\Classes\Directory\background\ShellEx\ContextMenuHandlers\NewPlusShellExtensionWin10">
|
||||
<RegistryValue Type="string" Value="{FF90D477-E32A-4BE8-8CC5-A502A97F5401}"/>
|
||||
</RegistryKey>
|
||||
</Component>
|
||||
</DirectoryRef>
|
||||
|
||||
<ComponentGroup Id="NewPlusComponentGroup">
|
||||
@@ -40,8 +27,7 @@
|
||||
</RegistryKey>
|
||||
<RemoveFolder Id="RemoveFolderNewPlusAssetsFolder" Directory="NewPlusAssetsInstallFolder" On="uninstall"/>
|
||||
</Component>
|
||||
<ComponentRef Id="NewPlus_ShellExtension_win10" />
|
||||
</ComponentGroup>
|
||||
</ComponentGroup>
|
||||
|
||||
|
||||
<!-- Example templates -->
|
||||
@@ -81,7 +67,7 @@
|
||||
</Component>
|
||||
<ComponentRef Id="NewPlusTemplateFiles_Component" />
|
||||
<ComponentRef Id="NewPlusTemplateSubFiles_Component" />
|
||||
</ComponentGroup>
|
||||
</ComponentGroup>
|
||||
|
||||
</Fragment>
|
||||
</Wix>
|
||||
|
||||
@@ -14,22 +14,6 @@
|
||||
<DirectoryRef Id="PowerRenameAssetsFolder" FileSource="$(var.PowerRenameAssetsFilesPath)">
|
||||
<!-- Generated by generateFileComponents.ps1 -->
|
||||
<!--PowerRenameAssetsFiles_Component_Def-->
|
||||
<!-- !Warning! Make sure to change Component Guid if you update something here -->
|
||||
<Component Id="Module_PowerRename" Guid="40D43079-240E-402D-8CE8-571BFFA71175" Win64="yes">
|
||||
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\CLSID\{0440049F-D1DC-4E46-B27B-98393D79486B}">
|
||||
<RegistryValue Type="string" Value="PowerRename Shell Extension" />
|
||||
<RegistryValue Type="string" Name="ContextMenuOptIn" Value="" />
|
||||
<RegistryValue Type="string" Key="InprocServer32" Value="[WinUI3AppsInstallFolder]PowerToys.PowerRenameExt.dll" />
|
||||
<RegistryValue Type="string" Key="InprocServer32" Name="ThreadingModel" Value="Apartment" />
|
||||
</RegistryKey>
|
||||
<RegistryKey Root="$(var.RegistryScope)" Key="SOFTWARE\Classes\AllFileSystemObjects\ShellEx\ContextMenuHandlers\PowerRenameExt">
|
||||
<RegistryValue Type="string" Value="{0440049F-D1DC-4E46-B27B-98393D79486B}"/>
|
||||
</RegistryKey>
|
||||
<RegistryKey Root="$(var.RegistryScope)" Key="SOFTWARE\Classes\Directory\background\ShellEx\ContextMenuHandlers\PowerRenameExt">
|
||||
<RegistryValue Type="string" Value="{0440049F-D1DC-4E46-B27B-98393D79486B}"/>
|
||||
</RegistryKey>
|
||||
|
||||
</Component>
|
||||
</DirectoryRef>
|
||||
|
||||
<ComponentGroup Id="PowerRenameComponentGroup">
|
||||
@@ -39,7 +23,6 @@
|
||||
</RegistryKey>
|
||||
<RemoveFolder Id="RemoveFolderPowerRenameAssetsFolder" Directory="PowerRenameAssetsFolder" On="uninstall"/>
|
||||
</Component>
|
||||
<ComponentRef Id="Module_PowerRename" />
|
||||
</ComponentGroup>
|
||||
|
||||
</Fragment>
|
||||
|
||||
@@ -176,6 +176,18 @@
|
||||
<Custom Action="UnRegisterContextMenuPackages" Before="RemoveFiles">
|
||||
Installed AND (REMOVE="ALL")
|
||||
</Custom>
|
||||
<Custom Action="CleanImageResizerRuntimeRegistry" Before="RemoveFiles">
|
||||
Installed AND (REMOVE="ALL")
|
||||
</Custom>
|
||||
<Custom Action="CleanFileLocksmithRuntimeRegistry" Before="RemoveFiles">
|
||||
Installed AND (REMOVE="ALL")
|
||||
</Custom>
|
||||
<Custom Action="CleanPowerRenameRuntimeRegistry" Before="RemoveFiles">
|
||||
Installed AND (REMOVE="ALL")
|
||||
</Custom>
|
||||
<Custom Action="CleanNewPlusRuntimeRegistry" Before="RemoveFiles">
|
||||
Installed AND (REMOVE="ALL")
|
||||
</Custom>
|
||||
<Custom Action="UnRegisterCmdPalPackage" Before="RemoveFiles">
|
||||
Installed AND (NOT UPGRADINGPRODUCTCODE) AND (REMOVE="ALL")
|
||||
</Custom>
|
||||
@@ -437,6 +449,35 @@
|
||||
Execute="deferred"
|
||||
BinaryKey="PTCustomActions"
|
||||
DllEntry="UnRegisterContextMenuPackagesCA"
|
||||
/>
|
||||
|
||||
<CustomAction Id="CleanImageResizerRuntimeRegistry"
|
||||
Return="ignore"
|
||||
Impersonate="yes"
|
||||
Execute="deferred"
|
||||
BinaryKey="PTCustomActions"
|
||||
DllEntry="CleanImageResizerRuntimeRegistryCA"
|
||||
/>
|
||||
<CustomAction Id="CleanFileLocksmithRuntimeRegistry"
|
||||
Return="ignore"
|
||||
Impersonate="yes"
|
||||
Execute="deferred"
|
||||
BinaryKey="PTCustomActions"
|
||||
DllEntry="CleanFileLocksmithRuntimeRegistryCA"
|
||||
/>
|
||||
<CustomAction Id="CleanPowerRenameRuntimeRegistry"
|
||||
Return="ignore"
|
||||
Impersonate="yes"
|
||||
Execute="deferred"
|
||||
BinaryKey="PTCustomActions"
|
||||
DllEntry="CleanPowerRenameRuntimeRegistryCA"
|
||||
/>
|
||||
<CustomAction Id="CleanNewPlusRuntimeRegistry"
|
||||
Return="ignore"
|
||||
Impersonate="yes"
|
||||
Execute="deferred"
|
||||
BinaryKey="PTCustomActions"
|
||||
DllEntry="CleanNewPlusRuntimeRegistryCA"
|
||||
/>
|
||||
|
||||
<CustomAction Id="UnRegisterCmdPalPackage"
|
||||
|
||||
@@ -1153,6 +1153,113 @@ UINT __stdcall UnRegisterContextMenuPackagesCA(MSIHANDLE hInstall)
|
||||
return WcaFinalize(er);
|
||||
}
|
||||
|
||||
UINT __stdcall CleanImageResizerRuntimeRegistryCA(MSIHANDLE hInstall)
|
||||
{
|
||||
HRESULT hr = S_OK;
|
||||
UINT er = ERROR_SUCCESS;
|
||||
hr = WcaInitialize(hInstall, "CleanImageResizerRuntimeRegistryCA");
|
||||
|
||||
try
|
||||
{
|
||||
const wchar_t* CLSID_STR = L"{51B4D7E5-7568-4234-B4BB-47FB3C016A69}";
|
||||
const wchar_t* exts[] = { L".bmp", L".dib", L".gif", L".jfif", L".jpe", L".jpeg", L".jpg", L".jxr", L".png", L".rle", L".tif", L".tiff", L".wdp" };
|
||||
|
||||
auto deleteKeyRecursive = [](HKEY root, const std::wstring &path) {
|
||||
RegDeleteTreeW(root, path.c_str());
|
||||
};
|
||||
|
||||
// InprocServer32 chain root CLSID
|
||||
deleteKeyRecursive(HKEY_CURRENT_USER, L"Software\\Classes\\CLSID\\" + std::wstring(CLSID_STR));
|
||||
// DragDrop handler
|
||||
deleteKeyRecursive(HKEY_CURRENT_USER, L"Software\\Classes\\Directory\\ShellEx\\DragDropHandlers\\ImageResizer");
|
||||
// Extensions
|
||||
for (auto ext : exts)
|
||||
{
|
||||
deleteKeyRecursive(HKEY_CURRENT_USER, L"Software\\Classes\\SystemFileAssociations\\" + std::wstring(ext) + L"\\ShellEx\\ContextMenuHandlers\\ImageResizer");
|
||||
}
|
||||
// Sentinel
|
||||
RegDeleteTreeW(HKEY_CURRENT_USER, L"Software\\Microsoft\\PowerToys\\ImageResizer");
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
er = ERROR_INSTALL_FAILURE;
|
||||
}
|
||||
|
||||
er = er == ERROR_SUCCESS ? (SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE) : er;
|
||||
return WcaFinalize(er);
|
||||
}
|
||||
|
||||
UINT __stdcall CleanFileLocksmithRuntimeRegistryCA(MSIHANDLE hInstall)
|
||||
{
|
||||
HRESULT hr = S_OK;
|
||||
UINT er = ERROR_SUCCESS;
|
||||
hr = WcaInitialize(hInstall, "CleanFileLocksmithRuntimeRegistryCA");
|
||||
try
|
||||
{
|
||||
const wchar_t* CLSID_STR = L"{84D68575-E186-46AD-B0CB-BAEB45EE29C0}";
|
||||
auto deleteKeyRecursive = [](HKEY root, const std::wstring& path) {
|
||||
RegDeleteTreeW(root, path.c_str());
|
||||
};
|
||||
deleteKeyRecursive(HKEY_CURRENT_USER, L"Software\\Classes\\CLSID\\" + std::wstring(CLSID_STR));
|
||||
deleteKeyRecursive(HKEY_CURRENT_USER, L"Software\\Classes\\AllFileSystemObjects\\ShellEx\\ContextMenuHandlers\\FileLocksmithExt");
|
||||
deleteKeyRecursive(HKEY_CURRENT_USER, L"Software\\Classes\\Drive\\ShellEx\\ContextMenuHandlers\\FileLocksmithExt");
|
||||
RegDeleteTreeW(HKEY_CURRENT_USER, L"Software\\Microsoft\\PowerToys\\FileLocksmith");
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
er = ERROR_INSTALL_FAILURE;
|
||||
}
|
||||
er = er == ERROR_SUCCESS ? (SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE) : er;
|
||||
return WcaFinalize(er);
|
||||
}
|
||||
|
||||
UINT __stdcall CleanPowerRenameRuntimeRegistryCA(MSIHANDLE hInstall)
|
||||
{
|
||||
HRESULT hr = S_OK;
|
||||
UINT er = ERROR_SUCCESS;
|
||||
hr = WcaInitialize(hInstall, "CleanPowerRenameRuntimeRegistryCA");
|
||||
try
|
||||
{
|
||||
const wchar_t* CLSID_STR = L"{0440049F-D1DC-4E46-B27B-98393D79486B}";
|
||||
auto deleteKeyRecursive = [](HKEY root, const std::wstring& path) {
|
||||
RegDeleteTreeW(root, path.c_str());
|
||||
};
|
||||
deleteKeyRecursive(HKEY_CURRENT_USER, L"Software\\Classes\\CLSID\\" + std::wstring(CLSID_STR));
|
||||
deleteKeyRecursive(HKEY_CURRENT_USER, L"Software\\Classes\\AllFileSystemObjects\\ShellEx\\ContextMenuHandlers\\PowerRenameExt");
|
||||
deleteKeyRecursive(HKEY_CURRENT_USER, L"Software\\Classes\\Directory\\background\\ShellEx\\ContextMenuHandlers\\PowerRenameExt");
|
||||
RegDeleteTreeW(HKEY_CURRENT_USER, L"Software\\Microsoft\\PowerToys\\PowerRename");
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
er = ERROR_INSTALL_FAILURE;
|
||||
}
|
||||
er = er == ERROR_SUCCESS ? (SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE) : er;
|
||||
return WcaFinalize(er);
|
||||
}
|
||||
|
||||
UINT __stdcall CleanNewPlusRuntimeRegistryCA(MSIHANDLE hInstall)
|
||||
{
|
||||
HRESULT hr = S_OK;
|
||||
UINT er = ERROR_SUCCESS;
|
||||
hr = WcaInitialize(hInstall, "CleanNewPlusRuntimeRegistryCA");
|
||||
try
|
||||
{
|
||||
const wchar_t* CLSID_STR = L"{FF90D477-E32A-4BE8-8CC5-A502A97F5401}";
|
||||
auto deleteKeyRecursive = [](HKEY root, const std::wstring& path) {
|
||||
RegDeleteTreeW(root, path.c_str());
|
||||
};
|
||||
deleteKeyRecursive(HKEY_CURRENT_USER, L"Software\\Classes\\CLSID\\" + std::wstring(CLSID_STR));
|
||||
deleteKeyRecursive(HKEY_CURRENT_USER, L"Software\\Classes\\Directory\\background\\ShellEx\\ContextMenuHandlers\\NewPlusShellExtensionWin10");
|
||||
RegDeleteTreeW(HKEY_CURRENT_USER, L"Software\\Microsoft\\PowerToys\\NewPlus");
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
er = ERROR_INSTALL_FAILURE;
|
||||
}
|
||||
er = er == ERROR_SUCCESS ? (SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE) : er;
|
||||
return WcaFinalize(er);
|
||||
}
|
||||
|
||||
UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall)
|
||||
{
|
||||
HRESULT hr = S_OK;
|
||||
|
||||
@@ -28,3 +28,7 @@ EXPORTS
|
||||
UninstallCommandNotFoundModuleCA
|
||||
UpgradeCommandNotFoundModuleCA
|
||||
UnsetAdvancedPasteAPIKeyCA
|
||||
CleanImageResizerRuntimeRegistryCA
|
||||
CleanFileLocksmithRuntimeRegistryCA
|
||||
CleanPowerRenameRuntimeRegistryCA
|
||||
CleanNewPlusRuntimeRegistryCA
|
||||
|
||||
@@ -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.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.PowerToys.UITest
|
||||
{
|
||||
/// <summary>
|
||||
@@ -25,8 +27,9 @@ namespace Microsoft.PowerToys.UITest
|
||||
/// </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>
|
||||
/// <param name="charDelayMS">Delay in milliseconds between each character. Default is 0 (no delay).</param>
|
||||
/// <returns>The current TextBox instance.</returns>
|
||||
public TextBox SetText(string value, bool clearText = true)
|
||||
public TextBox SetText(string value, bool clearText = true, int charDelayMS = 0)
|
||||
{
|
||||
if (clearText)
|
||||
{
|
||||
@@ -39,10 +42,36 @@ namespace Microsoft.PowerToys.UITest
|
||||
Task.Delay(500).Wait();
|
||||
}
|
||||
|
||||
PerformAction((actions, windowElement) =>
|
||||
// TODO: CmdPal bug – when inputting text, characters are swallowed too quickly.
|
||||
// This should be fixed within CmdPal itself.
|
||||
// Temporary workaround: introduce a delay between character inputs to avoid the issue
|
||||
if (charDelayMS > 0 || EnvironmentConfig.IsInPipeline)
|
||||
{
|
||||
windowElement.SendKeys(value);
|
||||
});
|
||||
// Send text character by character with delay (if specified or in pipeline)
|
||||
PerformAction((actions, windowElement) =>
|
||||
{
|
||||
foreach (char c in value)
|
||||
{
|
||||
windowElement.SendKeys(c.ToString());
|
||||
if (charDelayMS > 0)
|
||||
{
|
||||
Task.Delay(charDelayMS).Wait();
|
||||
}
|
||||
else if (EnvironmentConfig.IsInPipeline)
|
||||
{
|
||||
Task.Delay(50).Wait();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
// No character delay - send all text at once (original behavior)
|
||||
PerformAction((actions, windowElement) =>
|
||||
{
|
||||
windowElement.SendKeys(value);
|
||||
});
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
45
src/common/UITestAutomation/EnvironmentConfig.cs
Normal file
45
src/common/UITestAutomation/EnvironmentConfig.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
// 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 Microsoft.PowerToys.UITest
|
||||
{
|
||||
/// <summary>
|
||||
/// Centralized configuration for all environment variables used in UI tests.
|
||||
/// </summary>
|
||||
public static class EnvironmentConfig
|
||||
{
|
||||
private static readonly Lazy<bool> _isInPipeline = new(() =>
|
||||
!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("platform")));
|
||||
|
||||
private static readonly Lazy<bool> _useInstallerForTest = new(() =>
|
||||
{
|
||||
string? envValue = Environment.GetEnvironmentVariable("useInstallerForTest") ??
|
||||
Environment.GetEnvironmentVariable("USEINSTALLERFORTEST");
|
||||
return !string.IsNullOrEmpty(envValue) && bool.TryParse(envValue, out bool result) && result;
|
||||
});
|
||||
|
||||
private static readonly Lazy<string?> _platform = new(() =>
|
||||
Environment.GetEnvironmentVariable("platform"));
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the tests are running in a CI/CD pipeline.
|
||||
/// Determined by the presence of the "platform" environment variable.
|
||||
/// </summary>
|
||||
public static bool IsInPipeline => _isInPipeline.Value;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether to use installer paths for testing.
|
||||
/// Checks both "useInstallerForTest" and "USEINSTALLERFORTEST" environment variables.
|
||||
/// </summary>
|
||||
public static bool UseInstallerForTest => _useInstallerForTest.Value;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the platform name from the environment variable.
|
||||
/// Typically used in CI/CD pipelines to identify the build platform.
|
||||
/// </summary>
|
||||
public static string? Platform => _platform.Value;
|
||||
}
|
||||
}
|
||||
@@ -92,9 +92,7 @@ namespace Microsoft.PowerToys.UITest
|
||||
private ModuleConfigData()
|
||||
{
|
||||
// Check if we should use installer paths from environment variable
|
||||
string? useInstallerForTestEnv =
|
||||
Environment.GetEnvironmentVariable("useInstallerForTest") ?? Environment.GetEnvironmentVariable("USEINSTALLERFORTEST");
|
||||
UseInstallerForTest = !string.IsNullOrEmpty(useInstallerForTestEnv) && bool.TryParse(useInstallerForTestEnv, out bool result) && result;
|
||||
UseInstallerForTest = EnvironmentConfig.UseInstallerForTest;
|
||||
|
||||
// Module information including executable name, window name, and optional subdirectory
|
||||
ModuleInfo = new Dictionary<PowerToysModule, ModuleInfo>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
@@ -37,6 +38,9 @@ namespace Microsoft.PowerToys.UITest
|
||||
private PowerToysModule scope;
|
||||
private string[]? commandLineArgs;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether to use installer paths for testing.
|
||||
/// </summary>
|
||||
private bool UseInstallerForTest { get; }
|
||||
|
||||
[UnconditionalSuppressMessage("SingleFile", "IL3000:Avoid accessing Assembly file path when publishing as a single file", Justification = "<Pending>")]
|
||||
@@ -45,9 +49,7 @@ namespace Microsoft.PowerToys.UITest
|
||||
this.scope = scope;
|
||||
this.commandLineArgs = commandLineArgs;
|
||||
this.sessionPath = ModuleConfigData.Instance.GetModulePath(scope);
|
||||
string? useInstallerForTestEnv =
|
||||
Environment.GetEnvironmentVariable("useInstallerForTest") ?? Environment.GetEnvironmentVariable("USEINSTALLERFORTEST");
|
||||
UseInstallerForTest = !string.IsNullOrEmpty(useInstallerForTestEnv) && bool.TryParse(useInstallerForTestEnv, out bool result) && result;
|
||||
UseInstallerForTest = EnvironmentConfig.UseInstallerForTest;
|
||||
this.locationPath = UseInstallerForTest ? string.Empty : Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
|
||||
|
||||
CheckWinAppDriverAndRoot();
|
||||
@@ -136,6 +138,10 @@ namespace Microsoft.PowerToys.UITest
|
||||
{
|
||||
TryLaunchPowerToysSettings(opts);
|
||||
}
|
||||
else if (scope == PowerToysModule.CommandPalette && UseInstallerForTest)
|
||||
{
|
||||
TryLaunchCommandPalette(opts);
|
||||
}
|
||||
else
|
||||
{
|
||||
opts.AddAdditionalCapability("app", appPath);
|
||||
@@ -163,48 +169,77 @@ namespace Microsoft.PowerToys.UITest
|
||||
|
||||
private void TryLaunchPowerToysSettings(AppiumOptions opts)
|
||||
{
|
||||
CheckWinAppDriverAndRoot();
|
||||
|
||||
var runnerProcessInfo = new ProcessStartInfo
|
||||
try
|
||||
{
|
||||
FileName = locationPath + runnerPath,
|
||||
Verb = "runas",
|
||||
Arguments = "--open-settings",
|
||||
};
|
||||
|
||||
ExitExe(runnerProcessInfo.FileName);
|
||||
runner = Process.Start(runnerProcessInfo);
|
||||
Thread.Sleep(5000);
|
||||
|
||||
// Exit CmdPal UI before launching new process if use installer for test
|
||||
ExitExeByName("Microsoft.CmdPal.UI");
|
||||
|
||||
if (root != null)
|
||||
{
|
||||
const int maxRetries = 5;
|
||||
const int delayMs = 5000;
|
||||
var windowName = "PowerToys Settings";
|
||||
|
||||
for (int attempt = 1; attempt <= maxRetries; attempt++)
|
||||
var runnerProcessInfo = new ProcessStartInfo
|
||||
{
|
||||
var settingsWindow = ApiHelper.FindDesktopWindowHandler(
|
||||
[windowName, AdministratorPrefix + windowName]);
|
||||
FileName = locationPath + runnerPath,
|
||||
Verb = "runas",
|
||||
Arguments = "--open-settings",
|
||||
};
|
||||
|
||||
if (settingsWindow.Count > 0)
|
||||
{
|
||||
var hexHwnd = settingsWindow[0].HWnd.ToString("x");
|
||||
opts.AddAdditionalCapability("appTopLevelWindow", hexHwnd);
|
||||
return;
|
||||
}
|
||||
ExitExe(runnerProcessInfo.FileName);
|
||||
runner = Process.Start(runnerProcessInfo);
|
||||
|
||||
if (attempt < maxRetries)
|
||||
{
|
||||
Thread.Sleep(delayMs);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new TimeoutException("Failed to find PowerToys Settings window after multiple attempts.");
|
||||
}
|
||||
WaitForWindowAndSetCapability(opts, "PowerToys Settings", 5000, 5);
|
||||
|
||||
// Exit CmdPal UI before launching new process if use installer for test
|
||||
ExitExeByName("Microsoft.CmdPal.UI");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to launch PowerToys Settings: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void TryLaunchCommandPalette(AppiumOptions opts)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Exit any existing CmdPal UI process
|
||||
ExitExeByName("Microsoft.CmdPal.UI");
|
||||
|
||||
var processStartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "cmd.exe",
|
||||
Arguments = "/c start shell:appsFolder\\Microsoft.CommandPalette_8wekyb3d8bbwe!App",
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
WindowStyle = ProcessWindowStyle.Hidden,
|
||||
};
|
||||
|
||||
var process = Process.Start(processStartInfo);
|
||||
process?.WaitForExit();
|
||||
|
||||
WaitForWindowAndSetCapability(opts, "Command Palette", 5000, 10);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to launch Command Palette: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void WaitForWindowAndSetCapability(AppiumOptions opts, string windowName, int delayMs, int maxRetries)
|
||||
{
|
||||
for (int attempt = 1; attempt <= maxRetries; attempt++)
|
||||
{
|
||||
var window = ApiHelper.FindDesktopWindowHandler(
|
||||
[windowName, AdministratorPrefix + windowName]);
|
||||
|
||||
if (window.Count > 0)
|
||||
{
|
||||
var hexHwnd = window[0].HWnd.ToString("x");
|
||||
opts.AddAdditionalCapability("appTopLevelWindow", hexHwnd);
|
||||
return;
|
||||
}
|
||||
|
||||
if (attempt < maxRetries)
|
||||
{
|
||||
Thread.Sleep(delayMs);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new TimeoutException($"Failed to find {windowName} window after multiple attempts.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using OpenQA.Selenium;
|
||||
|
||||
@@ -20,6 +21,9 @@ namespace Microsoft.PowerToys.UITest
|
||||
|
||||
public required Session Session { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the tests are running in a CI/CD pipeline.
|
||||
/// </summary>
|
||||
public bool IsInPipeline { get; }
|
||||
|
||||
public string? ScreenshotDirectory { get; set; }
|
||||
@@ -34,8 +38,8 @@ namespace Microsoft.PowerToys.UITest
|
||||
|
||||
public UITestBase(PowerToysModule scope = PowerToysModule.PowerToysSettings, WindowSize size = WindowSize.UnSpecified, string[]? commandLineArgs = null)
|
||||
{
|
||||
this.IsInPipeline = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("platform"));
|
||||
Console.WriteLine($"Running tests on platform: {Environment.GetEnvironmentVariable("platform")}");
|
||||
this.IsInPipeline = EnvironmentConfig.IsInPipeline;
|
||||
Console.WriteLine($"Running tests on platform: {EnvironmentConfig.Platform}");
|
||||
if (IsInPipeline)
|
||||
{
|
||||
NativeMethods.ChangeDisplayResolution(1920, 1080);
|
||||
@@ -56,6 +60,7 @@ namespace Microsoft.PowerToys.UITest
|
||||
[TestInitialize]
|
||||
public void TestInit()
|
||||
{
|
||||
KeyboardHelper.SendKeys(Key.Win, Key.M);
|
||||
CloseOtherApplications();
|
||||
if (IsInPipeline)
|
||||
{
|
||||
@@ -247,6 +252,174 @@ namespace Microsoft.PowerToys.UITest
|
||||
return this.Session.Has<Element>(name, timeoutMS, global);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds an element using partial name matching (contains).
|
||||
/// Useful for finding windows with variable titles like "filename.txt - Notepad" or "filename - Notepad".
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The class of the element, should be Element or its derived class.</typeparam>
|
||||
/// <param name="partialName">Part of the name to search for.</param>
|
||||
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
|
||||
/// <returns>The found element.</returns>
|
||||
protected T FindByPartialName<T>(string partialName, int timeoutMS = 5000, bool global = false)
|
||||
where T : Element, new()
|
||||
{
|
||||
return Session.Find<T>(By.XPath($"//*[contains(@Name, '{partialName}')]"), timeoutMS, global);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds an element using partial name matching (contains).
|
||||
/// </summary>
|
||||
/// <param name="partialName">Part of the name to search for.</param>
|
||||
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
|
||||
/// <returns>The found element.</returns>
|
||||
protected Element FindByPartialName(string partialName, int timeoutMS = 5000, bool global = false)
|
||||
{
|
||||
return FindByPartialName<Element>(partialName, timeoutMS, global);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Base method for finding elements by selector and filtering by name pattern.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The class of the element, should be Element or its derived class.</typeparam>
|
||||
/// <param name="selector">The selector to find initial candidates.</param>
|
||||
/// <param name="namePattern">Pattern to match against the Name attribute. Supports regex patterns.</param>
|
||||
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
|
||||
/// <param name="errorMessage">Custom error message when no element is found.</param>
|
||||
/// <returns>The found element.</returns>
|
||||
private T FindByNamePattern<T>(By selector, string namePattern, int timeoutMS = 5000, bool global = false, string? errorMessage = null)
|
||||
where T : Element, new()
|
||||
{
|
||||
var elements = Session.FindAll<T>(selector, timeoutMS, global);
|
||||
var regex = new Regex(namePattern, RegexOptions.IgnoreCase);
|
||||
|
||||
foreach (var element in elements)
|
||||
{
|
||||
var name = element.GetAttribute("Name");
|
||||
if (!string.IsNullOrEmpty(name) && regex.IsMatch(name))
|
||||
{
|
||||
return element;
|
||||
}
|
||||
}
|
||||
|
||||
throw new NoSuchElementException(errorMessage ?? $"No element found matching pattern: {namePattern}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds an element using regular expression pattern matching.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The class of the element, should be Element or its derived class.</typeparam>
|
||||
/// <param name="pattern">Regular expression pattern to match against the Name attribute.</param>
|
||||
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
|
||||
/// <returns>The found element.</returns>
|
||||
protected T FindByPattern<T>(string pattern, int timeoutMS = 5000, bool global = false)
|
||||
where T : Element, new()
|
||||
{
|
||||
return FindByNamePattern<T>(By.XPath("//*[@Name]"), pattern, timeoutMS, global, $"No element found matching pattern: {pattern}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds an element using regular expression pattern matching.
|
||||
/// </summary>
|
||||
/// <param name="pattern">Regular expression pattern to match against the Name attribute.</param>
|
||||
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
|
||||
/// <returns>The found element.</returns>
|
||||
protected Element FindByPattern(string pattern, int timeoutMS = 5000, bool global = false)
|
||||
{
|
||||
return FindByPattern<Element>(pattern, timeoutMS, global);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds an element by ClassName only.
|
||||
/// Returns the first element found with the specified ClassName.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The class of the element, should be Element or its derived class.</typeparam>
|
||||
/// <param name="className">The ClassName to search for (e.g., "Notepad", "CabinetWClass").</param>
|
||||
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
|
||||
/// <returns>The found element.</returns>
|
||||
protected T FindByClassName<T>(string className, int timeoutMS = 5000, bool global = false)
|
||||
where T : Element, new()
|
||||
{
|
||||
return Session.Find<T>(By.ClassName(className), timeoutMS, global);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds an element by ClassName only.
|
||||
/// Returns the first element found with the specified ClassName.
|
||||
/// </summary>
|
||||
/// <param name="className">The ClassName to search for (e.g., "Notepad", "CabinetWClass").</param>
|
||||
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
|
||||
/// <returns>The found element.</returns>
|
||||
protected Element FindByClassName(string className, int timeoutMS = 5000, bool global = false)
|
||||
{
|
||||
return FindByClassName<Element>(className, timeoutMS, global);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds an element by ClassName and matches its Name attribute using regex pattern matching.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The class of the element, should be Element or its derived class.</typeparam>
|
||||
/// <param name="className">The ClassName to search for (e.g., "Notepad", "CabinetWClass").</param>
|
||||
/// <param name="namePattern">Pattern to match against the Name attribute. Supports regex patterns.</param>
|
||||
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
|
||||
/// <returns>The found element.</returns>
|
||||
protected T FindByClassNameAndNamePattern<T>(string className, string namePattern, int timeoutMS = 5000, bool global = false)
|
||||
where T : Element, new()
|
||||
{
|
||||
return FindByNamePattern<T>(By.ClassName(className), namePattern, timeoutMS, global, $"No element with ClassName '{className}' found matching name pattern: {namePattern}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds an element by ClassName and matches its Name attribute using regex pattern matching.
|
||||
/// </summary>
|
||||
/// <param name="className">The ClassName to search for (e.g., "Notepad", "CabinetWClass").</param>
|
||||
/// <param name="namePattern">Pattern to match against the Name attribute. Supports regex patterns.</param>
|
||||
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
|
||||
/// <returns>The found element.</returns>
|
||||
protected Element FindByClassNameAndNamePattern(string className, string namePattern, int timeoutMS = 5000, bool global = false)
|
||||
{
|
||||
return FindByClassNameAndNamePattern<Element>(className, namePattern, timeoutMS, global);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds a Notepad window regardless of whether the file extension is shown in the title.
|
||||
/// Handles both "filename.txt - Notepad" and "filename - Notepad" formats.
|
||||
/// Uses ClassName to efficiently find Notepad windows first, then matches the filename.
|
||||
/// </summary>
|
||||
/// <param name="baseFileName">The base filename without extension (e.g., "test" for "test.txt").</param>
|
||||
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
|
||||
/// <returns>The found Notepad window element.</returns>
|
||||
protected Element FindNotepadWindow(string baseFileName, int timeoutMS = 5000, bool global = false)
|
||||
{
|
||||
string pattern = $@"^{Regex.Escape(baseFileName)}(\.\w+)?(\s*-\s*|\s+)Notepad$";
|
||||
return FindByClassNameAndNamePattern("Notepad", pattern, timeoutMS, global);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds an Explorer window regardless of the folder or file name display format.
|
||||
/// Handles various Explorer window title formats like "FolderName", "FileName", "FolderName - File Explorer", etc.
|
||||
/// Uses ClassName to efficiently find Explorer windows first, then matches the folder or file name.
|
||||
/// </summary>
|
||||
/// <param name="folderName">The folder or file name to search for (e.g., "Documents", "Desktop", "test.txt").</param>
|
||||
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
|
||||
/// <returns>The found Explorer window element.</returns>
|
||||
protected Element FindExplorerWindow(string folderName, int timeoutMS = 5000, bool global = false)
|
||||
{
|
||||
string pattern = $@"^{Regex.Escape(folderName)}(\s*-\s*(File\s+Explorer|Windows\s+Explorer))?$";
|
||||
return FindByClassNameAndNamePattern("CabinetWClass", pattern, timeoutMS, global);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds an Explorer window by partial folder path.
|
||||
/// Useful when the full path might be displayed in the title.
|
||||
/// </summary>
|
||||
/// <param name="partialPath">Part of the folder path to search for.</param>
|
||||
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
|
||||
/// <returns>The found Explorer window element.</returns>
|
||||
protected Element FindExplorerByPartialPath(string partialPath, int timeoutMS = 5000, bool global = false)
|
||||
{
|
||||
return FindByPartialName(partialPath, timeoutMS, global);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds all elements by selector.
|
||||
/// Shortcut for this.Session.FindAll<T>(by, timeoutMS)
|
||||
|
||||
@@ -27,10 +27,8 @@ namespace Microsoft.PowerToys.UITest
|
||||
[RequiresUnreferencedCode("This method uses reflection which may not be compatible with trimming.")]
|
||||
public static void AreEqual(TestContext? testContext, Element element, string scenarioSubname = "")
|
||||
{
|
||||
var pipelinePlatform = Environment.GetEnvironmentVariable("platform");
|
||||
|
||||
// Perform visual validation only in the pipeline
|
||||
if (string.IsNullOrEmpty(pipelinePlatform))
|
||||
if (!EnvironmentConfig.IsInPipeline)
|
||||
{
|
||||
Console.WriteLine("Skip visual validation in the local run.");
|
||||
return;
|
||||
@@ -55,11 +53,11 @@ namespace Microsoft.PowerToys.UITest
|
||||
|
||||
if (string.IsNullOrWhiteSpace(scenarioSubname))
|
||||
{
|
||||
scenarioSubname = string.Join("_", callerClassName, callerName, pipelinePlatform);
|
||||
scenarioSubname = string.Join("_", callerClassName, callerName, EnvironmentConfig.Platform);
|
||||
}
|
||||
else
|
||||
{
|
||||
scenarioSubname = string.Join("_", callerClassName, callerName, scenarioSubname.Trim(), pipelinePlatform);
|
||||
scenarioSubname = string.Join("_", callerClassName, callerName, scenarioSubname.Trim(), EnvironmentConfig.Platform);
|
||||
}
|
||||
|
||||
var baselineImageResourceName = callerMethod!.DeclaringType!.Assembly.GetManifestResourceNames().Where(name => name.Contains(scenarioSubname)).FirstOrDefault();
|
||||
|
||||
266
src/common/utils/shell_ext_registration.h
Normal file
266
src/common/utils/shell_ext_registration.h
Normal file
@@ -0,0 +1,266 @@
|
||||
// Shared runtime shell extension registration utility for PowerToys modules.
|
||||
// Provides a generic EnsureRegistered function so individual modules only need
|
||||
// to supply a specification (CLSID, sentinel, handler key paths, etc.).
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <windows.h>
|
||||
#include <shlwapi.h>
|
||||
|
||||
#include "../logger/logger.h"
|
||||
|
||||
namespace runtime_shell_ext
|
||||
{
|
||||
struct Spec
|
||||
{
|
||||
// Mandatory
|
||||
std::wstring clsid; // e.g. {GUID}
|
||||
std::wstring sentinelKey; // e.g. Software\\Microsoft\\PowerToys\\ModuleName
|
||||
std::wstring sentinelValue; // e.g. ContextMenuRegistered
|
||||
std::vector<std::wstring> dllFileCandidates; // relative filenames (pick first existing)
|
||||
std::vector<std::wstring> contextMenuHandlerKeyPaths; // full HKCU relative paths where default value = CLSID
|
||||
|
||||
// Optional
|
||||
std::wstring friendlyName; // if non-empty written as default under CLSID root
|
||||
bool writeOptInEmptyValue = true; // write ContextMenuOptIn="" under CLSID root (legacy pattern)
|
||||
bool writeThreadingModel = true; // write Apartment threading model
|
||||
std::vector<std::wstring> extraAssociationPaths; // additional key paths (DragDropHandlers etc.) default=CLSID
|
||||
std::vector<std::wstring> systemFileAssocExtensions; // e.g. .png -> Software\\Classes\\SystemFileAssociations\\.png\\ShellEx\\ContextMenuHandlers\\<HandlerName>
|
||||
std::wstring systemFileAssocHandlerName; // e.g. ImageResizer
|
||||
std::wstring representativeSystemExt; // used to decide if associations need repair (.png)
|
||||
bool logRepairs = true;
|
||||
};
|
||||
|
||||
namespace detail
|
||||
{
|
||||
// Minimal RAII wrapper for HKEY
|
||||
struct unique_hkey
|
||||
{
|
||||
HKEY h{ nullptr };
|
||||
unique_hkey() = default;
|
||||
explicit unique_hkey(HKEY handle) : h(handle) {}
|
||||
~unique_hkey() { if (h) RegCloseKey(h); }
|
||||
unique_hkey(const unique_hkey&) = delete;
|
||||
unique_hkey& operator=(const unique_hkey&) = delete;
|
||||
unique_hkey(unique_hkey&& other) noexcept : h(other.h) { other.h = nullptr; }
|
||||
unique_hkey& operator=(unique_hkey&& other) noexcept { if (this != &other) { if (h) RegCloseKey(h); h = other.h; other.h = nullptr; } return *this; }
|
||||
HKEY get() const { return h; }
|
||||
HKEY* put() { if (h) { RegCloseKey(h); h = nullptr; } return &h; }
|
||||
};
|
||||
inline std::wstring base_dir_from_module(HMODULE h)
|
||||
{
|
||||
wchar_t buf[MAX_PATH];
|
||||
if (GetModuleFileNameW(h, buf, MAX_PATH))
|
||||
{
|
||||
PathRemoveFileSpecW(buf);
|
||||
return buf;
|
||||
}
|
||||
return L"";
|
||||
}
|
||||
|
||||
inline std::wstring pick_existing_dll(const std::wstring& base, const std::vector<std::wstring>& candidates)
|
||||
{
|
||||
for (const auto& rel : candidates)
|
||||
{
|
||||
std::wstring full = base + L"\\" + rel;
|
||||
if (GetFileAttributesW(full.c_str()) != INVALID_FILE_ATTRIBUTES)
|
||||
{
|
||||
return full;
|
||||
}
|
||||
}
|
||||
if (!candidates.empty())
|
||||
{
|
||||
return base + L"\\" + candidates.front();
|
||||
}
|
||||
return L"";
|
||||
}
|
||||
|
||||
inline bool sentinel_exists(const Spec& spec)
|
||||
{
|
||||
unique_hkey key;
|
||||
if (RegOpenKeyExW(HKEY_CURRENT_USER, spec.sentinelKey.c_str(), 0, KEY_READ, key.put()) != ERROR_SUCCESS)
|
||||
return false;
|
||||
DWORD v = 0; DWORD sz = sizeof(v);
|
||||
return RegQueryValueExW(key.get(), spec.sentinelValue.c_str(), nullptr, nullptr, reinterpret_cast<LPBYTE>(&v), &sz) == ERROR_SUCCESS && v == 1;
|
||||
}
|
||||
|
||||
inline void write_sentinel(const Spec& spec)
|
||||
{
|
||||
unique_hkey key;
|
||||
if (RegCreateKeyExW(HKEY_CURRENT_USER, spec.sentinelKey.c_str(), 0, nullptr, 0, KEY_WRITE, nullptr, key.put(), nullptr) == ERROR_SUCCESS)
|
||||
{
|
||||
DWORD one = 1;
|
||||
RegSetValueExW(key.get(), spec.sentinelValue.c_str(), 0, REG_DWORD, reinterpret_cast<const BYTE*>(&one), sizeof(one));
|
||||
}
|
||||
}
|
||||
|
||||
inline void write_inproc_server(const Spec& spec, const std::wstring& dllPath)
|
||||
{
|
||||
using namespace std::string_literals;
|
||||
std::wstring clsidRoot = L"Software\\Classes\\CLSID\\"s + spec.clsid;
|
||||
std::wstring inprocKey = clsidRoot + L"\\InprocServer32";
|
||||
{
|
||||
unique_hkey key;
|
||||
if (RegCreateKeyExW(HKEY_CURRENT_USER, clsidRoot.c_str(), 0, nullptr, 0, KEY_WRITE, nullptr, key.put(), nullptr) == ERROR_SUCCESS)
|
||||
{
|
||||
if (!spec.friendlyName.empty())
|
||||
{
|
||||
RegSetValueExW(key.get(), nullptr, 0, REG_SZ, reinterpret_cast<const BYTE*>(spec.friendlyName.c_str()), static_cast<DWORD>((spec.friendlyName.size() + 1) * sizeof(wchar_t)));
|
||||
}
|
||||
if (spec.writeOptInEmptyValue)
|
||||
{
|
||||
const wchar_t* optIn = L"ContextMenuOptIn";
|
||||
const wchar_t empty = L'\0';
|
||||
RegSetValueExW(key.get(), optIn, 0, REG_SZ, reinterpret_cast<const BYTE*>(&empty), sizeof(empty));
|
||||
}
|
||||
}
|
||||
}
|
||||
unique_hkey key;
|
||||
if (RegCreateKeyExW(HKEY_CURRENT_USER, inprocKey.c_str(), 0, nullptr, 0, KEY_WRITE, nullptr, key.put(), nullptr) == ERROR_SUCCESS)
|
||||
{
|
||||
RegSetValueExW(key.get(), nullptr, 0, REG_SZ, reinterpret_cast<const BYTE*>(dllPath.c_str()), static_cast<DWORD>((dllPath.size() + 1) * sizeof(wchar_t)));
|
||||
if (spec.writeThreadingModel)
|
||||
{
|
||||
const wchar_t* tm = L"Apartment";
|
||||
RegSetValueExW(key.get(), L"ThreadingModel", 0, REG_SZ, reinterpret_cast<const BYTE*>(tm), static_cast<DWORD>((wcslen(tm) + 1) * sizeof(wchar_t)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline std::wstring read_inproc_server(const Spec& spec)
|
||||
{
|
||||
using namespace std::string_literals;
|
||||
std::wstring inprocKey = L"Software\\Classes\\CLSID\\"s + spec.clsid + L"\\InprocServer32";
|
||||
unique_hkey key;
|
||||
if (RegOpenKeyExW(HKEY_CURRENT_USER, inprocKey.c_str(), 0, KEY_READ, key.put()) != ERROR_SUCCESS)
|
||||
return L"";
|
||||
wchar_t buf[MAX_PATH]; DWORD sz = sizeof(buf);
|
||||
if (RegQueryValueExW(key.get(), nullptr, nullptr, nullptr, reinterpret_cast<LPBYTE>(buf), &sz) == ERROR_SUCCESS)
|
||||
return std::wstring(buf);
|
||||
return L"";
|
||||
}
|
||||
|
||||
inline void write_default_value_key(const std::wstring& keyPath, const std::wstring& value)
|
||||
{
|
||||
unique_hkey key;
|
||||
if (RegCreateKeyExW(HKEY_CURRENT_USER, keyPath.c_str(), 0, nullptr, 0, KEY_WRITE, nullptr, key.put(), nullptr) == ERROR_SUCCESS)
|
||||
{
|
||||
RegSetValueExW(key.get(), nullptr, 0, REG_SZ, reinterpret_cast<const BYTE*>(value.c_str()), static_cast<DWORD>((value.size() + 1) * sizeof(wchar_t)));
|
||||
}
|
||||
}
|
||||
|
||||
inline bool representative_association_exists(const Spec& spec)
|
||||
{
|
||||
using namespace std::string_literals;
|
||||
if (spec.representativeSystemExt.empty() || spec.systemFileAssocHandlerName.empty())
|
||||
return true;
|
||||
std::wstring keyPath = L"Software\\Classes\\SystemFileAssociations\\"s + spec.representativeSystemExt + L"\\ShellEx\\ContextMenuHandlers\\" + spec.systemFileAssocHandlerName;
|
||||
unique_hkey key;
|
||||
return RegOpenKeyExW(HKEY_CURRENT_USER, keyPath.c_str(), 0, KEY_READ, key.put()) == ERROR_SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
inline bool EnsureRegistered(const Spec& spec, HMODULE moduleInstance)
|
||||
{
|
||||
using namespace std::string_literals;
|
||||
auto base = detail::base_dir_from_module(moduleInstance);
|
||||
auto dllPath = detail::pick_existing_dll(base, spec.dllFileCandidates);
|
||||
if (dllPath.empty())
|
||||
{
|
||||
Logger::error(L"Runtime registration: cannot locate dll path for CLSID {}", spec.clsid);
|
||||
return false;
|
||||
}
|
||||
bool exists = detail::sentinel_exists(spec);
|
||||
bool repaired = false;
|
||||
if (exists)
|
||||
{
|
||||
auto current = detail::read_inproc_server(spec);
|
||||
if (_wcsicmp(current.c_str(), dllPath.c_str()) != 0)
|
||||
{
|
||||
detail::write_inproc_server(spec, dllPath);
|
||||
repaired = true;
|
||||
}
|
||||
if (!detail::representative_association_exists(spec))
|
||||
{
|
||||
repaired = true;
|
||||
}
|
||||
}
|
||||
if (!exists)
|
||||
{
|
||||
detail::write_inproc_server(spec, dllPath);
|
||||
}
|
||||
if (!exists || repaired)
|
||||
{
|
||||
for (const auto& path : spec.contextMenuHandlerKeyPaths)
|
||||
{
|
||||
detail::write_default_value_key(path, spec.clsid);
|
||||
}
|
||||
for (const auto& path : spec.extraAssociationPaths)
|
||||
{
|
||||
detail::write_default_value_key(path, spec.clsid);
|
||||
}
|
||||
if (!spec.systemFileAssocExtensions.empty() && !spec.systemFileAssocHandlerName.empty())
|
||||
{
|
||||
for (const auto& ext : spec.systemFileAssocExtensions)
|
||||
{
|
||||
std::wstring path = L"Software\\Classes\\SystemFileAssociations\\"s + ext + L"\\ShellEx\\ContextMenuHandlers\\" + spec.systemFileAssocHandlerName;
|
||||
detail::write_default_value_key(path, spec.clsid);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!exists)
|
||||
{
|
||||
detail::write_sentinel(spec);
|
||||
Logger::info(L"Runtime registration completed for CLSID {}", spec.clsid);
|
||||
}
|
||||
else if (repaired && spec.logRepairs)
|
||||
{
|
||||
Logger::info(L"Runtime registration repaired for CLSID {}", spec.clsid);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool Unregister(const Spec& spec)
|
||||
{
|
||||
using namespace std::string_literals;
|
||||
// Remove handler key paths
|
||||
for (const auto& path : spec.contextMenuHandlerKeyPaths)
|
||||
{
|
||||
RegDeleteTreeW(HKEY_CURRENT_USER, path.c_str());
|
||||
}
|
||||
// Remove extra association paths (e.g., drag & drop handlers)
|
||||
for (const auto& path : spec.extraAssociationPaths)
|
||||
{
|
||||
RegDeleteTreeW(HKEY_CURRENT_USER, path.c_str());
|
||||
}
|
||||
// Remove per-extension system file association handler keys
|
||||
if (!spec.systemFileAssocExtensions.empty() && !spec.systemFileAssocHandlerName.empty())
|
||||
{
|
||||
for (const auto& ext : spec.systemFileAssocExtensions)
|
||||
{
|
||||
std::wstring keyPath = L"Software\\Classes\\SystemFileAssociations\\"s + ext + L"\\ShellEx\\ContextMenuHandlers\\" + spec.systemFileAssocHandlerName;
|
||||
RegDeleteTreeW(HKEY_CURRENT_USER, keyPath.c_str());
|
||||
}
|
||||
}
|
||||
// Remove CLSID branch
|
||||
if (!spec.clsid.empty())
|
||||
{
|
||||
std::wstring clsidRoot = L"Software\\Classes\\CLSID\\"s + spec.clsid;
|
||||
RegDeleteTreeW(HKEY_CURRENT_USER, clsidRoot.c_str());
|
||||
}
|
||||
// Remove sentinel value (not deleting entire key to avoid disturbing other values)
|
||||
if (!spec.sentinelKey.empty() && !spec.sentinelValue.empty())
|
||||
{
|
||||
HKEY hKey{};
|
||||
if (RegOpenKeyExW(HKEY_CURRENT_USER, spec.sentinelKey.c_str(), 0, KEY_SET_VALUE, &hKey) == ERROR_SUCCESS)
|
||||
{
|
||||
RegDeleteValueW(hKey, spec.sentinelValue.c_str());
|
||||
RegCloseKey(hKey);
|
||||
}
|
||||
}
|
||||
Logger::info(L"Successfully unregistered CLSID {}", spec.clsid);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -73,6 +73,7 @@
|
||||
<ClInclude Include="ClassFactory.h" />
|
||||
<ClInclude Include="dllmain.h" />
|
||||
<ClInclude Include="ExplorerCommand.h" />
|
||||
<ClInclude Include="RuntimeRegistration.h" />
|
||||
<ClInclude Include="pch.h" />
|
||||
<None Include="packages.config" />
|
||||
<None Include="resource.base.h" />
|
||||
|
||||
@@ -27,6 +27,9 @@
|
||||
<ClInclude Include="dllmain.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="RuntimeRegistration.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="dllmain.cpp">
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "FileLocksmithLib/Constants.h"
|
||||
#include "FileLocksmithLib/Settings.h"
|
||||
#include "FileLocksmithLib/Trace.h"
|
||||
#include "RuntimeRegistration.h"
|
||||
|
||||
#include "dllmain.h"
|
||||
#include "Generated Files/resource.h"
|
||||
@@ -82,12 +83,17 @@ public:
|
||||
{
|
||||
std::wstring path = get_module_folderpath(globals::instance);
|
||||
std::wstring packageUri = path + L"\\FileLocksmithContextMenuPackage.msix";
|
||||
|
||||
if (!package::IsPackageRegisteredWithPowerToysVersion(constants::nonlocalizable::ContextMenuPackageName))
|
||||
{
|
||||
package::RegisterSparsePackage(path, packageUri);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
|
||||
FileLocksmithRuntimeRegistration::EnsureRegistered();
|
||||
#endif
|
||||
}
|
||||
|
||||
m_enabled = true;
|
||||
}
|
||||
@@ -95,6 +101,13 @@ public:
|
||||
virtual void disable() override
|
||||
{
|
||||
Logger::info(L"File Locksmith disabled");
|
||||
if (!package::IsWin11OrGreater())
|
||||
{
|
||||
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
|
||||
FileLocksmithRuntimeRegistration::Unregister();
|
||||
Logger::info(L"File Locksmith context menu unregistered (Win10)");
|
||||
#endif
|
||||
}
|
||||
m_enabled = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
// Header-only runtime registration for FileLocksmith context menu extension.
|
||||
#pragma once
|
||||
|
||||
#include <common/utils/shell_ext_registration.h>
|
||||
|
||||
namespace globals { extern HMODULE instance; }
|
||||
|
||||
namespace FileLocksmithRuntimeRegistration
|
||||
{
|
||||
namespace
|
||||
{
|
||||
inline runtime_shell_ext::Spec BuildSpec()
|
||||
{
|
||||
runtime_shell_ext::Spec spec;
|
||||
spec.clsid = L"{84D68575-E186-46AD-B0CB-BAEB45EE29C0}";
|
||||
spec.sentinelKey = L"Software\\Microsoft\\PowerToys\\FileLocksmith";
|
||||
spec.sentinelValue = L"ContextMenuRegistered";
|
||||
spec.dllFileCandidates = { L"PowerToys.FileLocksmithExt.dll" };
|
||||
spec.contextMenuHandlerKeyPaths = {
|
||||
L"Software\\Classes\\AllFileSystemObjects\\ShellEx\\ContextMenuHandlers\\FileLocksmithExt",
|
||||
L"Software\\Classes\\Drive\\ShellEx\\ContextMenuHandlers\\FileLocksmithExt" };
|
||||
spec.friendlyName = L"File Locksmith Shell Extension";
|
||||
return spec;
|
||||
}
|
||||
}
|
||||
|
||||
inline bool EnsureRegistered()
|
||||
{
|
||||
return runtime_shell_ext::EnsureRegistered(BuildSpec(), globals::instance);
|
||||
}
|
||||
|
||||
inline void Unregister()
|
||||
{
|
||||
runtime_shell_ext::Unregister(BuildSpec());
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "MouseHighlighter.h"
|
||||
#include "trace.h"
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
|
||||
#ifdef COMPOSITION
|
||||
namespace winrt
|
||||
@@ -49,6 +50,9 @@ private:
|
||||
void BringToFront();
|
||||
HHOOK m_mouseHook = NULL;
|
||||
static LRESULT CALLBACK MouseHookProc(int nCode, WPARAM wParam, LPARAM lParam) noexcept;
|
||||
// Helpers for spotlight overlay
|
||||
float GetDpiScale() const;
|
||||
void UpdateSpotlightMask(float cx, float cy, float radius, bool show);
|
||||
|
||||
static constexpr auto m_className = L"MouseHighlighter";
|
||||
static constexpr auto m_windowTitle = L"PowerToys Mouse Highlighter";
|
||||
@@ -67,7 +71,14 @@ private:
|
||||
winrt::CompositionSpriteShape m_leftPointer{ nullptr };
|
||||
winrt::CompositionSpriteShape m_rightPointer{ nullptr };
|
||||
winrt::CompositionSpriteShape m_alwaysPointer{ nullptr };
|
||||
winrt::CompositionSpriteShape m_spotlightPointer{ nullptr };
|
||||
// Spotlight overlay (mask with soft feathered edge)
|
||||
winrt::SpriteVisual m_overlay{ nullptr };
|
||||
winrt::CompositionMaskBrush m_spotlightMask{ nullptr };
|
||||
winrt::CompositionRadialGradientBrush m_spotlightMaskGradient{ nullptr };
|
||||
winrt::CompositionColorBrush m_spotlightSource{ nullptr };
|
||||
winrt::CompositionColorGradientStop m_maskStopCenter{ nullptr };
|
||||
winrt::CompositionColorGradientStop m_maskStopInner{ nullptr };
|
||||
winrt::CompositionColorGradientStop m_maskStopOuter{ nullptr };
|
||||
|
||||
bool m_leftPointerEnabled = true;
|
||||
bool m_rightPointerEnabled = true;
|
||||
@@ -123,6 +134,35 @@ bool Highlighter::CreateHighlighter()
|
||||
m_shape.RelativeSizeAdjustment({ 1.0f, 1.0f });
|
||||
m_root.Children().InsertAtTop(m_shape);
|
||||
|
||||
// Create spotlight overlay (soft feather, DPI-aware)
|
||||
m_overlay = m_compositor.CreateSpriteVisual();
|
||||
m_overlay.RelativeSizeAdjustment({ 1.0f, 1.0f });
|
||||
m_spotlightSource = m_compositor.CreateColorBrush(m_alwaysColor);
|
||||
m_spotlightMaskGradient = m_compositor.CreateRadialGradientBrush();
|
||||
m_spotlightMaskGradient.MappingMode(winrt::CompositionMappingMode::Absolute);
|
||||
// Center region fully transparent
|
||||
m_maskStopCenter = m_compositor.CreateColorGradientStop();
|
||||
m_maskStopCenter.Offset(0.0f);
|
||||
m_maskStopCenter.Color(winrt::Windows::UI::ColorHelper::FromArgb(0, 0, 0, 0));
|
||||
// Inner edge of feather (still transparent)
|
||||
m_maskStopInner = m_compositor.CreateColorGradientStop();
|
||||
m_maskStopInner.Offset(0.995f); // will be updated per-radius
|
||||
m_maskStopInner.Color(winrt::Windows::UI::ColorHelper::FromArgb(0, 0, 0, 0));
|
||||
// Outer edge (opaque mask -> overlay visible)
|
||||
m_maskStopOuter = m_compositor.CreateColorGradientStop();
|
||||
m_maskStopOuter.Offset(1.0f);
|
||||
m_maskStopOuter.Color(winrt::Windows::UI::ColorHelper::FromArgb(255, 255, 255, 255));
|
||||
m_spotlightMaskGradient.ColorStops().Append(m_maskStopCenter);
|
||||
m_spotlightMaskGradient.ColorStops().Append(m_maskStopInner);
|
||||
m_spotlightMaskGradient.ColorStops().Append(m_maskStopOuter);
|
||||
|
||||
m_spotlightMask = m_compositor.CreateMaskBrush();
|
||||
m_spotlightMask.Source(m_spotlightSource);
|
||||
m_spotlightMask.Mask(m_spotlightMaskGradient);
|
||||
m_overlay.Brush(m_spotlightMask);
|
||||
m_overlay.IsVisible(false);
|
||||
m_root.Children().InsertAtTop(m_overlay);
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (...)
|
||||
@@ -165,12 +205,8 @@ void Highlighter::AddDrawingPoint(MouseButton button)
|
||||
// always
|
||||
if (m_spotlightMode)
|
||||
{
|
||||
float borderThickness = static_cast<float>(std::hypot(GetSystemMetrics(SM_CXVIRTUALSCREEN), GetSystemMetrics(SM_CYVIRTUALSCREEN)));
|
||||
circleGeometry.Radius({ static_cast<float>(borderThickness / 2.0 + m_radius), static_cast<float>(borderThickness / 2.0 + m_radius) });
|
||||
circleShape.FillBrush(nullptr);
|
||||
circleShape.StrokeBrush(m_compositor.CreateColorBrush(m_alwaysColor));
|
||||
circleShape.StrokeThickness(borderThickness);
|
||||
m_spotlightPointer = circleShape;
|
||||
UpdateSpotlightMask(static_cast<float>(pt.x), static_cast<float>(pt.y), m_radius, true);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -209,20 +245,14 @@ void Highlighter::UpdateDrawingPointPosition(MouseButton button)
|
||||
}
|
||||
else
|
||||
{
|
||||
// always
|
||||
// always / spotlight idle
|
||||
if (m_spotlightMode)
|
||||
{
|
||||
if (m_spotlightPointer)
|
||||
{
|
||||
m_spotlightPointer.Offset({ static_cast<float>(pt.x), static_cast<float>(pt.y) });
|
||||
}
|
||||
UpdateSpotlightMask(static_cast<float>(pt.x), static_cast<float>(pt.y), m_radius, true);
|
||||
}
|
||||
else
|
||||
else if (m_alwaysPointer)
|
||||
{
|
||||
if (m_alwaysPointer)
|
||||
{
|
||||
m_alwaysPointer.Offset({ static_cast<float>(pt.x), static_cast<float>(pt.y) });
|
||||
}
|
||||
m_alwaysPointer.Offset({ static_cast<float>(pt.x), static_cast<float>(pt.y) });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -266,9 +296,9 @@ void Highlighter::ClearDrawingPoint()
|
||||
{
|
||||
if (m_spotlightMode)
|
||||
{
|
||||
if (m_spotlightPointer)
|
||||
if (m_overlay)
|
||||
{
|
||||
m_spotlightPointer.StrokeBrush().as<winrt::Windows::UI::Composition::CompositionColorBrush>().Color(winrt::Windows::UI::ColorHelper::FromArgb(0, 0, 0, 0));
|
||||
m_overlay.IsVisible(false);
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -421,7 +451,10 @@ void Highlighter::StopDrawing()
|
||||
m_leftPointer = nullptr;
|
||||
m_rightPointer = nullptr;
|
||||
m_alwaysPointer = nullptr;
|
||||
m_spotlightPointer = nullptr;
|
||||
if (m_overlay)
|
||||
{
|
||||
m_overlay.IsVisible(false);
|
||||
}
|
||||
ShowWindow(m_hwnd, SW_HIDE);
|
||||
UnhookWindowsHookEx(m_mouseHook);
|
||||
ClearDrawing();
|
||||
@@ -452,6 +485,16 @@ void Highlighter::ApplySettings(MouseHighlighterSettings settings)
|
||||
m_rightPointerEnabled = false;
|
||||
}
|
||||
|
||||
// Keep spotlight overlay color updated
|
||||
if (m_spotlightSource)
|
||||
{
|
||||
m_spotlightSource.Color(m_alwaysColor);
|
||||
}
|
||||
if (!m_spotlightMode && m_overlay)
|
||||
{
|
||||
m_overlay.IsVisible(false);
|
||||
}
|
||||
|
||||
if (instance->m_visible)
|
||||
{
|
||||
instance->StopDrawing();
|
||||
@@ -563,6 +606,43 @@ void Highlighter::Terminate()
|
||||
}
|
||||
}
|
||||
|
||||
float Highlighter::GetDpiScale() const
|
||||
{
|
||||
return static_cast<float>(GetDpiForWindow(m_hwnd)) / 96.0f;
|
||||
}
|
||||
|
||||
// Update spotlight radial mask center/radius with DPI-aware feather
|
||||
void Highlighter::UpdateSpotlightMask(float cx, float cy, float radius, bool show)
|
||||
{
|
||||
if (!m_spotlightMaskGradient)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
m_spotlightMaskGradient.EllipseCenter({ cx, cy });
|
||||
m_spotlightMaskGradient.EllipseRadius({ radius, radius });
|
||||
|
||||
const float dpiScale = GetDpiScale();
|
||||
// Target a very fine edge: ~1 physical pixel, convert to DIPs: 1 / dpiScale
|
||||
const float featherDip = 1.0f / (dpiScale > 0.0f ? dpiScale : 1.0f);
|
||||
const float safeRadius = (std::max)(radius, 1.0f);
|
||||
const float featherRel = (std::min)(0.25f, featherDip / safeRadius);
|
||||
|
||||
if (m_maskStopInner)
|
||||
{
|
||||
m_maskStopInner.Offset((std::max)(0.0f, 1.0f - featherRel));
|
||||
}
|
||||
|
||||
if (m_spotlightSource)
|
||||
{
|
||||
m_spotlightSource.Color(m_alwaysColor);
|
||||
}
|
||||
if (m_overlay)
|
||||
{
|
||||
m_overlay.IsVisible(show);
|
||||
}
|
||||
}
|
||||
|
||||
#pragma region MouseHighlighter_API
|
||||
|
||||
void MouseHighlighterApplySettings(MouseHighlighterSettings settings)
|
||||
|
||||
@@ -123,6 +123,7 @@ MakeAppx.exe pack /d . /p $(OutDir)NewPlusPackage.msix /nv</Command>
|
||||
<ClInclude Include="settings.h" />
|
||||
<ClInclude Include="trace.h" />
|
||||
<ClInclude Include="new_utilities.h" />
|
||||
<ClInclude Include="RuntimeRegistration.h" />
|
||||
<ClInclude Include="resource.base.h" />
|
||||
<ClInclude Include="template_folder.h" />
|
||||
<ClInclude Include="pch.h" />
|
||||
|
||||
@@ -84,6 +84,9 @@
|
||||
<ClInclude Include="helpers_variables.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="RuntimeRegistration.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
// Header-only runtime registration for New+ Win10 context menu.
|
||||
#pragma once
|
||||
|
||||
#include <windows.h>
|
||||
#include <string>
|
||||
#include <common/utils/shell_ext_registration.h>
|
||||
|
||||
// Provided by dll_main.cpp
|
||||
extern HMODULE module_instance_handle;
|
||||
|
||||
namespace NewPlusRuntimeRegistration
|
||||
{
|
||||
namespace {
|
||||
inline runtime_shell_ext::Spec BuildSpec()
|
||||
{
|
||||
runtime_shell_ext::Spec spec;
|
||||
spec.clsid = L"{FF90D477-E32A-4BE8-8CC5-A502A97F5401}";
|
||||
spec.sentinelKey = L"Software\\Microsoft\\PowerToys\\NewPlus";
|
||||
spec.sentinelValue = L"ContextMenuRegisteredWin10";
|
||||
spec.dllFileCandidates = { L"PowerToys.NewPlus.ShellExtension.win10.dll" };
|
||||
spec.contextMenuHandlerKeyPaths = { L"Software\\Classes\\Directory\\background\\ShellEx\\ContextMenuHandlers\\NewPlusShellExtensionWin10" };
|
||||
spec.friendlyName = L"NewPlus Shell Extension Win10";
|
||||
return spec;
|
||||
}
|
||||
}
|
||||
|
||||
inline bool EnsureRegisteredWin10()
|
||||
{
|
||||
return runtime_shell_ext::EnsureRegistered(BuildSpec(), module_instance_handle);
|
||||
}
|
||||
|
||||
inline void Unregister()
|
||||
{
|
||||
runtime_shell_ext::Unregister(BuildSpec());
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@
|
||||
#include "trace.h"
|
||||
#include "new_utilities.h"
|
||||
#include "Generated Files/resource.h"
|
||||
#include "RuntimeRegistration.h"
|
||||
|
||||
// Note: Settings are managed via Settings and UI Settings
|
||||
class NewModule : public PowertoyModuleIface
|
||||
@@ -93,8 +94,16 @@ public:
|
||||
|
||||
// Log telemetry
|
||||
Trace::EventToggleOnOff(true);
|
||||
|
||||
newplus::utilities::register_msix_package();
|
||||
if (package::IsWin11OrGreater())
|
||||
{
|
||||
newplus::utilities::register_msix_package();
|
||||
}
|
||||
else
|
||||
{
|
||||
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
|
||||
NewPlusRuntimeRegistration::EnsureRegisteredWin10();
|
||||
#endif
|
||||
}
|
||||
|
||||
powertoy_new_enabled = true;
|
||||
}
|
||||
@@ -141,6 +150,13 @@ private:
|
||||
{
|
||||
Trace::EventToggleOnOff(false);
|
||||
}
|
||||
if (!package::IsWin11OrGreater())
|
||||
{
|
||||
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
|
||||
NewPlusRuntimeRegistration::Unregister();
|
||||
Logger::info(L"New+ context menu unregistered (Win10)");
|
||||
#endif
|
||||
}
|
||||
powertoy_new_enabled = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,6 @@
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.7.250513003" />
|
||||
<PackageVersion Include="Shmuelie.WinRTServer" Version="2.1.1" />
|
||||
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
|
||||
<PackageVersion Include="System.Text.Json" Version="9.0.7" />
|
||||
<PackageVersion Include="System.Text.Json" Version="9.0.8" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -89,7 +89,7 @@ public class SupersedingAsyncGate : IDisposable
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
CompleteIfCurrent(currentTcs, currentCallId, tcs => tcs.SetCanceled(currentCts.Token));
|
||||
CompleteIfCurrent(currentTcs, currentCallId, tcs => tcs.TrySetCanceled(currentCts.Token));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -2,11 +2,14 @@
|
||||
// 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.CodeAnalysis;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Models;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.ViewModels;
|
||||
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
|
||||
public partial class CommandContextItemViewModel(ICommandContextItem contextItem, WeakReference<IPageContext> context) : CommandItemViewModel(new(contextItem), context), IContextItemViewModel
|
||||
{
|
||||
private readonly KeyChord nullKeyChord = new(0, 0, 0);
|
||||
|
||||
@@ -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.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Models;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
@@ -9,6 +10,7 @@ using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.ViewModels;
|
||||
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
|
||||
public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBarContext
|
||||
{
|
||||
public ExtensionObject<ICommandItem> Model => _commandItemModel;
|
||||
|
||||
@@ -4,12 +4,14 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.ViewModels;
|
||||
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
|
||||
public interface IContextItemViewModel
|
||||
{
|
||||
}
|
||||
|
||||
@@ -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.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.ViewModels;
|
||||
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
|
||||
public partial class SeparatorContextItemViewModel() : IContextItemViewModel, ISeparatorContextItem
|
||||
{
|
||||
}
|
||||
|
||||
@@ -6,8 +6,10 @@ using System.Collections.Immutable;
|
||||
using System.Collections.Specialized;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.Common.Helpers;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.Ext.Apps;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -29,9 +31,13 @@ public partial class MainListPage : DynamicListPage,
|
||||
private bool _includeApps;
|
||||
private bool _filteredItemsIncludesApps;
|
||||
|
||||
private InterlockedBoolean _refreshRunning;
|
||||
private InterlockedBoolean _refreshRequested;
|
||||
|
||||
public MainListPage(IServiceProvider serviceProvider)
|
||||
{
|
||||
Icon = IconHelpers.FromRelativePath("Assets\\StoreLogo.scale-200.png");
|
||||
PlaceholderText = Properties.Resources.builtin_main_list_page_searchbar_placeholder;
|
||||
_serviceProvider = serviceProvider;
|
||||
|
||||
_tlcManager = _serviceProvider.GetService<TopLevelCommandManager>()!;
|
||||
@@ -55,6 +61,7 @@ public partial class MainListPage : DynamicListPage,
|
||||
var settings = _serviceProvider.GetService<SettingsModel>()!;
|
||||
settings.SettingsChanged += SettingsChangedHandler;
|
||||
HotReloadSettings(settings);
|
||||
_includeApps = _tlcManager.IsProviderActive(AllAppsCommandProvider.WellKnownId);
|
||||
|
||||
IsLoading = true;
|
||||
}
|
||||
@@ -82,18 +89,47 @@ public partial class MainListPage : DynamicListPage,
|
||||
|
||||
private void ReapplySearchInBackground()
|
||||
{
|
||||
_ = Task.Run(() =>
|
||||
_refreshRequested.Set();
|
||||
if (!_refreshRunning.Set())
|
||||
{
|
||||
try
|
||||
return;
|
||||
}
|
||||
|
||||
_ = Task.Run(RunRefreshLoop);
|
||||
}
|
||||
|
||||
private void RunRefreshLoop()
|
||||
{
|
||||
try
|
||||
{
|
||||
do
|
||||
{
|
||||
_refreshRequested.Clear();
|
||||
lock (_tlcManager.TopLevelCommands)
|
||||
{
|
||||
if (_filteredItemsIncludesApps == _includeApps)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var currentSearchText = SearchText;
|
||||
UpdateSearchText(currentSearchText, currentSearchText);
|
||||
}
|
||||
catch (Exception e)
|
||||
while (_refreshRequested.Value);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogError("Failed to reload search", e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_refreshRunning.Clear();
|
||||
if (_refreshRequested.Value && _refreshRunning.Set())
|
||||
{
|
||||
Logger.LogError("Failed to reload search", e);
|
||||
_ = Task.Run(RunRefreshLoop);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public override IListItem[] GetItems()
|
||||
@@ -125,6 +161,15 @@ public partial class MainListPage : DynamicListPage,
|
||||
var aliases = _serviceProvider.GetService<AliasManager>()!;
|
||||
if (aliases.CheckAlias(newSearch))
|
||||
{
|
||||
if (_filteredItemsIncludesApps != _includeApps)
|
||||
{
|
||||
lock (_tlcManager.TopLevelCommands)
|
||||
{
|
||||
_filteredItemsIncludesApps = _includeApps;
|
||||
_filteredItems = null;
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -137,6 +182,7 @@ public partial class MainListPage : DynamicListPage,
|
||||
// Cleared out the filter text? easy. Reset _filteredItems, and bail out.
|
||||
if (string.IsNullOrEmpty(newSearch))
|
||||
{
|
||||
_filteredItemsIncludesApps = _includeApps;
|
||||
_filteredItems = null;
|
||||
RaiseItemsChanged(commands.Count);
|
||||
return;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.Messages;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
|
||||
@@ -98,35 +98,107 @@ public partial class ContentFormViewModel(IFormContent _form, WeakReference<IPag
|
||||
|
||||
public void HandleSubmit(IAdaptiveActionElement action, JsonObject inputs)
|
||||
{
|
||||
if (action is AdaptiveOpenUrlAction openUrlAction)
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<LaunchUriMessage>(new(openUrlAction.Url));
|
||||
return;
|
||||
}
|
||||
// BODGY circa GH #40979
|
||||
// Usually, you're supposed to try to cast the action to a specific
|
||||
// type, and use those objects to get the data you need.
|
||||
// However, there's something weird with AdaptiveCards and the way it
|
||||
// works when we consume it when built in Release, with AOT (and
|
||||
// trimming) enabled. Any sort of `action.As<IAdaptiveSubmitAction>()`
|
||||
// or similar will throw a System.InvalidCastException.
|
||||
//
|
||||
// Instead we have this horror show.
|
||||
//
|
||||
// The `action.ToJson()` blob ACTUALLY CONTAINS THE `type` field, which
|
||||
// we can use to determine what kind of action it is. Then we can parse
|
||||
// the JSON manually based on the type.
|
||||
var actionJson = action.ToJson();
|
||||
|
||||
if (action is AdaptiveSubmitAction or AdaptiveExecuteAction)
|
||||
if (actionJson.TryGetValue("type", out var actionTypeValue))
|
||||
{
|
||||
// Get the data and inputs
|
||||
var dataString = (action as AdaptiveSubmitAction)?.DataJson.Stringify() ?? string.Empty;
|
||||
var inputString = inputs.Stringify();
|
||||
var actionTypeString = actionTypeValue.GetString();
|
||||
Logger.LogTrace($"atString={actionTypeString}");
|
||||
|
||||
_ = Task.Run(() =>
|
||||
var actionType = actionTypeString switch
|
||||
{
|
||||
try
|
||||
{
|
||||
var model = _formModel.Unsafe!;
|
||||
if (model != null)
|
||||
"Action.Submit" => ActionType.Submit,
|
||||
"Action.Execute" => ActionType.Execute,
|
||||
"Action.OpenUrl" => ActionType.OpenUrl,
|
||||
_ => ActionType.Unsupported,
|
||||
};
|
||||
|
||||
Logger.LogDebug($"{actionTypeString}->{actionType}");
|
||||
|
||||
switch (actionType)
|
||||
{
|
||||
case ActionType.OpenUrl:
|
||||
{
|
||||
var result = model.SubmitForm(inputString, dataString);
|
||||
WeakReferenceMessenger.Default.Send<HandleCommandResultMessage>(new(new(result)));
|
||||
HandleOpenUrlAction(action, actionJson);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ShowException(ex);
|
||||
}
|
||||
});
|
||||
|
||||
break;
|
||||
case ActionType.Submit:
|
||||
case ActionType.Execute:
|
||||
{
|
||||
HandleSubmitAction(action, actionJson, inputs);
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
Logger.LogError($"{actionType} was an unexpected action `type`");
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogError($"actionJson.TryGetValue(type) failed");
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleOpenUrlAction(IAdaptiveActionElement action, JsonObject actionJson)
|
||||
{
|
||||
if (actionJson.TryGetValue("url", out var actionUrlValue))
|
||||
{
|
||||
var actionUrl = actionUrlValue.GetString() ?? string.Empty;
|
||||
if (Uri.TryCreate(actionUrl, default(UriCreationOptions), out var uri))
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<LaunchUriMessage>(new(uri));
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogError($"Failed to produce URI for {actionUrlValue}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleSubmitAction(
|
||||
IAdaptiveActionElement action,
|
||||
JsonObject actionJson,
|
||||
JsonObject inputs)
|
||||
{
|
||||
var dataString = string.Empty;
|
||||
if (actionJson.TryGetValue("data", out var actionDataValue))
|
||||
{
|
||||
dataString = actionDataValue.Stringify() ?? string.Empty;
|
||||
}
|
||||
|
||||
var inputString = inputs.Stringify();
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var model = _formModel.Unsafe!;
|
||||
if (model != null)
|
||||
{
|
||||
var result = model.SubmitForm(inputString, dataString);
|
||||
Logger.LogDebug($"SubmitForm() returned {result}");
|
||||
WeakReferenceMessenger.Default.Send<HandleCommandResultMessage>(new(new(result)));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ShowException(ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static readonly string ErrorCardJson = """
|
||||
|
||||
@@ -2,7 +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.
|
||||
|
||||
namespace Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
namespace Microsoft.CmdPal.UI.Messages;
|
||||
|
||||
public record OpenSettingsMessage()
|
||||
{
|
||||
@@ -2,7 +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.
|
||||
|
||||
namespace Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
|
||||
/// <summary>
|
||||
/// Message which closes the application. Used by <see cref="QuitCommand"/> via <see cref="BuiltInsCommandProvider"/>.
|
||||
@@ -2,7 +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.
|
||||
|
||||
namespace Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
|
||||
public record ReloadCommandsMessage()
|
||||
{
|
||||
@@ -2,7 +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.
|
||||
|
||||
namespace Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
|
||||
public record UpdateFallbackItemsMessage()
|
||||
{
|
||||
@@ -321,6 +321,15 @@ namespace Microsoft.CmdPal.UI.ViewModels.Properties {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Search for apps, files and commands....
|
||||
/// </summary>
|
||||
public static string builtin_main_list_page_searchbar_placeholder {
|
||||
get {
|
||||
return ResourceManager.GetString("builtin_main_list_page_searchbar_placeholder", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Creates a project for a new Command Palette extension.
|
||||
/// </summary>
|
||||
|
||||
@@ -227,4 +227,7 @@
|
||||
<data name="builtin_disabled_extension" xml:space="preserve">
|
||||
<value>Disabled</value>
|
||||
</data>
|
||||
<data name="builtin_main_list_page_searchbar_placeholder" xml:space="preserve">
|
||||
<value>Search for apps, files and commands...</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -7,7 +7,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.CmdPal.Common.Services;
|
||||
using Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Properties;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ using ManagedCommon;
|
||||
using Microsoft.CmdPal.Common.Helpers;
|
||||
using Microsoft.CmdPal.Common.Services;
|
||||
using Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -151,7 +151,7 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
WeakReference<IPageContext> weakSelf = new(this);
|
||||
await sender.LoadTopLevelCommands(_serviceProvider, weakSelf);
|
||||
|
||||
List<TopLevelViewModel> newItems = [..sender.TopLevelItems];
|
||||
List<TopLevelViewModel> newItems = [.. sender.TopLevelItems];
|
||||
foreach (var i in sender.FallbackItems)
|
||||
{
|
||||
if (i.IsEnabled)
|
||||
|
||||
@@ -8,6 +8,7 @@ using CommunityToolkit.Mvvm.Messaging;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
@@ -64,6 +64,9 @@ public partial class App : Application
|
||||
|
||||
this.InitializeComponent();
|
||||
|
||||
// Ensure types used in XAML are preserved for AOT compilation
|
||||
TypePreservation.PreserveTypes();
|
||||
|
||||
NativeEventWaiter.WaitForEventLoop(
|
||||
"Local\\PowerToysCmdPal-ExitEvent-eb73f6be-3f22-4b36-aee3-62924ba40bfd", () =>
|
||||
{
|
||||
|
||||
@@ -59,8 +59,24 @@
|
||||
<Setter Property="BorderBrush" Value="{ThemeResource DividerStrokeColorDefaultBrush}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="CornerRadius" Value="6" />
|
||||
<Setter Property="MinWidth" Value="20" />
|
||||
</Style.Setters>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="HotkeyTextBlockStyle" TargetType="TextBlock">
|
||||
<Setter Property="FontSize" Value="10" />
|
||||
<Setter Property="CharacterSpacing" Value="4" />
|
||||
<Setter Property="Foreground" Value="{ThemeResource TextFillColorPrimaryBrush}" />
|
||||
<Setter Property="HorizontalAlignment" Value="Center" />
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
</Style>
|
||||
|
||||
<Style x:Key="HotkeyFontIconStyle" TargetType="FontIcon">
|
||||
<Setter Property="FontSize" Value="10" />
|
||||
<Setter Property="Foreground" Value="{ThemeResource TextFillColorPrimaryBrush}" />
|
||||
<Setter Property="HorizontalAlignment" Value="Center" />
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
</UserControl.Resources>
|
||||
|
||||
@@ -76,44 +92,44 @@
|
||||
|
||||
<Grid
|
||||
x:Name="IconRoot"
|
||||
Margin="8,0,0,0"
|
||||
Tapped="PageIcon_Tapped"
|
||||
Margin="3,0,-5,0"
|
||||
Visibility="{x:Bind CurrentPageViewModel.IsNested, Mode=OneWay}">
|
||||
<InfoBadge Visibility="{x:Bind CurrentPageViewModel.HasStatusMessage, Mode=OneWay}" Value="{x:Bind CurrentPageViewModel.StatusMessages.Count, Mode=OneWay}" />
|
||||
<Grid.ContextFlyout>
|
||||
<Flyout x:Name="StatusMessagesFlyout" Placement="TopEdgeAlignedLeft">
|
||||
<ItemsRepeater
|
||||
x:Name="MessagesDropdown"
|
||||
Margin="-8"
|
||||
ItemsSource="{x:Bind CurrentPageViewModel.StatusMessages, Mode=OneWay}"
|
||||
Layout="{StaticResource VerticalStackLayout}">
|
||||
<ItemsRepeater.ItemTemplate>
|
||||
<DataTemplate x:DataType="coreViewModels:StatusMessageViewModel">
|
||||
<StackPanel
|
||||
Grid.Row="0"
|
||||
Margin="0"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Bottom"
|
||||
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
|
||||
CornerRadius="0">
|
||||
<InfoBar
|
||||
CornerRadius="{ThemeResource ControlCornerRadius}"
|
||||
IsClosable="False"
|
||||
IsOpen="True"
|
||||
Message="{x:Bind Message, Mode=OneWay}"
|
||||
Severity="{x:Bind State, Mode=OneWay, Converter={StaticResource MessageStateToSeverityConverter}}" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ItemsRepeater.ItemTemplate>
|
||||
</ItemsRepeater>
|
||||
</Flyout>
|
||||
</Grid.ContextFlyout>
|
||||
<Button
|
||||
x:Name="StatusMessagesButton"
|
||||
x:Uid="StatusMessagesButton"
|
||||
Padding="4"
|
||||
Style="{StaticResource SubtleButtonStyle}"
|
||||
Visibility="{x:Bind CurrentPageViewModel.HasStatusMessage, Mode=OneWay}">
|
||||
<InfoBadge Value="{x:Bind CurrentPageViewModel.StatusMessages.Count, Mode=OneWay}" />
|
||||
<Button.Flyout>
|
||||
<Flyout x:Name="StatusMessagesFlyout" Placement="TopEdgeAlignedLeft">
|
||||
<ItemsRepeater
|
||||
x:Name="MessagesDropdown"
|
||||
Margin="-8"
|
||||
ItemsSource="{x:Bind CurrentPageViewModel.StatusMessages, Mode=OneWay}"
|
||||
Layout="{StaticResource VerticalStackLayout}">
|
||||
<ItemsRepeater.ItemTemplate>
|
||||
<DataTemplate x:DataType="coreViewModels:StatusMessageViewModel">
|
||||
<StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Bottom">
|
||||
<InfoBar
|
||||
CornerRadius="{ThemeResource ControlCornerRadius}"
|
||||
IsClosable="False"
|
||||
IsOpen="True"
|
||||
Message="{x:Bind Message, Mode=OneWay}"
|
||||
Severity="{x:Bind State, Mode=OneWay, Converter={StaticResource MessageStateToSeverityConverter}}" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ItemsRepeater.ItemTemplate>
|
||||
</ItemsRepeater>
|
||||
</Flyout>
|
||||
</Button.Flyout>
|
||||
</Button>
|
||||
</Grid>
|
||||
<Button
|
||||
x:Name="SettingsIconButton"
|
||||
x:Uid="SettingsButton"
|
||||
Click="SettingsIcon_Clicked"
|
||||
Style="{StaticResource SubtleButtonStyle}"
|
||||
Tapped="SettingsIcon_Tapped"
|
||||
Visibility="{x:Bind CurrentPageViewModel.IsNested, Mode=OneWay, Converter={StaticResource BoolToInvertedVisibilityConverter}}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<FontIcon
|
||||
@@ -146,8 +162,8 @@
|
||||
x:Load="{x:Bind IsLoaded, Mode=OneWay}"
|
||||
AutomationProperties.Name="{x:Bind ViewModel.PrimaryCommand.Name, Mode=OneWay}"
|
||||
Background="Transparent"
|
||||
Click="PrimaryButton_Clicked"
|
||||
Style="{StaticResource SubtleButtonStyle}"
|
||||
Tapped="PrimaryButton_Tapped"
|
||||
Visibility="{x:Bind ViewModel.HasPrimaryCommand, Mode=OneWay}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<TextBlock
|
||||
@@ -155,12 +171,7 @@
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind ViewModel.PrimaryCommand.Name, Mode=OneWay}" />
|
||||
<Border Style="{StaticResource HotkeyStyle}">
|
||||
<FontIcon
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="10"
|
||||
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
|
||||
Glyph="" />
|
||||
<FontIcon Glyph="" Style="{StaticResource HotkeyFontIconStyle}" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
@@ -169,8 +180,8 @@
|
||||
Padding="6,4,4,4"
|
||||
x:Load="{x:Bind IsLoaded, Mode=OneWay}"
|
||||
AutomationProperties.Name="{x:Bind ViewModel.SecondaryCommand.Name, Mode=OneWay}"
|
||||
Click="SecondaryButton_Clicked"
|
||||
Style="{StaticResource SubtleButtonStyle}"
|
||||
Tapped="SecondaryButton_Tapped"
|
||||
Visibility="{x:Bind ViewModel.HasSecondaryCommand, Mode=OneWay}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<TextBlock
|
||||
@@ -179,19 +190,10 @@
|
||||
Text="{x:Bind ViewModel.SecondaryCommand.Name, Mode=OneWay}" />
|
||||
<StackPanel Orientation="Horizontal" Spacing="4">
|
||||
<Border Padding="4,2,4,2" Style="{StaticResource HotkeyStyle}">
|
||||
<TextBlock
|
||||
CharacterSpacing="4"
|
||||
FontSize="10"
|
||||
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
|
||||
Text="Ctrl" />
|
||||
<TextBlock Style="{StaticResource HotkeyTextBlockStyle}" Text="Ctrl" />
|
||||
</Border>
|
||||
<Border Style="{StaticResource HotkeyStyle}">
|
||||
<FontIcon
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="10"
|
||||
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
|
||||
Glyph="" />
|
||||
<FontIcon Glyph="" Style="{StaticResource HotkeyFontIconStyle}" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
@@ -199,9 +201,9 @@
|
||||
<Button
|
||||
x:Name="MoreCommandsButton"
|
||||
x:Uid="MoreCommandsButton"
|
||||
Padding="4"
|
||||
Padding="6,4,4,4"
|
||||
Click="MoreCommandsButton_Clicked"
|
||||
Style="{StaticResource SubtleButtonStyle}"
|
||||
Tapped="MoreCommandsButton_Tapped"
|
||||
ToolTipService.ToolTip="Ctrl+K"
|
||||
Visibility="{x:Bind ViewModel.ShouldShowContextMenu, Mode=OneWay}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
@@ -209,32 +211,12 @@
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="More" />
|
||||
<StackPanel Orientation="Horizontal" Spacing="2">
|
||||
<Border
|
||||
Padding="4,2,4,2"
|
||||
VerticalAlignment="Center"
|
||||
Background="{ThemeResource SubtleFillColorSecondaryBrush}"
|
||||
BorderBrush="{ThemeResource DividerStrokeColorDefaultBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="4">
|
||||
<TextBlock
|
||||
CharacterSpacing="4"
|
||||
FontSize="10"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Text="Ctrl" />
|
||||
<StackPanel Orientation="Horizontal" Spacing="4">
|
||||
<Border Padding="4,2,4,2" Style="{StaticResource HotkeyStyle}">
|
||||
<TextBlock Style="{StaticResource HotkeyTextBlockStyle}" Text="Ctrl" />
|
||||
</Border>
|
||||
<Border
|
||||
Padding="4,2,4,2"
|
||||
VerticalAlignment="Center"
|
||||
Background="{ThemeResource SubtleFillColorSecondaryBrush}"
|
||||
BorderBrush="{ThemeResource DividerStrokeColorDefaultBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="4">
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
FontSize="10"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Text="K" />
|
||||
<Border Padding="4,2,4,2" Style="{StaticResource HotkeyStyle}">
|
||||
<TextBlock Style="{StaticResource HotkeyTextBlockStyle}" Text="K" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
@@ -114,34 +114,23 @@ public sealed partial class CommandBar : UserControl,
|
||||
}
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "VS has a tendency to delete XAML bound methods over-aggressively")]
|
||||
private void PrimaryButton_Tapped(object sender, TappedRoutedEventArgs e)
|
||||
private void PrimaryButton_Clicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
ViewModel.InvokePrimaryCommand();
|
||||
}
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "VS has a tendency to delete XAML bound methods over-aggressively")]
|
||||
private void SecondaryButton_Tapped(object sender, TappedRoutedEventArgs e)
|
||||
private void SecondaryButton_Clicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
ViewModel.InvokeSecondaryCommand();
|
||||
}
|
||||
|
||||
private void PageIcon_Tapped(object sender, TappedRoutedEventArgs e)
|
||||
{
|
||||
if (CurrentPageViewModel?.StatusMessages.Count > 0)
|
||||
{
|
||||
StatusMessagesFlyout.ShowAt(
|
||||
placementTarget: IconRoot,
|
||||
showOptions: new FlyoutShowOptions() { ShowMode = FlyoutShowMode.Standard });
|
||||
}
|
||||
}
|
||||
|
||||
private void SettingsIcon_Tapped(object sender, TappedRoutedEventArgs e)
|
||||
private void SettingsIcon_Clicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<OpenSettingsMessage>();
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void MoreCommandsButton_Tapped(object sender, TappedRoutedEventArgs e)
|
||||
private void MoreCommandsButton_Clicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<OpenContextMenuMessage>(new OpenContextMenuMessage(null, null, null, ContextMenuFilterLocation.Bottom));
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock Grid.Row="0" />
|
||||
<TextBlock Grid.Row="0" IsTabStop="True" />
|
||||
|
||||
<ItemsControl
|
||||
x:Name="KeysControl"
|
||||
|
||||
@@ -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.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
@@ -10,6 +11,7 @@ using Microsoft.UI.Xaml.Data;
|
||||
|
||||
namespace Microsoft.CmdPal.UI;
|
||||
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
|
||||
internal sealed partial class ContextItemTemplateSelector : DataTemplateSelector
|
||||
{
|
||||
public DataTemplate? Default { get; set; }
|
||||
|
||||
@@ -113,13 +113,14 @@
|
||||
<ListView
|
||||
x:Name="ItemsList"
|
||||
Padding="0,2,0,0"
|
||||
ContextCanceled="ItemsList_OnContextCanceled"
|
||||
ContextRequested="ItemsList_OnContextRequested"
|
||||
DoubleTapped="ItemsList_DoubleTapped"
|
||||
IsDoubleTapEnabled="True"
|
||||
IsItemClickEnabled="True"
|
||||
ItemClick="ItemsList_ItemClick"
|
||||
ItemTemplate="{StaticResource ListItemViewModelTemplate}"
|
||||
ItemsSource="{x:Bind ViewModel.FilteredItems, Mode=OneWay}"
|
||||
RightTapped="ItemsList_RightTapped"
|
||||
SelectionChanged="ItemsList_SelectionChanged">
|
||||
<ListView.ItemContainerTransitions>
|
||||
<TransitionCollection />
|
||||
|
||||
@@ -316,30 +316,51 @@ public sealed partial class ListPage : Page,
|
||||
return null;
|
||||
}
|
||||
|
||||
private void ItemsList_RightTapped(object sender, RightTappedRoutedEventArgs e)
|
||||
private void ItemsList_OnContextRequested(UIElement sender, ContextRequestedEventArgs e)
|
||||
{
|
||||
if (e.OriginalSource is FrameworkElement element &&
|
||||
element.DataContext is ListItemViewModel item)
|
||||
var (item, element) = e.OriginalSource switch
|
||||
{
|
||||
if (ItemsList.SelectedItem != item)
|
||||
{
|
||||
ItemsList.SelectedItem = item;
|
||||
}
|
||||
// caused by keyboard shortcut (e.g. Context menu key or Shift+F10)
|
||||
ListViewItem listViewItem => (ItemsList.ItemFromContainer(listViewItem) as ListItemViewModel, listViewItem),
|
||||
|
||||
ViewModel?.UpdateSelectedItemCommand.Execute(item);
|
||||
// caused by right-click on the ListViewItem
|
||||
FrameworkElement { DataContext: ListItemViewModel itemViewModel } frameworkElement => (itemViewModel, frameworkElement),
|
||||
|
||||
var pos = e.GetPosition(element);
|
||||
_ => (null, null),
|
||||
};
|
||||
|
||||
_ = DispatcherQueue.TryEnqueue(
|
||||
() =>
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<OpenContextMenuMessage>(
|
||||
new OpenContextMenuMessage(
|
||||
element,
|
||||
Microsoft.UI.Xaml.Controls.Primitives.FlyoutPlacementMode.BottomEdgeAlignedLeft,
|
||||
pos,
|
||||
ContextMenuFilterLocation.Top));
|
||||
});
|
||||
if (item == null || element == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (ItemsList.SelectedItem != item)
|
||||
{
|
||||
ItemsList.SelectedItem = item;
|
||||
}
|
||||
|
||||
ViewModel?.UpdateSelectedItemCommand.Execute(item);
|
||||
|
||||
if (!e.TryGetPosition(element, out var pos))
|
||||
{
|
||||
pos = new(0, element.ActualHeight);
|
||||
}
|
||||
|
||||
_ = DispatcherQueue.TryEnqueue(
|
||||
() =>
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<OpenContextMenuMessage>(
|
||||
new OpenContextMenuMessage(
|
||||
element,
|
||||
Microsoft.UI.Xaml.Controls.Primitives.FlyoutPlacementMode.BottomEdgeAlignedLeft,
|
||||
pos,
|
||||
ContextMenuFilterLocation.Top));
|
||||
});
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void ItemsList_OnContextCanceled(UIElement sender, RoutedEventArgs e)
|
||||
{
|
||||
_ = DispatcherQueue.TryEnqueue(() => WeakReferenceMessenger.Default.Send<CloseContextMenuMessage>());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.InteropServices;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Windows.Win32;
|
||||
using Windows.Win32.Foundation;
|
||||
|
||||
@@ -29,7 +29,7 @@ public static class WindowExtensions
|
||||
}
|
||||
catch (NotImplementedException)
|
||||
{
|
||||
// SetShownInSwitchers failed. This can happen if the Explorer is not running.
|
||||
// Setting IsShownInSwitchers failed. This can happen if the Explorer is not running.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,11 +10,12 @@ using ManagedCommon;
|
||||
using Microsoft.CmdPal.Common.Helpers;
|
||||
using Microsoft.CmdPal.Common.Messages;
|
||||
using Microsoft.CmdPal.Common.Services;
|
||||
using Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.Events;
|
||||
using Microsoft.CmdPal.UI.Helpers;
|
||||
using Microsoft.CmdPal.UI.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.PowerToys.Telemetry;
|
||||
using Microsoft.UI.Composition;
|
||||
@@ -176,7 +177,11 @@ public sealed partial class MainWindow : WindowEx,
|
||||
|
||||
private void UpdateAcrylic()
|
||||
{
|
||||
_acrylicController?.RemoveAllSystemBackdropTargets();
|
||||
if (_acrylicController != null)
|
||||
{
|
||||
_acrylicController.RemoveAllSystemBackdropTargets();
|
||||
_acrylicController.Dispose();
|
||||
}
|
||||
|
||||
_acrylicController = GetAcrylicConfig(Content);
|
||||
|
||||
|
||||
@@ -2,7 +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.
|
||||
|
||||
namespace Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
namespace Microsoft.CmdPal.UI.Messages;
|
||||
|
||||
public record HotkeySummonMessage(string CommandId, IntPtr Hwnd)
|
||||
{
|
||||
@@ -2,7 +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.
|
||||
|
||||
namespace Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
namespace Microsoft.CmdPal.UI.Messages;
|
||||
|
||||
public record SettingsWindowClosedMessage
|
||||
{
|
||||
@@ -109,10 +109,6 @@
|
||||
<ProjectCapability Include="Msix" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<RdXmlFile Include="rd.xml" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\common\ManagedTelemetry\Telemetry\ManagedTelemetry.csproj" />
|
||||
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.ClipboardHistory\Microsoft.CmdPal.Ext.ClipboardHistory.csproj" />
|
||||
|
||||
@@ -225,11 +225,11 @@
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
ui:VisualExtensions.NormalizedCenterPoint="0.5,0.5"
|
||||
Click="BackButton_Clicked"
|
||||
Content="{ui:FontIcon Glyph=,
|
||||
FontSize=14}"
|
||||
FontSize="16"
|
||||
Style="{StaticResource SubtleButtonStyle}"
|
||||
Tapped="BackButton_Tapped"
|
||||
Visibility="{x:Bind ViewModel.CurrentPage.IsNested, Mode=OneWay}">
|
||||
<animations:Implicit.ShowAnimations>
|
||||
<animations:OpacityAnimation
|
||||
|
||||
@@ -9,6 +9,7 @@ using ManagedCommon;
|
||||
using Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.Events;
|
||||
using Microsoft.CmdPal.UI.Messages;
|
||||
using Microsoft.CmdPal.UI.Settings;
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
@@ -17,7 +18,6 @@ using Microsoft.PowerToys.Telemetry;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Input;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Controls.Primitives;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using Microsoft.UI.Xaml.Media.Animation;
|
||||
using DispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue;
|
||||
@@ -242,6 +242,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
}
|
||||
|
||||
_settingsWindow.Activate();
|
||||
_settingsWindow.BringToFront();
|
||||
}
|
||||
|
||||
public void Receive(ShowDetailsMessage message)
|
||||
@@ -413,7 +414,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
}
|
||||
}
|
||||
|
||||
private void BackButton_Tapped(object sender, Microsoft.UI.Xaml.Input.TappedRoutedEventArgs e) => WeakReferenceMessenger.Default.Send<NavigateBackMessage>(new());
|
||||
private void BackButton_Clicked(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) => WeakReferenceMessenger.Default.Send<NavigateBackMessage>(new());
|
||||
|
||||
private void RootFrame_Navigated(object sender, Microsoft.UI.Xaml.Navigation.NavigationEventArgs e)
|
||||
{
|
||||
|
||||
@@ -4,9 +4,10 @@
|
||||
|
||||
using System.Collections.ObjectModel;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.Helpers;
|
||||
using Microsoft.CmdPal.UI.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.UI.Windowing;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
@@ -428,4 +428,10 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
||||
<data name="Settings_ExtensionPage_Alias_ToggleSwitch.OffContent" xml:space="preserve">
|
||||
<value>Indirect</value>
|
||||
</data>
|
||||
<data name="StatusMessagesButton.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Show status messages</value>
|
||||
</data>
|
||||
<data name="StatusMessagesButton.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
|
||||
<value>Show status messages</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -6,8 +6,8 @@ using CommunityToolkit.Mvvm.Messaging;
|
||||
using CommunityToolkit.WinUI;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.Helpers;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Windowing;
|
||||
using Windows.Win32;
|
||||
|
||||
40
src/modules/cmdpal/Microsoft.CmdPal.UI/TypePreservation.cs
Normal file
40
src/modules/cmdpal/Microsoft.CmdPal.UI/TypePreservation.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
// 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.CodeAnalysis;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace Microsoft.CmdPal.UI;
|
||||
|
||||
/// <summary>
|
||||
/// This class ensures types used in XAML are preserved during AOT compilation.
|
||||
/// Framework types cannot have attributes added directly to their definitions since they're external types.
|
||||
/// Application types that require runtime type checking should also be preserved here if needed.
|
||||
/// </summary>
|
||||
internal static class TypePreservation
|
||||
{
|
||||
/// <summary>
|
||||
/// This method ensures critical types are preserved for AOT compilation.
|
||||
/// These types are used dynamically in XAML and would otherwise be trimmed.
|
||||
/// </summary>
|
||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.FontIconSource))]
|
||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.PathIcon))]
|
||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.DataTemplate))]
|
||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.DataTemplateSelector))]
|
||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.ListViewItem))]
|
||||
public static void PreserveTypes()
|
||||
{
|
||||
// This method exists only to hold the DynamicDependency attributes above.
|
||||
// It must be called to ensure the types are not trimmed during AOT compilation.
|
||||
|
||||
// Note: We cannot add [DynamicallyAccessedMembers] directly to framework types
|
||||
// since we don't own their source code. DynamicDependency is the correct approach
|
||||
// for preserving external types that are used dynamically (e.g., in XAML).
|
||||
|
||||
// For application types that require runtime type checking (e.g., in template selectors),
|
||||
// prefer adding [DynamicallyAccessedMembers] attributes directly on the type definitions.
|
||||
// Only use DynamicDependency here for types we cannot modify directly.
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
<Directives xmlns="http://schemas.microsoft.com/netfx/2013/01/metadata">
|
||||
<Application>
|
||||
<Assembly Name="Microsoft.WinUI">
|
||||
<Type Name="Microsoft.UI.Xaml.Controls.FontIconSource" Dynamic="Required All" />
|
||||
<Type Name="Microsoft.UI.Xaml.DataTemplate" Dynamic="Required All" />
|
||||
<Type Name="Microsoft.UI.Xaml.Controls.DataTemplateSelector" Dynamic="Required All" />
|
||||
<Type Name="Microsoft.UI.Xaml.Controls.ListViewItem" Dynamic="Required All" />
|
||||
</Assembly>
|
||||
|
||||
<!-- Add ViewModel types for AOT compatibility -->
|
||||
<Assembly Name="Microsoft.CmdPal.Core.ViewModels">
|
||||
<Type Name="Microsoft.CmdPal.Core.ViewModels.CommandContextItemViewModel" Dynamic="Required All" />
|
||||
<Type Name="Microsoft.CmdPal.Core.ViewModels.SeparatorContextItemViewModel" Dynamic="Required All" />
|
||||
<Type Name="Microsoft.CmdPal.Core.ViewModels.IContextItemViewModel" Dynamic="Required All" />
|
||||
<Type Name="Microsoft.CmdPal.Core.ViewModels.CommandItemViewModel" Dynamic="Required All" />
|
||||
</Assembly>
|
||||
|
||||
<!-- Add UI types for AOT compatibility -->
|
||||
<Assembly Name="Microsoft.CmdPal.UI">
|
||||
<Type Name="Microsoft.CmdPal.UI.ContextItemTemplateSelector" Dynamic="Required All" />
|
||||
</Assembly>
|
||||
</Application>
|
||||
</Directives>
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// 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.
|
||||
|
||||
@@ -6,12 +6,15 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using Microsoft.CmdPal.Ext.Calc.Helper;
|
||||
using Microsoft.CmdPal.Ext.UnitTestBase;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Calc.UnitTests;
|
||||
|
||||
[TestClass]
|
||||
public class ExtendedCalculatorParserTests
|
||||
public class ExtendedCalculatorParserTests : CommandPaletteUnitTestBase
|
||||
{
|
||||
[DataTestMethod]
|
||||
[DataRow(null)]
|
||||
@@ -28,7 +31,7 @@ public class ExtendedCalculatorParserTests
|
||||
[DataRow("[10,10]")] // '[10,10]' is interpreted as array by mages engine
|
||||
public void Interpret_NoResult_WhenCalled(string input)
|
||||
{
|
||||
var settings = new SettingsManager();
|
||||
var settings = new Settings();
|
||||
|
||||
var result = CalculateEngine.Interpret(settings, input, CultureInfo.CurrentCulture, out _);
|
||||
|
||||
@@ -68,7 +71,7 @@ public class ExtendedCalculatorParserTests
|
||||
[DynamicData(nameof(Interpret_NoErrors_WhenCalledWithRounding_Data))]
|
||||
public void Interpret_NoErrors_WhenCalledWithRounding(string input, decimal expectedResult)
|
||||
{
|
||||
var settings = new SettingsManager();
|
||||
var settings = new Settings();
|
||||
|
||||
// Act
|
||||
// Using InvariantCulture since this is internal
|
||||
@@ -90,7 +93,7 @@ public class ExtendedCalculatorParserTests
|
||||
public void Interpret_GreaterPrecision_WhenCalled(string input, decimal expectedResult)
|
||||
{
|
||||
// Arrange
|
||||
var settings = new SettingsManager();
|
||||
var settings = new Settings();
|
||||
|
||||
// Act
|
||||
// Using InvariantCulture since this is internal
|
||||
@@ -114,7 +117,7 @@ public class ExtendedCalculatorParserTests
|
||||
{
|
||||
// Arrange
|
||||
var cultureInfo = CultureInfo.GetCultureInfo(cultureName);
|
||||
var settings = new SettingsManager();
|
||||
var settings = new Settings();
|
||||
|
||||
// Act
|
||||
var result = CalculateEngine.Interpret(settings, input, cultureInfo, out _);
|
||||
@@ -175,7 +178,7 @@ public class ExtendedCalculatorParserTests
|
||||
public void Interpret_MustReturnResult_WhenResultIsZero(string input)
|
||||
{
|
||||
// Arrange
|
||||
var settings = new SettingsManager();
|
||||
var settings = new Settings();
|
||||
|
||||
// Act
|
||||
// Using InvariantCulture since this is internal
|
||||
@@ -203,7 +206,7 @@ public class ExtendedCalculatorParserTests
|
||||
public void Interpret_MustReturnExpectedResult_WhenCalled(string input, decimal expectedResult)
|
||||
{
|
||||
// Arrange
|
||||
var settings = new SettingsManager();
|
||||
var settings = new Settings();
|
||||
|
||||
// Act
|
||||
// Using en-us culture to have a fixed number style
|
||||
@@ -226,7 +229,7 @@ public class ExtendedCalculatorParserTests
|
||||
{
|
||||
// Arrange
|
||||
var translator = NumberTranslator.Create(new CultureInfo(sourceCultureName, false), new CultureInfo("en-US", false));
|
||||
var settings = new SettingsManager();
|
||||
var settings = new Settings();
|
||||
|
||||
// Act
|
||||
// Using en-us culture to have a fixed number style
|
||||
|
||||
@@ -14,5 +14,6 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\ext\Microsoft.CmdPal.Ext.Calc\Microsoft.CmdPal.Ext.Calc.csproj" />
|
||||
<ProjectReference Include="..\Microsoft.CmdPal.Ext.UnitTestsBase\Microsoft.CmdPal.Ext.UnitTestBase.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,86 @@
|
||||
// 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.Linq;
|
||||
using Microsoft.CmdPal.Ext.Calc.Helper;
|
||||
using Microsoft.CmdPal.Ext.Calc.Pages;
|
||||
using Microsoft.CmdPal.Ext.UnitTestBase;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Calc.UnitTests;
|
||||
|
||||
[TestClass]
|
||||
public class QueryTests : CommandPaletteUnitTestBase
|
||||
{
|
||||
[DataTestMethod]
|
||||
[DataRow("2+2", "4")]
|
||||
[DataRow("5*3", "15")]
|
||||
[DataRow("10/2", "5")]
|
||||
[DataRow("sqrt(16)", "4")]
|
||||
[DataRow("2^3", "8")]
|
||||
public void TopLevelPageQueryTest(string input, string expectedResult)
|
||||
{
|
||||
var settings = new Settings();
|
||||
var page = new CalculatorListPage(settings);
|
||||
|
||||
// Simulate query execution
|
||||
page.UpdateSearchText(string.Empty, input);
|
||||
var result = page.GetItems();
|
||||
|
||||
Assert.IsTrue(result.Length == 1, "Valid input should always return result");
|
||||
|
||||
var firstResult = result.FirstOrDefault();
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
Assert.IsTrue(
|
||||
firstResult.Title.Contains(expectedResult),
|
||||
$"Expected result to contain '{expectedResult}' but got '{firstResult.Title}'");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void EmptyQueryTest()
|
||||
{
|
||||
var settings = new Settings();
|
||||
var page = new CalculatorListPage(settings);
|
||||
page.UpdateSearchText("abc", string.Empty);
|
||||
var results = page.GetItems();
|
||||
Assert.IsNotNull(results);
|
||||
|
||||
var firstItem = results.FirstOrDefault();
|
||||
Assert.AreEqual("Type an equation...", firstItem.Title);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void InvalidExpressionTest()
|
||||
{
|
||||
var settings = new Settings();
|
||||
|
||||
var page = new CalculatorListPage(settings);
|
||||
|
||||
// Simulate query execution
|
||||
page.UpdateSearchText(string.Empty, "invalid expression");
|
||||
var result = page.GetItems().FirstOrDefault();
|
||||
|
||||
Assert.AreEqual("Type an equation...", result.Title);
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow("sin(60)", "-0.30481", CalculateEngine.TrigMode.Radians)]
|
||||
[DataRow("sin(60)", "0.866025", CalculateEngine.TrigMode.Degrees)]
|
||||
[DataRow("sin(60)", "0.809016", CalculateEngine.TrigMode.Gradians)]
|
||||
public void TrigModeSettingsTest(string input, string expected, CalculateEngine.TrigMode trigMode)
|
||||
{
|
||||
var settings = new Settings(trigUnit: trigMode);
|
||||
|
||||
var page = new CalculatorListPage(settings);
|
||||
|
||||
page.UpdateSearchText(string.Empty, input);
|
||||
var result = page.GetItems().FirstOrDefault();
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
|
||||
Assert.IsTrue(result.Title.Contains(expected, System.StringComparison.Ordinal), $"Calc trigMode convert result isn't correct. Current result: {result.Title}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CmdPal.Ext.Calc.Helper;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Calc.UnitTests;
|
||||
|
||||
public class Settings : ISettingsInterface
|
||||
{
|
||||
private readonly CalculateEngine.TrigMode trigUnit;
|
||||
private readonly bool inputUseEnglishFormat;
|
||||
private readonly bool outputUseEnglishFormat;
|
||||
private readonly bool closeOnEnter;
|
||||
|
||||
public Settings(
|
||||
CalculateEngine.TrigMode trigUnit = CalculateEngine.TrigMode.Radians,
|
||||
bool inputUseEnglishFormat = false,
|
||||
bool outputUseEnglishFormat = false,
|
||||
bool closeOnEnter = true)
|
||||
{
|
||||
this.trigUnit = trigUnit;
|
||||
this.inputUseEnglishFormat = inputUseEnglishFormat;
|
||||
this.outputUseEnglishFormat = outputUseEnglishFormat;
|
||||
this.closeOnEnter = closeOnEnter;
|
||||
}
|
||||
|
||||
public CalculateEngine.TrigMode TrigUnit => trigUnit;
|
||||
|
||||
public bool InputUseEnglishFormat => inputUseEnglishFormat;
|
||||
|
||||
public bool OutputUseEnglishFormat => outputUseEnglishFormat;
|
||||
|
||||
public bool CloseOnEnter => closeOnEnter;
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CmdPal.Ext.Calc.Helper;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Calc.UnitTests;
|
||||
|
||||
[TestClass]
|
||||
public class SettingsManagerTests
|
||||
{
|
||||
[TestMethod]
|
||||
public void SettingsManagerInitializationTest()
|
||||
{
|
||||
// Act
|
||||
var settingsManager = new SettingsManager();
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(settingsManager);
|
||||
Assert.IsNotNull(settingsManager.Settings);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SettingsInterfaceTest()
|
||||
{
|
||||
// Act
|
||||
ISettingsInterface settings = new SettingsManager();
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(settings);
|
||||
Assert.IsTrue(settings.TrigUnit == CalculateEngine.TrigMode.Radians);
|
||||
Assert.IsFalse(settings.InputUseEnglishFormat);
|
||||
Assert.IsFalse(settings.OutputUseEnglishFormat);
|
||||
Assert.IsTrue(settings.CloseOnEnter);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void MockSettingsTest()
|
||||
{
|
||||
// Act
|
||||
var settings = new Settings(
|
||||
trigUnit: CalculateEngine.TrigMode.Degrees,
|
||||
inputUseEnglishFormat: true,
|
||||
outputUseEnglishFormat: true,
|
||||
closeOnEnter: false);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(settings);
|
||||
Assert.AreEqual(CalculateEngine.TrigMode.Degrees, settings.TrigUnit);
|
||||
Assert.IsTrue(settings.InputUseEnglishFormat);
|
||||
Assert.IsTrue(settings.OutputUseEnglishFormat);
|
||||
Assert.IsFalse(settings.CloseOnEnter);
|
||||
}
|
||||
}
|
||||
@@ -18,5 +18,6 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\ext\Microsoft.CmdPal.Ext.Registry\Microsoft.CmdPal.Ext.Registry.csproj" />
|
||||
<ProjectReference Include="..\Microsoft.CmdPal.Ext.UnitTestsBase\Microsoft.CmdPal.Ext.UnitTestBase.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
// 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.Linq;
|
||||
using Microsoft.CmdPal.Ext.Registry.Helpers;
|
||||
using Microsoft.CmdPal.Ext.UnitTestBase;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Registry.UnitTests;
|
||||
|
||||
[TestClass]
|
||||
public class QueryTests : CommandPaletteUnitTestBase
|
||||
{
|
||||
[DataTestMethod]
|
||||
[DataRow("HKLM", "HKEY_LOCAL_MACHINE")]
|
||||
[DataRow("HKCU", "HKEY_CURRENT_USER")]
|
||||
[DataRow("HKCR", "HKEY_CLASSES_ROOT")]
|
||||
[DataRow("HKU", "HKEY_USERS")]
|
||||
[DataRow("HKCC", "HKEY_CURRENT_CONFIG")]
|
||||
public void TopLevelPageQueryTest(string input, string expectedKeyName)
|
||||
{
|
||||
var settings = new Settings();
|
||||
var page = new RegistryListPage(settings);
|
||||
var results = page.Query(input);
|
||||
|
||||
Assert.IsNotNull(results);
|
||||
Assert.IsTrue(results.Count > 0, "No items matched the query.");
|
||||
|
||||
var firstItem = results.FirstOrDefault();
|
||||
Assert.IsNotNull(firstItem, "No items matched the query.");
|
||||
Assert.IsTrue(
|
||||
firstItem.Title.Contains(expectedKeyName, System.StringComparison.OrdinalIgnoreCase),
|
||||
$"Expected to match '{expectedKeyName}' but got '{firstItem.Title}'");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void EmptyQueryTest()
|
||||
{
|
||||
var settings = new Settings();
|
||||
var page = new RegistryListPage(settings);
|
||||
var results = page.Query(string.Empty);
|
||||
|
||||
Assert.IsNotNull(results);
|
||||
|
||||
// Empty query should return all base keys
|
||||
Assert.IsTrue(results.Count >= 5, "Expected at least 5 base registry keys.");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void NullQueryTest()
|
||||
{
|
||||
var settings = new Settings();
|
||||
var page = new RegistryListPage(settings);
|
||||
var results = page.Query(null);
|
||||
|
||||
Assert.IsNotNull(results);
|
||||
Assert.AreEqual(0, results.Count, "Null query should return empty results.");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void InvalidBaseKeyTest()
|
||||
{
|
||||
var settings = new Settings();
|
||||
var page = new RegistryListPage(settings);
|
||||
var results = page.Query("INVALID_KEY");
|
||||
|
||||
Assert.IsNotNull(results);
|
||||
|
||||
Assert.AreEqual(0, results.Count, "Invalid query should return empty results.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CmdPal.Ext.Registry.Helpers;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Registry.UnitTests;
|
||||
|
||||
public class Settings : ISettingsInterface
|
||||
{
|
||||
public Settings()
|
||||
{
|
||||
// Currently no specific settings for Registry extension
|
||||
}
|
||||
}
|
||||
@@ -11,17 +11,6 @@ namespace Microsoft.CmdPal.Ext.System.UnitTests;
|
||||
[TestClass]
|
||||
public class BasicTests
|
||||
{
|
||||
[TestMethod]
|
||||
public void CommandsHelperTest()
|
||||
{
|
||||
// Setup & Act
|
||||
var commands = Commands.GetSystemCommands(false, false, false, false);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(commands);
|
||||
Assert.IsTrue(commands.Count > 0);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void IconsHelperTest()
|
||||
{
|
||||
|
||||
@@ -16,56 +16,25 @@ namespace Microsoft.CmdPal.Ext.System.UnitTests;
|
||||
[TestClass]
|
||||
public class ImageTests
|
||||
{
|
||||
[DataTestMethod]
|
||||
[DataRow("shutdown", "ShutdownIcon")]
|
||||
[DataRow("restart", "RestartIcon")]
|
||||
[DataRow("sign out", "LogoffIcon")]
|
||||
[DataRow("lock", "LockIcon")]
|
||||
[DataRow("sleep", "SleepIcon")]
|
||||
[DataRow("hibernate", "SleepIcon")]
|
||||
[DataRow("recycle bin", "RecycleBinIcon")]
|
||||
[DataRow("uefi firmware settings", "FirmwareSettingsIcon")]
|
||||
[DataRow("IPv4 addr", "NetworkAdapterIcon")]
|
||||
[DataRow("IPV6 addr", "NetworkAdapterIcon")]
|
||||
[DataRow("MAC addr", "NetworkAdapterIcon")]
|
||||
public void IconThemeDarkTest(string typedString, string expectedIconPropertyName)
|
||||
[DataRow(true)]
|
||||
[DataRow(false)]
|
||||
[TestMethod]
|
||||
public void IconThemeTest(bool isDarkIcon)
|
||||
{
|
||||
var systemPage = new SystemCommandPage(new SettingsManager());
|
||||
var systemPage = new SystemCommandPage(new Settings());
|
||||
var commands = systemPage.GetItems();
|
||||
|
||||
foreach (var item in systemPage.GetItems())
|
||||
foreach (var item in commands)
|
||||
{
|
||||
if (item.Title.Contains(typedString, StringComparison.OrdinalIgnoreCase) || item.Subtitle.Contains(typedString, StringComparison.OrdinalIgnoreCase))
|
||||
var icon = item.Icon;
|
||||
Assert.IsNotNull(icon, $"Icon for '{item.Title}' should not be null.");
|
||||
if (isDarkIcon)
|
||||
{
|
||||
var icon = item.Icon;
|
||||
Assert.IsNotNull(icon, $"Icon for '{typedString}' should not be null.");
|
||||
Assert.IsNotEmpty(icon.Dark.Icon, $"Icon for '{typedString}' should not be empty.");
|
||||
Assert.IsNotEmpty(icon.Dark.Icon, $"Icon for '{item.Title}' should not be empty.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow("shutdown", "ShutdownIcon")]
|
||||
[DataRow("restart", "RestartIcon")]
|
||||
[DataRow("sign out", "LogoffIcon")]
|
||||
[DataRow("lock", "LockIcon")]
|
||||
[DataRow("sleep", "SleepIcon")]
|
||||
[DataRow("hibernate", "SleepIcon")]
|
||||
[DataRow("recycle bin", "RecycleBinIcon")]
|
||||
[DataRow("uefi firmware settings", "FirmwareSettingsIcon")]
|
||||
[DataRow("IPv4 addr", "NetworkAdapterIcon")]
|
||||
[DataRow("IPV6 addr", "NetworkAdapterIcon")]
|
||||
[DataRow("MAC addr", "NetworkAdapterIcon")]
|
||||
public void IconThemeLightTest(string typedString, string expectedIconPropertyName)
|
||||
{
|
||||
var systemPage = new SystemCommandPage(new SettingsManager());
|
||||
|
||||
foreach (var item in systemPage.GetItems())
|
||||
{
|
||||
if (item.Title.Contains(typedString, StringComparison.OrdinalIgnoreCase) || item.Subtitle.Contains(typedString, StringComparison.OrdinalIgnoreCase))
|
||||
else
|
||||
{
|
||||
var icon = item.Icon;
|
||||
Assert.IsNotNull(icon, $"Icon for '{typedString}' should not be null.");
|
||||
Assert.IsNotEmpty(icon.Light.Icon, $"Icon for '{typedString}' should not be empty.");
|
||||
Assert.IsNotEmpty(icon.Light.Icon, $"Icon for '{item.Title}' should not be empty.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,5 +20,6 @@
|
||||
<ProjectReference Include="..\..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
|
||||
<ProjectReference Include="..\..\ext\Microsoft.CmdPal.Ext.System\Microsoft.CmdPal.Ext.System.csproj" />
|
||||
<ProjectReference Include="..\..\extensionsdk\Microsoft.CommandPalette.Extensions.Toolkit\Microsoft.CommandPalette.Extensions.Toolkit.csproj" />
|
||||
<ProjectReference Include="..\Microsoft.CmdPal.Ext.UnitTestsBase\Microsoft.CmdPal.Ext.UnitTestBase.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -5,12 +5,16 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Microsoft.CmdPal.Ext.System.Helpers;
|
||||
using Microsoft.CmdPal.Ext.System.Pages;
|
||||
using Microsoft.CmdPal.Ext.UnitTestBase;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.System.UnitTests;
|
||||
|
||||
[TestClass]
|
||||
public class QueryTests
|
||||
public class QueryTests : CommandPaletteUnitTestBase
|
||||
{
|
||||
[DataTestMethod]
|
||||
[DataRow("shutdown", "Shutdown")]
|
||||
@@ -19,87 +23,126 @@ public class QueryTests
|
||||
[DataRow("lock", "Lock")]
|
||||
[DataRow("sleep", "Sleep")]
|
||||
[DataRow("hibernate", "Hibernate")]
|
||||
public void SystemCommandsTest(string typedString, string expectedCommand)
|
||||
[DataRow("open recycle", "Open Recycle Bin")]
|
||||
[DataRow("empty recycle", "Empty Recycle Bin")]
|
||||
[DataRow("uefi", "UEFI Firmware Settings")]
|
||||
public void TopLevelPageQueryTest(string input, string matchedTitle)
|
||||
{
|
||||
// Setup
|
||||
var commands = Commands.GetSystemCommands(false, false, false, false);
|
||||
var settings = new Settings();
|
||||
var pages = new SystemCommandPage(settings);
|
||||
var allCommands = pages.GetItems();
|
||||
|
||||
// Act
|
||||
var result = commands.Where(c => c.Title.Contains(expectedCommand, StringComparison.OrdinalIgnoreCase)).FirstOrDefault();
|
||||
var result = Query(input, allCommands);
|
||||
|
||||
// Assert
|
||||
// Empty recycle bin command should exist
|
||||
Assert.IsNotNull(result);
|
||||
Assert.IsTrue(result.Title.Contains(expectedCommand, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
var firstItem = result.FirstOrDefault();
|
||||
|
||||
Assert.IsNotNull(firstItem, "No items matched the query.");
|
||||
Assert.AreEqual(matchedTitle, firstItem.Title, $"Expected to match '{input}' but got '{firstItem.Title}'");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void RecycleBinCommandTest()
|
||||
{
|
||||
// Setup
|
||||
var commands = Commands.GetSystemCommands(false, false, false, false);
|
||||
var settings = new Settings(hideEmptyRecycleBin: true);
|
||||
var pages = new SystemCommandPage(settings);
|
||||
var allCommands = pages.GetItems();
|
||||
|
||||
// Act
|
||||
var result = commands.Where(c => c.Title.Contains("Recycle", StringComparison.OrdinalIgnoreCase)).FirstOrDefault();
|
||||
var result = Query("recycle", allCommands);
|
||||
|
||||
// Assert
|
||||
// Empty recycle bin command should exist
|
||||
Assert.IsNotNull(result);
|
||||
|
||||
foreach (var item in result)
|
||||
{
|
||||
if (item.Title.Contains("Open Recycle Bin") || item.Title.Contains("Empty Recycle Bin"))
|
||||
{
|
||||
Assert.Fail("Recycle Bin commands should not be available when hideEmptyRecycleBin is true.");
|
||||
}
|
||||
}
|
||||
|
||||
var firstItem = result.FirstOrDefault();
|
||||
Assert.IsNotNull(firstItem, "No items matched the query.");
|
||||
Assert.IsTrue(
|
||||
firstItem.Title.Contains("Recycle Bin", StringComparison.OrdinalIgnoreCase),
|
||||
$"Expected to match 'Recycle Bin' but got '{firstItem.Title}'");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void NetworkCommandsTest()
|
||||
{
|
||||
// Test that network commands can be retrieved
|
||||
try
|
||||
var settings = new Settings();
|
||||
var pages = new SystemCommandPage(settings);
|
||||
var allCommands = pages.GetItems();
|
||||
|
||||
var ipv4Result = Query("IPv4", allCommands);
|
||||
|
||||
Assert.IsNotNull(ipv4Result);
|
||||
Assert.IsTrue(ipv4Result.Length > 0, "No IPv4 commands matched the query.");
|
||||
|
||||
var ipv6Result = Query("IPv6", allCommands);
|
||||
Assert.IsNotNull(ipv6Result);
|
||||
Assert.IsTrue(ipv6Result.Length > 0, "No IPv6 commands matched the query.");
|
||||
|
||||
var macResult = Query("MAC", allCommands);
|
||||
Assert.IsNotNull(macResult);
|
||||
Assert.IsTrue(macResult.Length > 0, "No MAC commands matched the query.");
|
||||
|
||||
var findDisconnectedMACResult = false;
|
||||
foreach (var item in macResult)
|
||||
{
|
||||
var networkPropertiesList = NetworkConnectionProperties.GetList();
|
||||
Assert.IsTrue(networkPropertiesList.Count >= 0); // Should not throw exceptions
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Assert.Fail($"Network commands should not throw exceptions: {ex.Message}");
|
||||
if (item.Details.Body.Contains("Disconnected"))
|
||||
{
|
||||
findDisconnectedMACResult = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Assert.IsTrue(findDisconnectedMACResult, "No disconnected MAC address found in the results.");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void UefiCommandIsAvailableTest()
|
||||
public void HideDisconnectedNetworkInfoTest()
|
||||
{
|
||||
// Setup
|
||||
var firmwareType = Win32Helpers.GetSystemFirmwareType();
|
||||
var isUefiMode = firmwareType == FirmwareType.Uefi;
|
||||
var settings = new Settings(hideDisconnectedNetworkInfo: true);
|
||||
var pages = new SystemCommandPage(settings);
|
||||
var allCommands = pages.GetItems();
|
||||
|
||||
// Act
|
||||
var commands = Commands.GetSystemCommands(isUefiMode, false, false, false);
|
||||
var uefiCommand = commands.Where(c => c.Title.Contains("UEFI", StringComparison.OrdinalIgnoreCase)).FirstOrDefault();
|
||||
var macResult = Query("MAC", allCommands);
|
||||
Assert.IsNotNull(macResult);
|
||||
Assert.IsTrue(macResult.Length > 0, "No MAC commands matched the query.");
|
||||
|
||||
// Assert
|
||||
if (isUefiMode)
|
||||
var findDisconnectedMACResult = false;
|
||||
foreach (var item in macResult)
|
||||
{
|
||||
Assert.IsNotNull(uefiCommand);
|
||||
}
|
||||
else
|
||||
{
|
||||
// UEFI command may still exist but be disabled on non-UEFI systems
|
||||
Assert.IsTrue(true); // Test environment independent
|
||||
if (item.Details.Body.Contains("Disconnected"))
|
||||
{
|
||||
findDisconnectedMACResult = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Assert.IsTrue(!findDisconnectedMACResult, "Disconnected MAC address found in the results.");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void FirmwareTypeTest()
|
||||
[DataRow(FirmwareType.Uefi, true)]
|
||||
[DataRow(FirmwareType.Bios, false)]
|
||||
[DataRow(FirmwareType.Max, false)]
|
||||
[DataRow(FirmwareType.Unknown, false)]
|
||||
public void FirmwareSettingsTest(FirmwareType firmwareType, bool hasCommand)
|
||||
{
|
||||
// Test that GetSystemFirmwareType returns a valid enum value
|
||||
var firmwareType = Win32Helpers.GetSystemFirmwareType();
|
||||
Assert.IsTrue(Enum.IsDefined(typeof(FirmwareType), firmwareType));
|
||||
}
|
||||
var settings = new Settings(firmwareType: firmwareType);
|
||||
var pages = new SystemCommandPage(settings);
|
||||
var allCommands = pages.GetItems();
|
||||
var result = Query("UEFI", allCommands);
|
||||
|
||||
[TestMethod]
|
||||
public void EmptyRecycleBinCommandTest()
|
||||
{
|
||||
// Test that empty recycle bin command exists
|
||||
var commands = Commands.GetSystemCommands(false, false, false, false);
|
||||
var result = commands.Where(c => c.Title.Contains("Empty", StringComparison.OrdinalIgnoreCase) &&
|
||||
c.Title.Contains("Recycle", StringComparison.OrdinalIgnoreCase)).FirstOrDefault();
|
||||
|
||||
// Empty recycle bin command should exist
|
||||
// UEFI Firmware Settings command should exist
|
||||
Assert.IsNotNull(result);
|
||||
var firstItem = result.FirstOrDefault();
|
||||
var firstItemIsUefiCommand = firstItem?.Title.Contains("UEFI", StringComparison.OrdinalIgnoreCase) ?? false;
|
||||
Assert.AreEqual(hasCommand, firstItemIsUefiCommand, $"Expected to match (or not match) 'UEFI Firmware Settings' but got '{firstItem?.Title}'");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CmdPal.Ext.System.Helpers;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.System.UnitTests;
|
||||
|
||||
public class Settings : ISettingsInterface
|
||||
{
|
||||
private bool hideDisconnectedNetworkInfo;
|
||||
private bool hideEmptyRecycleBin;
|
||||
private bool showDialogToConfirmCommand;
|
||||
private bool showSuccessMessageAfterEmptyingRecycleBin;
|
||||
private FirmwareType firmwareType;
|
||||
|
||||
public Settings(bool hideDisconnectedNetworkInfo = false, bool hideEmptyRecycleBin = false, bool showDialogToConfirmCommand = false, bool showSuccessMessageAfterEmptyingRecycleBin = false, FirmwareType firmwareType = FirmwareType.Uefi)
|
||||
{
|
||||
this.hideDisconnectedNetworkInfo = hideDisconnectedNetworkInfo;
|
||||
this.hideEmptyRecycleBin = hideEmptyRecycleBin;
|
||||
this.showDialogToConfirmCommand = showDialogToConfirmCommand;
|
||||
this.showSuccessMessageAfterEmptyingRecycleBin = showSuccessMessageAfterEmptyingRecycleBin;
|
||||
this.firmwareType = firmwareType;
|
||||
}
|
||||
|
||||
public bool HideDisconnectedNetworkInfo() => hideDisconnectedNetworkInfo;
|
||||
|
||||
public bool HideEmptyRecycleBin() => hideEmptyRecycleBin;
|
||||
|
||||
public bool ShowDialogToConfirmCommand() => showDialogToConfirmCommand;
|
||||
|
||||
public bool ShowSuccessMessageAfterEmptyingRecycleBin() => showSuccessMessageAfterEmptyingRecycleBin;
|
||||
|
||||
public FirmwareType GetSystemFirmwareType() => firmwareType;
|
||||
}
|
||||
@@ -367,7 +367,7 @@ public class AvailableResultsListTests
|
||||
public void UnixTimestampSecondsFormat()
|
||||
{
|
||||
// Setup
|
||||
string formatLabel = "Unix epoch time";
|
||||
var formatLabel = "Unix epoch time";
|
||||
DateTime timeValue = DateTime.Now.ToUniversalTime();
|
||||
var settings = new SettingsManager();
|
||||
var helperResults = AvailableResultsList.GetList(true, settings, null, null, timeValue);
|
||||
@@ -384,7 +384,7 @@ public class AvailableResultsListTests
|
||||
public void UnixTimestampMillisecondsFormat()
|
||||
{
|
||||
// Setup
|
||||
string formatLabel = "Unix epoch time in milliseconds";
|
||||
var formatLabel = "Unix epoch time in milliseconds";
|
||||
DateTime timeValue = DateTime.Now.ToUniversalTime();
|
||||
var settings = new SettingsManager();
|
||||
var helperResults = AvailableResultsList.GetList(true, settings, null, null, timeValue);
|
||||
@@ -401,7 +401,7 @@ public class AvailableResultsListTests
|
||||
public void WindowsFileTimeFormat()
|
||||
{
|
||||
// Setup
|
||||
string formatLabel = "Windows file time (Int64 number)";
|
||||
var formatLabel = "Windows file time (Int64 number)";
|
||||
DateTime timeValue = DateTime.Now;
|
||||
var settings = new SettingsManager();
|
||||
var helperResults = AvailableResultsList.GetList(true, settings, null, null, timeValue);
|
||||
@@ -418,7 +418,7 @@ public class AvailableResultsListTests
|
||||
public void ValidateEraResult()
|
||||
{
|
||||
// Setup
|
||||
string formatLabel = "Era";
|
||||
var formatLabel = "Era";
|
||||
DateTime timeValue = DateTime.Now;
|
||||
var settings = new SettingsManager();
|
||||
var helperResults = AvailableResultsList.GetList(true, settings, null, null, timeValue);
|
||||
@@ -435,7 +435,7 @@ public class AvailableResultsListTests
|
||||
public void ValidateEraAbbreviationResult()
|
||||
{
|
||||
// Setup
|
||||
string formatLabel = "Era abbreviation";
|
||||
var formatLabel = "Era abbreviation";
|
||||
DateTime timeValue = DateTime.Now;
|
||||
var settings = new SettingsManager();
|
||||
var helperResults = AvailableResultsList.GetList(true, settings, null, null, timeValue);
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
// 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 Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.TimeDate.UnitTests;
|
||||
|
||||
[TestClass]
|
||||
public class BasicTests
|
||||
{
|
||||
[TestMethod]
|
||||
public void BasicTest()
|
||||
{
|
||||
// This is a basic test to verify the test project can run
|
||||
Assert.IsTrue(true);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void DateTimeTest()
|
||||
{
|
||||
// Test basic DateTime functionality
|
||||
var now = DateTime.Now;
|
||||
Assert.IsNotNull(now);
|
||||
Assert.IsTrue(now > DateTime.MinValue);
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,7 @@ public class FallbackTimeDateItemTests
|
||||
public void FallbackQueryTests(string query, string expectedTitle)
|
||||
{
|
||||
// Setup
|
||||
var settingsManager = new SettingsManager();
|
||||
var settingsManager = new Settings();
|
||||
DateTime now = new DateTime(2025, 7, 1, 12, 0, 0); // Fixed date for testing
|
||||
var fallbackItem = new FallbackTimeDateItem(settingsManager, now);
|
||||
|
||||
@@ -66,7 +66,7 @@ public class FallbackTimeDateItemTests
|
||||
public void InvalidQueryTests(string query)
|
||||
{
|
||||
// Setup
|
||||
var settingsManager = new SettingsManager();
|
||||
var settingsManager = new Settings();
|
||||
DateTime now = new DateTime(2025, 7, 1, 12, 0, 0); // Fixed date for testing
|
||||
var fallbackItem = new FallbackTimeDateItem(settingsManager, now);
|
||||
|
||||
@@ -83,4 +83,26 @@ public class FallbackTimeDateItemTests
|
||||
Assert.Fail($"UpdateQuery should not throw exceptions: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
public void DisableFallbackItemTest()
|
||||
{
|
||||
// Setup
|
||||
var settingsManager = new Settings(enableFallbackItems: false);
|
||||
DateTime now = new DateTime(2025, 7, 1, 12, 0, 0); // Fixed date for testing
|
||||
var fallbackItem = new FallbackTimeDateItem(settingsManager, now);
|
||||
|
||||
// Act & Assert - Test that UpdateQuery doesn't throw exceptions
|
||||
try
|
||||
{
|
||||
fallbackItem.UpdateQuery("now");
|
||||
|
||||
Assert.AreEqual(string.Empty, fallbackItem.Title, "Title should be empty when disable fallback item");
|
||||
Assert.AreEqual(string.Empty, fallbackItem.Subtitle, "Subtitle should be empty when disable fallback item");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Assert.Fail($"UpdateQuery should not throw exceptions: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
// 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.Globalization;
|
||||
using Microsoft.CmdPal.Ext.TimeDate.Helpers;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.TimeDate.UnitTests;
|
||||
|
||||
[TestClass]
|
||||
public class IconTests
|
||||
{
|
||||
private CultureInfo originalCulture;
|
||||
private CultureInfo originalUiCulture;
|
||||
|
||||
[TestInitialize]
|
||||
public void Setup()
|
||||
{
|
||||
// Set culture to 'en-us'
|
||||
originalCulture = CultureInfo.CurrentCulture;
|
||||
CultureInfo.CurrentCulture = new CultureInfo("en-us", false);
|
||||
originalUiCulture = CultureInfo.CurrentUICulture;
|
||||
CultureInfo.CurrentUICulture = new CultureInfo("en-us", false);
|
||||
}
|
||||
|
||||
[TestCleanup]
|
||||
public void CleanUp()
|
||||
{
|
||||
// Set culture to original value
|
||||
CultureInfo.CurrentCulture = originalCulture;
|
||||
CultureInfo.CurrentUICulture = originalUiCulture;
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TimeDateCommandsProvider_HasIcon()
|
||||
{
|
||||
// Setup
|
||||
var provider = new TimeDateCommandsProvider();
|
||||
|
||||
// Act
|
||||
var icon = provider.Icon;
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(icon, "Provider should have an icon");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TimeDateCommandsProvider_TopLevelCommands_HaveIcons()
|
||||
{
|
||||
// Setup
|
||||
var provider = new TimeDateCommandsProvider();
|
||||
|
||||
// Act
|
||||
var commands = provider.TopLevelCommands();
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(commands);
|
||||
Assert.IsTrue(commands.Length > 0, "Should have at least one top-level command");
|
||||
|
||||
foreach (var command in commands)
|
||||
{
|
||||
Assert.IsNotNull(command.Icon, "Each command should have an icon");
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void AvailableResults_HaveIcons()
|
||||
{
|
||||
// Setup
|
||||
var settings = new SettingsManager();
|
||||
|
||||
// Act
|
||||
var results = AvailableResultsList.GetList(true, settings);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(results);
|
||||
Assert.IsTrue(results.Count > 0, "Should have results");
|
||||
|
||||
foreach (var result in results)
|
||||
{
|
||||
Assert.IsNotNull(result.GetIconInfo(), $"Result '{result.Label}' should have an icon");
|
||||
}
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow(ResultIconType.Time, "\uE823")]
|
||||
[DataRow(ResultIconType.Date, "\uE787")]
|
||||
[DataRow(ResultIconType.DateTime, "\uEC92")]
|
||||
public void ResultHelper_CreateListItem_PreservesIcon(ResultIconType resultIconType, string expectedIcon)
|
||||
{
|
||||
// Setup
|
||||
var availableResult = new AvailableResult
|
||||
{
|
||||
Label = "Test Label",
|
||||
Value = "Test Value",
|
||||
IconType = resultIconType,
|
||||
};
|
||||
|
||||
// Act
|
||||
var listItem = availableResult.ToListItem();
|
||||
|
||||
var icon = listItem.Icon;
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(listItem);
|
||||
Assert.IsNotNull(listItem.Icon, "ListItem should preserve the icon from AvailableResult");
|
||||
Assert.AreEqual(expectedIcon, icon.Dark.Icon, $"Icon for {resultIconType} should match expected value");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Icons_AreNotEmpty()
|
||||
{
|
||||
// Setup
|
||||
var settings = new SettingsManager();
|
||||
var results = AvailableResultsList.GetList(true, settings);
|
||||
|
||||
// Act & Assert
|
||||
foreach (var result in results)
|
||||
{
|
||||
Assert.IsNotNull(result.GetIconInfo(), $"Result '{result.Label}' should have an icon");
|
||||
Assert.IsFalse(string.IsNullOrWhiteSpace(result.GetIconInfo().ToString()), $"Icon for '{result.Label}' should not be empty");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,5 +19,6 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
|
||||
<ProjectReference Include="..\..\ext\Microsoft.CmdPal.Ext.TimeDate\Microsoft.CmdPal.Ext.TimeDate.csproj" />
|
||||
<ProjectReference Include="..\Microsoft.CmdPal.Ext.UnitTestsBase\Microsoft.CmdPal.Ext.UnitTestBase.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -6,13 +6,14 @@ using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Microsoft.CmdPal.Ext.TimeDate.Helpers;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Microsoft.CmdPal.Ext.TimeDate.Pages;
|
||||
using Microsoft.CmdPal.Ext.UnitTestBase;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.TimeDate.UnitTests;
|
||||
|
||||
[TestClass]
|
||||
public class QueryTests
|
||||
public class QueryTests : CommandPaletteUnitTestBase
|
||||
{
|
||||
private CultureInfo originalCulture;
|
||||
private CultureInfo originalUiCulture;
|
||||
@@ -46,7 +47,7 @@ public class QueryTests
|
||||
public void CountBasicQueries(string query, int expectedMinResultCount)
|
||||
{
|
||||
// Setup
|
||||
var settings = new SettingsManager();
|
||||
var settings = new Settings();
|
||||
|
||||
// Act
|
||||
var results = TimeDateCalculator.ExecuteSearch(settings, query);
|
||||
@@ -58,30 +59,32 @@ public class QueryTests
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow("time")]
|
||||
[DataRow("date")]
|
||||
[DataRow("year")]
|
||||
[DataRow("now")]
|
||||
[DataRow("current")]
|
||||
[DataRow("")]
|
||||
[DataRow("now::10:10:10")] // Windows file time
|
||||
public void AllQueriesReturnResults(string query)
|
||||
[DataRow("time", "time")]
|
||||
[DataRow("date", "date")]
|
||||
[DataRow("year", "year")]
|
||||
[DataRow("now", "now")]
|
||||
[DataRow("year", "year")]
|
||||
public void BasicQueryTest(string input, string expectedMatchTerm)
|
||||
{
|
||||
// Setup
|
||||
var settings = new SettingsManager();
|
||||
var settings = new Settings();
|
||||
var page = new TimeDateExtensionPage(settings);
|
||||
page.UpdateSearchText(string.Empty, input);
|
||||
var resultLists = page.GetItems();
|
||||
|
||||
// Act
|
||||
var results = TimeDateCalculator.ExecuteSearch(settings, query);
|
||||
var result = Query(input, resultLists);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(results);
|
||||
Assert.IsTrue(results.Count > 0, $"Query '{query}' should return at least one result");
|
||||
Assert.IsNotNull(result);
|
||||
Assert.IsTrue(result.Length > 0, "No items matched the query.");
|
||||
|
||||
var firstItem = result.FirstOrDefault();
|
||||
Assert.IsNotNull(firstItem, "No items matched the query.");
|
||||
Assert.IsTrue(
|
||||
firstItem.Title.Contains(expectedMatchTerm, System.StringComparison.OrdinalIgnoreCase) ||
|
||||
firstItem.Subtitle.Contains(expectedMatchTerm, System.StringComparison.OrdinalIgnoreCase),
|
||||
$"Expected to match '{expectedMatchTerm}' in title or subtitle but got '{firstItem.Title}' - '{firstItem.Subtitle}'");
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow("time", "Time")]
|
||||
[DataRow("date", "Date")]
|
||||
[DataRow("now", "Now")]
|
||||
[DataRow("unix", "Unix epoch time")]
|
||||
[DataRow("unix epoch time in milli", "Unix epoch time in milliseconds")]
|
||||
[DataRow("file", "Windows file time (Int64 number)")]
|
||||
@@ -98,12 +101,8 @@ public class QueryTests
|
||||
[DataRow("month", "Month")]
|
||||
[DataRow("month of year", "Month of the year")]
|
||||
[DataRow("month and d", "Month and day")]
|
||||
[DataRow("month and y", "Month and year")]
|
||||
[DataRow("year", "Year")]
|
||||
[DataRow("era", "Era")]
|
||||
[DataRow("era a", "Era abbreviation")]
|
||||
[DataRow("universal", "Universal time format: YYYY-MM-DD hh:mm:ss")]
|
||||
[DataRow("iso", "ISO 8601")]
|
||||
[DataRow("rfc", "RFC1123")]
|
||||
[DataRow("time::12:30", "Time")]
|
||||
[DataRow("date::10.10.2022", "Date")]
|
||||
@@ -114,40 +113,19 @@ public class QueryTests
|
||||
[DataRow("week num", "Week of the year (Calendar week, Week number)")]
|
||||
[DataRow("days in mo", "Days in month")]
|
||||
[DataRow("Leap y", "Leap year")]
|
||||
public void CanFindFormatResult(string query, string expectedSubtitle)
|
||||
public void FormatDateQueryTest(string input, string expectedMatchTerm)
|
||||
{
|
||||
// Setup
|
||||
var settings = new SettingsManager();
|
||||
var settings = new Settings();
|
||||
var page = new TimeDateExtensionPage(settings);
|
||||
page.UpdateSearchText(string.Empty, input);
|
||||
var resultLists = page.GetItems();
|
||||
|
||||
// Act
|
||||
var results = TimeDateCalculator.ExecuteSearch(settings, query);
|
||||
|
||||
// Assert
|
||||
var matchingResult = results.FirstOrDefault(x => x.Subtitle?.StartsWith(expectedSubtitle, StringComparison.CurrentCulture) == true);
|
||||
Assert.IsNotNull(matchingResult, $"Could not find result with subtitle starting with '{expectedSubtitle}' for query '{query}'");
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow("12:30", "Time")]
|
||||
[DataRow("10.10.2022", "Date")]
|
||||
[DataRow("u1646408119", "Date and time")]
|
||||
[DataRow("u+1646408119", "Date and time")]
|
||||
[DataRow("u-1646408119", "Date and time")]
|
||||
[DataRow("ums1646408119", "Date and time")]
|
||||
[DataRow("ums+1646408119", "Date and time")]
|
||||
[DataRow("ums-1646408119", "Date and time")]
|
||||
[DataRow("ft637820085517321977", "Date and time")]
|
||||
public void DateTimeNumberOnlyInput(string query, string expectedSubtitle)
|
||||
{
|
||||
// Setup
|
||||
var settings = new SettingsManager();
|
||||
|
||||
// Act
|
||||
var results = TimeDateCalculator.ExecuteSearch(settings, query);
|
||||
|
||||
// Assert
|
||||
var matchingResult = results.FirstOrDefault(x => x.Subtitle?.StartsWith(expectedSubtitle, StringComparison.CurrentCulture) == true);
|
||||
Assert.IsNotNull(matchingResult, $"Could not find result with subtitle starting with '{expectedSubtitle}' for query '{query}'");
|
||||
var firstItem = resultLists.FirstOrDefault();
|
||||
Assert.IsNotNull(firstItem, "No items matched the query.");
|
||||
Assert.IsTrue(
|
||||
firstItem.Title.Contains(expectedMatchTerm, System.StringComparison.OrdinalIgnoreCase) ||
|
||||
firstItem.Subtitle.Contains(expectedMatchTerm, System.StringComparison.OrdinalIgnoreCase),
|
||||
$"Expected to match '{expectedMatchTerm}' in title or subtitle but got '{firstItem.Title}' - '{firstItem.Subtitle}'");
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
@@ -157,24 +135,6 @@ public class QueryTests
|
||||
[DataRow("time:eeee")]
|
||||
[DataRow("time::eeee")]
|
||||
[DataRow("time//eeee")]
|
||||
public void InvalidInputShowsErrorResults(string query)
|
||||
{
|
||||
// Setup
|
||||
var settings = new SettingsManager();
|
||||
|
||||
// Act
|
||||
var results = TimeDateCalculator.ExecuteSearch(settings, query);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(results, $"Results should not be null for query '{query}'");
|
||||
Assert.IsTrue(results.Count > 0, $"Query '{query}' should return at least one result");
|
||||
|
||||
// For invalid input, cmdpal returns an error result
|
||||
var hasErrorResult = results.Any(r => r.Title?.StartsWith("Error: Invalid input", StringComparison.CurrentCulture) == true);
|
||||
Assert.IsTrue(hasErrorResult, $"Query '{query}' should return an error result for invalid input");
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow("ug1646408119")] // Invalid prefix
|
||||
[DataRow("u9999999999999")] // Unix number + prefix is longer than 12 characters
|
||||
[DataRow("ums999999999999999")] // Unix number in milliseconds + prefix is longer than 17 characters
|
||||
@@ -194,116 +154,33 @@ public class QueryTests
|
||||
[DataRow("10.aa.22")]
|
||||
[DataRow("12::55")]
|
||||
[DataRow("12:aa:55")]
|
||||
public void InvalidNumberInputShowsErrorMessage(string query)
|
||||
public void InvalidInputShowsErrorResults(string query)
|
||||
{
|
||||
// Setup
|
||||
var settings = new SettingsManager();
|
||||
|
||||
// Act
|
||||
var results = TimeDateCalculator.ExecuteSearch(settings, query);
|
||||
var settings = new Settings();
|
||||
var page = new TimeDateExtensionPage(settings);
|
||||
page.UpdateSearchText(string.Empty, query);
|
||||
var results = page.GetItems();
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(results, $"Results should not be null for query '{query}'");
|
||||
Assert.IsTrue(results.Count > 0, $"Should return at least one result (error message) for invalid query '{query}'");
|
||||
Assert.IsTrue(results.Length > 0, $"Query '{query}' should return at least one result");
|
||||
|
||||
// Check if we get an error result
|
||||
var errorResult = results.FirstOrDefault(r => r.Title?.StartsWith("Error: Invalid input", StringComparison.CurrentCulture) == true);
|
||||
Assert.IsNotNull(errorResult, $"Should return an error result for invalid query '{query}'");
|
||||
var firstItem = results.FirstOrDefault();
|
||||
Assert.IsTrue(firstItem.Title.StartsWith("Error: Invalid input", StringComparison.CurrentCulture), $"Query '{query}' should return an error result for invalid input");
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow("10.10aa")] // Input contains <Number>.<Number> (Can be part of a date.)
|
||||
[DataRow("10:10aa")] // Input contains <Number>:<Number> (Can be part of a time.)
|
||||
[DataRow("10/10aa")] // Input contains <Number>/<Number> (Can be part of a date.)
|
||||
public void InvalidInputNotShowsErrorMessage(string query)
|
||||
[DataRow("")]
|
||||
[DataRow(null)]
|
||||
public void EmptyQueryReturnsAllResults(string input)
|
||||
{
|
||||
// Setup
|
||||
var settings = new SettingsManager();
|
||||
|
||||
// Act
|
||||
var results = TimeDateCalculator.ExecuteSearch(settings, query);
|
||||
var settings = new Settings();
|
||||
var page = new TimeDateExtensionPage(settings);
|
||||
page.UpdateSearchText("abc", input);
|
||||
var results = page.GetItems();
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(results, $"Results should not be null for query '{query}'");
|
||||
|
||||
// These queries are ambiguous and cmdpal returns an error for them
|
||||
// This test might need to be adjusted based on actual cmdpal behavior
|
||||
if (results.Count > 0)
|
||||
{
|
||||
var hasErrorResult = results.Any(r => r.Title?.StartsWith("Error: Invalid input", StringComparison.CurrentCulture) == true);
|
||||
|
||||
// For these ambiguous inputs, cmdpal may return error results, which is acceptable
|
||||
// We just verify that the system handles them gracefully (doesn't crash)
|
||||
Assert.IsTrue(true, $"Query '{query}' handled gracefully");
|
||||
}
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow("time", "time", true)] // Full word match should work
|
||||
[DataRow("date", "date", true)] // Full word match should work
|
||||
[DataRow("now", "now", true)] // Full word match should work
|
||||
[DataRow("year", "year", true)] // Full word match should work
|
||||
[DataRow("abcdefg", "", false)] // Invalid query should return error
|
||||
public void ValidateBehaviorOnSearchQueries(string query, string expectedMatchTerm, bool shouldHaveValidResults)
|
||||
{
|
||||
// Setup
|
||||
var settings = new SettingsManager();
|
||||
|
||||
// Act
|
||||
var results = TimeDateCalculator.ExecuteSearch(settings, query);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(results, $"Results should not be null for query '{query}'");
|
||||
Assert.IsTrue(results.Count > 0, $"Query '{query}' should return at least one result");
|
||||
|
||||
if (shouldHaveValidResults)
|
||||
{
|
||||
// Should have non-error results
|
||||
var hasValidResult = results.Any(r => !r.Title?.StartsWith("Error: Invalid input", StringComparison.CurrentCulture) == true);
|
||||
Assert.IsTrue(hasValidResult, $"Query '{query}' should return valid (non-error) results");
|
||||
|
||||
if (!string.IsNullOrEmpty(expectedMatchTerm))
|
||||
{
|
||||
var hasMatchingResult = results.Any(r =>
|
||||
r.Title?.Contains(expectedMatchTerm, StringComparison.CurrentCultureIgnoreCase) == true ||
|
||||
r.Subtitle?.Contains(expectedMatchTerm, StringComparison.CurrentCultureIgnoreCase) == true);
|
||||
Assert.IsTrue(hasMatchingResult, $"Query '{query}' should return results containing '{expectedMatchTerm}'");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Should have error results
|
||||
var hasErrorResult = results.Any(r => r.Title?.StartsWith("Error: Invalid input", StringComparison.CurrentCulture) == true);
|
||||
Assert.IsTrue(hasErrorResult, $"Query '{query}' should return error results for invalid input");
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void EmptyQueryReturnsAllResults()
|
||||
{
|
||||
// Setup
|
||||
var settings = new SettingsManager();
|
||||
|
||||
// Act
|
||||
var results = TimeDateCalculator.ExecuteSearch(settings, string.Empty);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(results);
|
||||
Assert.IsTrue(results.Count > 0, "Empty query should return all available results");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void NullQueryReturnsAllResults()
|
||||
{
|
||||
// Setup
|
||||
var settings = new SettingsManager();
|
||||
|
||||
// Act
|
||||
var results = TimeDateCalculator.ExecuteSearch(settings, null);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(results);
|
||||
Assert.IsTrue(results.Count > 0, "Null query should return all available results");
|
||||
Assert.IsTrue(results.Length > 0, $"Empty query should return results");
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
@@ -312,39 +189,34 @@ public class QueryTests
|
||||
[DataRow("iso utc", "ISO 8601 UTC")]
|
||||
[DataRow("iso zone", "ISO 8601 with time zone")]
|
||||
[DataRow("iso utc zone", "ISO 8601 UTC with time zone")]
|
||||
public void UTCRelatedQueries(string query, string expectedSubtitle)
|
||||
public void TimeZoneQuery(string query, string expectedSubtitle)
|
||||
{
|
||||
// Setup
|
||||
var settings = new SettingsManager();
|
||||
|
||||
// Act
|
||||
var results = TimeDateCalculator.ExecuteSearch(settings, query);
|
||||
var settings = new Settings();
|
||||
var page = new TimeDateExtensionPage(settings);
|
||||
page.UpdateSearchText(string.Empty, query);
|
||||
var resultsList = page.GetItems();
|
||||
var results = Query(query, resultsList);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(results);
|
||||
Assert.IsTrue(results.Count > 0, $"Query '{query}' should return results");
|
||||
|
||||
var matchingResult = results.FirstOrDefault(x => x.Subtitle?.StartsWith(expectedSubtitle, StringComparison.CurrentCulture) == true);
|
||||
Assert.IsNotNull(matchingResult, $"Could not find result with subtitle starting with '{expectedSubtitle}' for query '{query}'");
|
||||
var firstResult = results.FirstOrDefault();
|
||||
Assert.IsTrue(firstResult.Subtitle.StartsWith(expectedSubtitle, StringComparison.CurrentCulture), $"Could not find result with subtitle starting with '{expectedSubtitle}' for query '{query}'");
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow("time::12:30:45")]
|
||||
[DataRow("date::2023-12-25")]
|
||||
[DataRow("now::u1646408119")]
|
||||
[DataRow("current::ft637820085517321977")]
|
||||
public void DelimiterQueriesReturnResults(string query)
|
||||
[DataRow("time::12:30:45", "12:30 PM")]
|
||||
[DataRow("date::2023-12-25", "12/25/2023")]
|
||||
[DataRow("now::u1646408119", "132908817190000000")]
|
||||
public void DelimiterQueriesReturnResults(string query, string expectedResult)
|
||||
{
|
||||
// Setup
|
||||
var settings = new SettingsManager();
|
||||
|
||||
// Act
|
||||
var results = TimeDateCalculator.ExecuteSearch(settings, query);
|
||||
var settings = new Settings();
|
||||
var page = new TimeDateExtensionPage(settings);
|
||||
page.UpdateSearchText(string.Empty, query);
|
||||
var resultsList = page.GetItems();
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(results);
|
||||
|
||||
// Delimiter queries should return results even if parsing fails (error results)
|
||||
Assert.IsTrue(results.Count > 0, $"Delimiter query '{query}' should return at least one result");
|
||||
Assert.IsNotNull(resultsList);
|
||||
var firstResult = resultsList.FirstOrDefault();
|
||||
Assert.IsTrue(firstResult.Title.Contains(expectedResult, StringComparison.CurrentCulture), $"Delimiter query '{query}' result not match {expectedResult} current result {firstResult.Title}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
// 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.Generic;
|
||||
using Microsoft.CmdPal.Ext.TimeDate.Helpers;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.TimeDate.UnitTests;
|
||||
|
||||
public class Settings : ISettingsInterface
|
||||
{
|
||||
private readonly int firstWeekOfYear;
|
||||
private readonly int firstDayOfWeek;
|
||||
private readonly bool enableFallbackItems;
|
||||
private readonly bool timeWithSecond;
|
||||
private readonly bool dateWithWeekday;
|
||||
private readonly List<string> customFormats;
|
||||
|
||||
public Settings(
|
||||
int firstWeekOfYear = -1,
|
||||
int firstDayOfWeek = -1,
|
||||
bool enableFallbackItems = true,
|
||||
bool timeWithSecond = false,
|
||||
bool dateWithWeekday = false,
|
||||
List<string> customFormats = null)
|
||||
{
|
||||
this.firstWeekOfYear = firstWeekOfYear;
|
||||
this.firstDayOfWeek = firstDayOfWeek;
|
||||
this.enableFallbackItems = enableFallbackItems;
|
||||
this.timeWithSecond = timeWithSecond;
|
||||
this.dateWithWeekday = dateWithWeekday;
|
||||
this.customFormats = customFormats ?? new List<string>();
|
||||
}
|
||||
|
||||
public int FirstWeekOfYear => firstWeekOfYear;
|
||||
|
||||
public int FirstDayOfWeek => firstDayOfWeek;
|
||||
|
||||
public bool EnableFallbackItems => enableFallbackItems;
|
||||
|
||||
public bool TimeWithSecond => timeWithSecond;
|
||||
|
||||
public bool DateWithWeekday => dateWithWeekday;
|
||||
|
||||
public List<string> CustomFormats => customFormats;
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
// 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.Globalization;
|
||||
using Microsoft.CmdPal.Ext.TimeDate.Helpers;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.TimeDate.UnitTests;
|
||||
|
||||
[TestClass]
|
||||
public class SettingsManagerTests
|
||||
{
|
||||
private CultureInfo originalCulture;
|
||||
private CultureInfo originalUiCulture;
|
||||
|
||||
[TestInitialize]
|
||||
public void Setup()
|
||||
{
|
||||
// Set culture to 'en-us'
|
||||
originalCulture = CultureInfo.CurrentCulture;
|
||||
CultureInfo.CurrentCulture = new CultureInfo("en-us", false);
|
||||
originalUiCulture = CultureInfo.CurrentUICulture;
|
||||
CultureInfo.CurrentUICulture = new CultureInfo("en-us", false);
|
||||
}
|
||||
|
||||
[TestCleanup]
|
||||
public void Cleanup()
|
||||
{
|
||||
// Restore original culture
|
||||
CultureInfo.CurrentCulture = originalCulture;
|
||||
CultureInfo.CurrentUICulture = originalUiCulture;
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SettingsManagerInitializationTest()
|
||||
{
|
||||
// Act
|
||||
var settingsManager = new SettingsManager();
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(settingsManager);
|
||||
Assert.IsNotNull(settingsManager.Settings);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void DefaultSettingsValidation()
|
||||
{
|
||||
// Act
|
||||
var settingsManager = new SettingsManager();
|
||||
|
||||
// Assert - Check that properties are accessible
|
||||
var enableFallback = settingsManager.EnableFallbackItems;
|
||||
var timeWithSecond = settingsManager.TimeWithSecond;
|
||||
var dateWithWeekday = settingsManager.DateWithWeekday;
|
||||
var firstWeekOfYear = settingsManager.FirstWeekOfYear;
|
||||
var firstDayOfWeek = settingsManager.FirstDayOfWeek;
|
||||
var customFormats = settingsManager.CustomFormats;
|
||||
|
||||
Assert.IsNotNull(customFormats);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SettingsPropertiesAccessibilityTest()
|
||||
{
|
||||
// Setup
|
||||
var settingsManager = new SettingsManager();
|
||||
|
||||
// Act & Assert - Verify all properties are accessible without exception
|
||||
try
|
||||
{
|
||||
_ = settingsManager.EnableFallbackItems;
|
||||
_ = settingsManager.TimeWithSecond;
|
||||
_ = settingsManager.DateWithWeekday;
|
||||
_ = settingsManager.FirstWeekOfYear;
|
||||
_ = settingsManager.FirstDayOfWeek;
|
||||
_ = settingsManager.CustomFormats;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Assert.Fail($"Settings properties should be accessible: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.UnitTestBase;
|
||||
|
||||
public class CommandPaletteUnitTestBase
|
||||
{
|
||||
private bool MatchesFilter(string filter, IListItem item) => StringMatcher.FuzzySearch(filter, item.Title).Success || StringMatcher.FuzzySearch(filter, item.Subtitle).Success;
|
||||
|
||||
public IListItem[] Query(string query, IListItem[] candidates)
|
||||
{
|
||||
IListItem[] listItems = candidates
|
||||
.Where(item => MatchesFilter(query, item))
|
||||
.ToArray();
|
||||
|
||||
return listItems;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
<IsPackable>false</IsPackable>
|
||||
<RootNamespace>Microsoft.CmdPal.Ext.UnitTestsBase</RootNamespace>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Moq" />
|
||||
<PackageReference Include="MSTest" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\extensionsdk\Microsoft.CommandPalette.Extensions.Toolkit\Microsoft.CommandPalette.Extensions.Toolkit.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user