mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-01-14 16:26:50 +01:00
Compare commits
7 Commits
dev/migrie
...
leilzh/tim
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e25722300 | ||
|
|
617c6e00f2 | ||
|
|
6d29c3a2c9 | ||
|
|
608eb1e034 | ||
|
|
f341aeb627 | ||
|
|
ee764d5f56 | ||
|
|
40a1729462 |
4
.github/actions/spell-check/expect.txt
vendored
4
.github/actions/spell-check/expect.txt
vendored
@@ -76,6 +76,7 @@ ARPINSTALLLOCATION
|
||||
ARPPRODUCTICON
|
||||
ARRAYSIZE
|
||||
ARROWKEYS
|
||||
ARTIFACTSTAGINGDIRECTORY
|
||||
asf
|
||||
Ashcraft
|
||||
AShortcut
|
||||
@@ -1722,6 +1723,8 @@ UFlags
|
||||
UHash
|
||||
UIA
|
||||
UIEx
|
||||
uild
|
||||
uitests
|
||||
ULONGLONG
|
||||
ums
|
||||
uncompilable
|
||||
@@ -1748,6 +1751,7 @@ Uptool
|
||||
urld
|
||||
Usb
|
||||
USEDEFAULT
|
||||
USEINSTALLERFORTEST
|
||||
USEFILEATTRIBUTES
|
||||
USESHOWWINDOW
|
||||
USESTDHANDLES
|
||||
|
||||
46
.pipelines/installPowertoys.ps1
Normal file
46
.pipelines/installPowertoys.ps1
Normal file
@@ -0,0 +1,46 @@
|
||||
param(
|
||||
[Parameter()]
|
||||
[ValidateSet("Machine", "PerUser")]
|
||||
[string]$InstallMode = "Machine"
|
||||
)
|
||||
|
||||
$ProgressPreference = 'SilentlyContinue'
|
||||
|
||||
# Get artifact path
|
||||
$ArtifactPath = $ENV:BUILD_ARTIFACTSTAGINGDIRECTORY
|
||||
if (-not $ArtifactPath) {
|
||||
throw "BUILD_ARTIFACTSTAGINGDIRECTORY environment variable not set"
|
||||
}
|
||||
|
||||
# Since we only download PowerToysSetup-*.exe files, we can directly find it
|
||||
$Installer = Get-ChildItem -Path $ArtifactPath -Filter 'PowerToys*.exe' | Select-Object -First 1
|
||||
|
||||
if (-not $Installer) {
|
||||
throw "PowerToys installer not found"
|
||||
}
|
||||
|
||||
Write-Host "Installing PowerToys: $($Installer.Name)"
|
||||
|
||||
# Install PowerToys
|
||||
$Process = Start-Process -Wait -FilePath $Installer.FullName -ArgumentList "/passive", "/norestart" -PassThru -NoNewWindow
|
||||
|
||||
if ($Process.ExitCode -eq 0 -or $Process.ExitCode -eq 3010) {
|
||||
Write-Host "✅ PowerToys installation completed successfully"
|
||||
} else {
|
||||
throw "PowerToys installation failed with exit code: $($Process.ExitCode)"
|
||||
}
|
||||
|
||||
# Verify installation
|
||||
if ($InstallMode -eq "PerUser") {
|
||||
if (Test-Path "${env:LOCALAPPDATA}\PowerToys\PowerToys.exe") {
|
||||
Write-Host "✅ PowerToys verified at: ${env:LOCALAPPDATA}\PowerToys\PowerToys.exe"
|
||||
} else {
|
||||
throw "PowerToys installation verification failed"
|
||||
}
|
||||
} else {
|
||||
if (Test-Path "${env:ProgramFiles}\PowerToys\PowerToys.exe") {
|
||||
Write-Host "✅ PowerToys verified at: ${env:ProgramFiles}\PowerToys\PowerToys.exe"
|
||||
} else {
|
||||
throw "PowerToys installation verification failed"
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,7 @@ stages:
|
||||
enableMsBuildCaching: ${{ parameters.enableMsBuildCaching }}
|
||||
runTests: true
|
||||
useVSPreview: ${{ parameters.useVSPreview }}
|
||||
timeoutInMinutes: 90
|
||||
|
||||
- stage: OneFuzz
|
||||
displayName: Fuzz ${{ parameters.platform }}
|
||||
|
||||
@@ -81,6 +81,12 @@ parameters:
|
||||
- 'src/modules/previewpane/SvgPreviewHandler/SvgPreviewHandler.csproj'
|
||||
- 'src/modules/previewpane/SvgThumbnailProvider/SvgThumbnailProvider.csproj'
|
||||
- 'src/modules/FileLocksmith/FileLocksmithUI/FileLocksmithUI.csproj'
|
||||
- name: timeoutInMinutes
|
||||
type: number
|
||||
default: 240
|
||||
- name: cancelTimeoutInMinutes
|
||||
type: number
|
||||
default: 1
|
||||
|
||||
jobs:
|
||||
- job: ${{ parameters.jobName }}
|
||||
@@ -123,8 +129,8 @@ jobs:
|
||||
${{ else }}:
|
||||
RestoreAdditionalProjectSourcesArg: ''
|
||||
displayName: Build
|
||||
timeoutInMinutes: 240
|
||||
cancelTimeoutInMinutes: 1
|
||||
timeoutInMinutes: ${{ parameters.timeoutInMinutes }}
|
||||
cancelTimeoutInMinutes: ${{ parameters.cancelTimeoutInMinutes }}
|
||||
templateContext: # Required when this template is hosted in 1ES PT
|
||||
outputs:
|
||||
- output: pipelineArtifact
|
||||
|
||||
132
.pipelines/v2/templates/job-build-ui-tests.yml
Normal file
132
.pipelines/v2/templates/job-build-ui-tests.yml
Normal file
@@ -0,0 +1,132 @@
|
||||
# Minimal UI Tests Build Template
|
||||
# This template only builds UI test projects and stages their test DLLs for consumption by test pipelines
|
||||
|
||||
parameters:
|
||||
- name: buildConfigurations
|
||||
type: object
|
||||
default:
|
||||
- Release
|
||||
- name: buildPlatforms
|
||||
type: object
|
||||
default:
|
||||
- x64
|
||||
- name: condition
|
||||
type: string
|
||||
default: ''
|
||||
- name: dependsOn
|
||||
type: object
|
||||
default: []
|
||||
- name: pool
|
||||
type: object
|
||||
default: []
|
||||
- name: variables
|
||||
type: object
|
||||
default: {}
|
||||
- name: uiTestModules
|
||||
type: object
|
||||
default: []
|
||||
|
||||
jobs:
|
||||
- job: BuildUITests
|
||||
${{ if ne(length(parameters.pool), 0) }}:
|
||||
pool: ${{ parameters.pool }}
|
||||
dependsOn: ${{ parameters.dependsOn }}
|
||||
condition: ${{ parameters.condition }}
|
||||
strategy:
|
||||
matrix:
|
||||
${{ each config in parameters.buildConfigurations }}:
|
||||
${{ each platform in parameters.buildPlatforms }}:
|
||||
${{ config }}_${{ platform }}:
|
||||
BuildConfiguration: ${{ config }}
|
||||
BuildPlatform: ${{ platform }}
|
||||
variables:
|
||||
JobOutputDirectory: $(Build.ArtifactStagingDirectory)
|
||||
LogOutputDirectory: $(Build.ArtifactStagingDirectory)\logs
|
||||
JobOutputArtifactName: build-$(BuildPlatform)-$(BuildConfiguration)
|
||||
NUGET_RESTORE_MSBUILD_ARGS: /p:Platform=$(BuildPlatform)
|
||||
${{ insert }}: ${{ parameters.variables }}
|
||||
displayName: Build UI Tests Only
|
||||
timeoutInMinutes: 60
|
||||
cancelTimeoutInMinutes: 1
|
||||
templateContext:
|
||||
outputs:
|
||||
- output: pipelineArtifact
|
||||
artifactName: $(JobOutputArtifactName)
|
||||
targetPath: $(Build.ArtifactStagingDirectory)
|
||||
steps:
|
||||
- checkout: self
|
||||
clean: true
|
||||
submodules: true
|
||||
persistCredentials: True
|
||||
fetchTags: false
|
||||
fetchDepth: 1
|
||||
|
||||
- template: steps-ensure-dotnet-version.yml
|
||||
parameters:
|
||||
sdk: true
|
||||
version: '9.0'
|
||||
|
||||
- template: .\steps-restore-nuget.yml
|
||||
|
||||
- task: NuGetCommand@2
|
||||
displayName: Restore solution-level NuGet packages
|
||||
inputs:
|
||||
command: restore
|
||||
feedsToUse: config
|
||||
configPath: nuget.config
|
||||
restoreSolution: PowerToys.sln
|
||||
restoreDirectory: '$(Build.SourcesDirectory)\packages'
|
||||
|
||||
# Build all UI test projects if no specific modules are specified
|
||||
- ${{ if eq(length(parameters.uiTestModules), 0) }}:
|
||||
- task: VSBuild@1
|
||||
displayName: Build UI Test Projects
|
||||
inputs:
|
||||
solution: '**/*UITest*.csproj'
|
||||
vsVersion: 17.0
|
||||
msbuildArgs: >-
|
||||
-restore
|
||||
-graph
|
||||
/p:RestorePackagesConfig=true
|
||||
/p:BuildProjectReferences=true
|
||||
/p:CIBuild=true
|
||||
/bl:$(LogOutputDirectory)\build-all-uitests.binlog
|
||||
$(NUGET_RESTORE_MSBUILD_ARGS)
|
||||
platform: $(BuildPlatform)
|
||||
configuration: $(BuildConfiguration)
|
||||
msbuildArchitecture: x64
|
||||
maximumCpuCount: true
|
||||
|
||||
# Build specific UI test modules
|
||||
- ${{ if ne(length(parameters.uiTestModules), 0) }}:
|
||||
- ${{ each module in parameters.uiTestModules }}:
|
||||
- task: VSBuild@1
|
||||
displayName: 'Build UI Test Module: ${{ module }}'
|
||||
inputs:
|
||||
solution: '**/*${{ module }}*.csproj'
|
||||
vsVersion: 17.0
|
||||
msbuildArgs: >-
|
||||
-restore
|
||||
-graph
|
||||
/p:RestorePackagesConfig=true
|
||||
/p:BuildProjectReferences=true
|
||||
/p:CIBuild=true
|
||||
/bl:$(LogOutputDirectory)\build-${{ module }}.binlog
|
||||
$(NUGET_RESTORE_MSBUILD_ARGS)
|
||||
platform: $(BuildPlatform)
|
||||
configuration: $(BuildConfiguration)
|
||||
msbuildArchitecture: x64
|
||||
maximumCpuCount: true
|
||||
|
||||
# Stage test project outputs with directory structure
|
||||
- task: CopyFiles@2
|
||||
displayName: Stage UI Test Build Outputs
|
||||
inputs:
|
||||
sourceFolder: '$(Build.SourcesDirectory)'
|
||||
contents: '$(BuildPlatform)/$(BuildConfiguration)/**/*'
|
||||
targetFolder: '$(JobOutputDirectory)\$(BuildPlatform)\$(BuildConfiguration)'
|
||||
|
||||
- publish: $(JobOutputDirectory)
|
||||
artifact: $(JobOutputArtifactName)
|
||||
displayName: Publish UI Test artifacts
|
||||
condition: always()
|
||||
@@ -11,10 +11,28 @@ parameters:
|
||||
- name: useLatestWebView2
|
||||
type: boolean
|
||||
default: false
|
||||
- name: useLatestOfficialBuild
|
||||
type: boolean
|
||||
default: true
|
||||
- name: useCurrentBranchBuild
|
||||
type: boolean
|
||||
default: false
|
||||
- name: uiTestModules
|
||||
type: object
|
||||
default: []
|
||||
- name: installMode
|
||||
type: string
|
||||
default: 'machine'
|
||||
values:
|
||||
- 'machine'
|
||||
- 'peruser'
|
||||
- name: jobSuffix
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
jobs:
|
||||
- job: Test${{ parameters.platform }}${{ parameters.configuration }}
|
||||
displayName: Test ${{ parameters.platform }} ${{ parameters.configuration }}
|
||||
- job: Test${{ parameters.platform }}${{ parameters.configuration }}${{ parameters.jobSuffix }}
|
||||
displayName: Test ${{ parameters.platform }} ${{ parameters.configuration }}${{ parameters.jobSuffix }}
|
||||
timeoutInMinutes: 300
|
||||
variables:
|
||||
${{ if or(eq(parameters.platform, 'x64Win10'), eq(parameters.platform, 'x64Win11')) }}:
|
||||
@@ -95,28 +113,80 @@ jobs:
|
||||
& '$(build.sourcesdirectory)\.pipelines\InstallWinAppDriver.ps1'
|
||||
displayName: Download and install WinAppDriver
|
||||
|
||||
- ${{ if eq(parameters.useLatestOfficialBuild, true) }}:
|
||||
- task: DownloadPipelineArtifact@2
|
||||
inputs:
|
||||
buildType: 'specific'
|
||||
project: 'Dart'
|
||||
definition: '76541'
|
||||
buildVersionToDownload: 'latestFromBranch'
|
||||
${{ if eq(parameters.useCurrentBranchBuild, true) }}:
|
||||
branchName: '$(Build.SourceBranch)'
|
||||
${{ else }}:
|
||||
branchName: 'refs/heads/main'
|
||||
artifactName: 'build-$(BuildPlatform)-Release'
|
||||
targetPath: '$(Build.ArtifactStagingDirectory)'
|
||||
${{ if eq(parameters.installMode, 'peruser') }}:
|
||||
patterns: |
|
||||
**/PowerToysUserSetup*.exe
|
||||
${{ else }}:
|
||||
patterns: |
|
||||
**/PowerToysSetup*.exe
|
||||
|
||||
- ${{ if eq(parameters.useLatestOfficialBuild, true) }}:
|
||||
- ${{ if eq(parameters.installMode, 'peruser') }}:
|
||||
- pwsh: |-
|
||||
& "$(build.sourcesdirectory)\.pipelines\installPowerToys.ps1" -InstallMode "PerUser"
|
||||
displayName: Install PowerToys (Per-User)
|
||||
|
||||
- ${{ if eq(parameters.installMode, 'machine') }}:
|
||||
- pwsh: |-
|
||||
& "$(build.sourcesdirectory)\.pipelines\installPowerToys.ps1" -InstallMode "Machine"
|
||||
displayName: Install PowerToys (Machine-Level)
|
||||
|
||||
- ${{ if ne(parameters.platform, 'arm64') }}:
|
||||
- task: ScreenResolutionUtility@1
|
||||
inputs:
|
||||
displaySettings: 'optimal'
|
||||
|
||||
- task: VSTest@3
|
||||
displayName: Run UI Tests
|
||||
inputs:
|
||||
platform: '$(BuildPlatform)'
|
||||
configuration: '$(BuildConfiguration)'
|
||||
testSelector: 'testAssemblies'
|
||||
searchFolder: '$(Pipeline.Workspace)\$(TestArtifactsName)'
|
||||
vsTestVersion: 'toolsInstaller'
|
||||
uiTests: true
|
||||
rerunFailedTests: true
|
||||
# Since UITests-FancyZonesEditor.dll is generated in both UITests-FancyZonesEditor and UITests-FancyZones, removed one to avoid duplicate test runs
|
||||
testAssemblyVer2: |
|
||||
**\*UITest*.dll
|
||||
!**\obj\**
|
||||
!**\ref\**
|
||||
!**\UITests-FancyZones\**\UITests-FancyZonesEditor.dll
|
||||
- ${{ if eq(length(parameters.uiTestModules), 0) }}:
|
||||
- task: VSTest@3
|
||||
displayName: Run UI Tests
|
||||
inputs:
|
||||
platform: '$(BuildPlatform)'
|
||||
configuration: '$(BuildConfiguration)'
|
||||
testSelector: 'testAssemblies'
|
||||
searchFolder: '$(Pipeline.Workspace)\$(TestArtifactsName)'
|
||||
vsTestVersion: 'toolsInstaller'
|
||||
uiTests: true
|
||||
rerunFailedTests: true
|
||||
# Since UITests-FancyZonesEditor.dll is generated in both UITests-FancyZonesEditor and UITests-FancyZones, removed one to avoid duplicate test runs
|
||||
testAssemblyVer2: |
|
||||
**\*UITest*.dll
|
||||
!**\obj\**
|
||||
!**\ref\**
|
||||
!**\UITests-FancyZones\**\UITests-FancyZonesEditor.dll
|
||||
env:
|
||||
platform: '$(TestPlatform)'
|
||||
useInstallerForTest: ${{ parameters.useLatestOfficialBuild }}
|
||||
|
||||
|
||||
env:
|
||||
platform: '$(TestPlatform)'
|
||||
- ${{ if ne(length(parameters.uiTestModules), 0) }}:
|
||||
- ${{ each module in parameters.uiTestModules }}:
|
||||
- task: VSTest@3
|
||||
displayName: Run UI Test - ${{ module }}
|
||||
inputs:
|
||||
platform: '$(BuildPlatform)'
|
||||
configuration: '$(BuildConfiguration)'
|
||||
testSelector: 'testAssemblies'
|
||||
searchFolder: '$(Pipeline.Workspace)\$(TestArtifactsName)'
|
||||
vsTestVersion: 'toolsInstaller'
|
||||
uiTests: true
|
||||
rerunFailedTests: true
|
||||
testAssemblyVer2: |
|
||||
**\*${{ module }}*.dll
|
||||
!**\obj\**
|
||||
!**\ref\**
|
||||
!**\UITests-FancyZones\**\UITests-FancyZonesEditor.dll
|
||||
env:
|
||||
platform: '$(TestPlatform)'
|
||||
useInstallerForTest: ${{ parameters.useLatestOfficialBuild }}
|
||||
|
||||
@@ -60,4 +60,5 @@ stages:
|
||||
useLatestWinAppSDK: ${{ parameters.useLatestWinAppSDK }}
|
||||
${{ if eq(parameters.useLatestWinAppSDK, true) }}:
|
||||
winAppSDKVersionNumber: ${{ parameters.winAppSDKVersionNumber }}
|
||||
useExperimentalVersion: ${{ parameters.useExperimentalVersion }}
|
||||
useExperimentalVersion: ${{ parameters.useExperimentalVersion }}
|
||||
timeoutInMinutes: 90
|
||||
|
||||
@@ -22,63 +22,155 @@ 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: uiTestModules
|
||||
type: object
|
||||
default: []
|
||||
|
||||
stages:
|
||||
- ${{ each platform in parameters.buildPlatforms }}:
|
||||
- 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 }}
|
||||
- ${{ 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
|
||||
|
||||
- ${{ 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
|
||||
dependsOn:
|
||||
- Build_${{platform}}
|
||||
${{ 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
|
||||
dependsOn:
|
||||
- Build_${{platform}}
|
||||
${{ 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 }}
|
||||
dependsOn:
|
||||
- Build_${{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'
|
||||
@@ -16,6 +16,56 @@
|
||||
|
||||
- Run tests in the Test Explorer (`Test > Test Explorer` or `Ctrl+E, T`).
|
||||
|
||||
## Running tests in pipeline
|
||||
|
||||
The PowerToys UI test pipeline provides flexible options for building and testing:
|
||||
|
||||
### 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.
|
||||
|
||||
- **useCurrentBranchBuild**: When checked along with `useLatestOfficialBuild`, downloads the official build from the current branch instead of main.
|
||||
|
||||
**Default value**: `false` (downloads from main branch)
|
||||
|
||||
**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
|
||||
|
||||
**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
|
||||
|
||||
- **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
|
||||
- `['MouseUtils.UITests']` - Only MouseUtils UI tests
|
||||
- `['UITests-FancyZones', 'MouseUtils.UITests']` - Multiple specific modules
|
||||
- Leave empty to build and run all UI test modules
|
||||
|
||||
**Important**: The `uiTestModules` parameter values must match both the test project names (for `.csproj` selection during build) and the test assembly names (for `.dll` execution during testing).
|
||||
|
||||
### 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
|
||||
|
||||
2. **Full Build + Testing** (`useLatestOfficialBuild = false`)
|
||||
- Builds entire PowerToys solution
|
||||
- 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
|
||||
|
||||
> **Note**: Both modes support the `uiTestModules` parameter to control which specific UI test modules to build and run.
|
||||
|
||||
### Pipeline Access
|
||||
- Pipeline: https://microsoft.visualstudio.com/Dart/_build?definitionId=161438&_a=summary
|
||||
|
||||
## How to add the first UI tests for your modules
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ namespace Microsoft.PowerToys.UITest
|
||||
|
||||
internal class ModuleConfigData
|
||||
{
|
||||
private Dictionary<PowerToysModule, string> ModulePath { get; }
|
||||
private Dictionary<PowerToysModule, ModuleInfo> ModuleInfo { get; }
|
||||
|
||||
// Singleton instance of ModuleConfigData.
|
||||
private static readonly Lazy<ModuleConfigData> SingletonInstance = new Lazy<ModuleConfigData>(() => new ModuleConfigData());
|
||||
@@ -86,37 +86,74 @@ namespace Microsoft.PowerToys.UITest
|
||||
|
||||
public const string WindowsApplicationDriverUrl = "http://127.0.0.1:4723";
|
||||
|
||||
public Dictionary<PowerToysModule, string> ModuleWindowName { get; }
|
||||
private bool UseInstallerForTest { get; }
|
||||
|
||||
private ModuleConfigData()
|
||||
{
|
||||
// The exe window name for each module.
|
||||
ModuleWindowName = new Dictionary<PowerToysModule, string>
|
||||
{
|
||||
[PowerToysModule.PowerToysSettings] = "PowerToys Settings",
|
||||
[PowerToysModule.FancyZone] = "FancyZones Layout",
|
||||
[PowerToysModule.Hosts] = "Hosts File Editor",
|
||||
[PowerToysModule.Runner] = "PowerToys",
|
||||
[PowerToysModule.Workspaces] = "Workspaces Editor",
|
||||
[PowerToysModule.PowerRename] = "PowerRename",
|
||||
};
|
||||
// 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;
|
||||
|
||||
// Exe start path for the module if it exists.
|
||||
ModulePath = new Dictionary<PowerToysModule, string>
|
||||
// Module information including executable name, window name, and optional subdirectory
|
||||
ModuleInfo = new Dictionary<PowerToysModule, ModuleInfo>
|
||||
{
|
||||
[PowerToysModule.PowerToysSettings] = @"\..\..\..\WinUI3Apps\PowerToys.Settings.exe",
|
||||
[PowerToysModule.FancyZone] = @"\..\..\..\PowerToys.FancyZonesEditor.exe",
|
||||
[PowerToysModule.Hosts] = @"\..\..\..\WinUI3Apps\PowerToys.Hosts.exe",
|
||||
[PowerToysModule.Runner] = @"\..\..\..\PowerToys.exe",
|
||||
[PowerToysModule.Workspaces] = @"\..\..\..\PowerToys.WorkspacesEditor.exe",
|
||||
[PowerToysModule.PowerRename] = @"\..\..\..\WinUI3Apps\PowerToys.PowerRename.exe",
|
||||
[PowerToysModule.PowerToysSettings] = new ModuleInfo("PowerToys.Settings.exe", "PowerToys Settings", "WinUI3Apps"),
|
||||
[PowerToysModule.FancyZone] = new ModuleInfo("PowerToys.FancyZonesEditor.exe", "FancyZones Layout"),
|
||||
[PowerToysModule.Hosts] = new ModuleInfo("PowerToys.Hosts.exe", "Hosts File Editor", "WinUI3Apps"),
|
||||
[PowerToysModule.Runner] = new ModuleInfo("PowerToys.exe", "PowerToys"),
|
||||
[PowerToysModule.Workspaces] = new ModuleInfo("PowerToys.WorkspacesEditor.exe", "Workspaces Editor"),
|
||||
[PowerToysModule.PowerRename] = new ModuleInfo("PowerToys.PowerRename.exe", "PowerRename", "WinUI3Apps"),
|
||||
};
|
||||
}
|
||||
|
||||
public string GetModulePath(PowerToysModule scope) => ModulePath[scope];
|
||||
private string GetPowerToysInstallPath()
|
||||
{
|
||||
// Try common installation paths
|
||||
string[] possiblePaths =
|
||||
{
|
||||
@"C:\Program Files\PowerToys",
|
||||
@"C:\Program Files (x86)\PowerToys",
|
||||
Environment.ExpandEnvironmentVariables(@"%LocalAppData%\PowerToys"),
|
||||
Environment.ExpandEnvironmentVariables(@"%ProgramFiles%\PowerToys"),
|
||||
};
|
||||
|
||||
foreach (string path in possiblePaths)
|
||||
{
|
||||
if (Directory.Exists(path) && File.Exists(Path.Combine(path, "PowerToys.exe")))
|
||||
{
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to Program Files if not found
|
||||
return @"C:\Program Files\PowerToys";
|
||||
}
|
||||
|
||||
public string GetModulePath(PowerToysModule scope)
|
||||
{
|
||||
var moduleInfo = ModuleInfo[scope];
|
||||
|
||||
if (UseInstallerForTest)
|
||||
{
|
||||
string powerToysInstallPath = GetPowerToysInstallPath();
|
||||
string installedPath = moduleInfo.GetInstalledPath(powerToysInstallPath);
|
||||
|
||||
if (File.Exists(installedPath))
|
||||
{
|
||||
return installedPath;
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"Warning: Installed module not found at {installedPath}, using development path");
|
||||
}
|
||||
}
|
||||
|
||||
return moduleInfo.GetDevelopmentPath();
|
||||
}
|
||||
|
||||
public string GetWindowsApplicationDriverUrl() => WindowsApplicationDriverUrl;
|
||||
|
||||
public string GetModuleWindowName(PowerToysModule scope) => ModuleWindowName[scope];
|
||||
public string GetModuleWindowName(PowerToysModule scope) => ModuleInfo[scope].WindowName;
|
||||
}
|
||||
}
|
||||
|
||||
54
src/common/UITestAutomation/ModuleInfo.cs
Normal file
54
src/common/UITestAutomation/ModuleInfo.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.PowerToys.UITest
|
||||
{
|
||||
internal class ModuleInfo
|
||||
{
|
||||
public string ExecutableName { get; }
|
||||
|
||||
public string? SubDirectory { get; }
|
||||
|
||||
public string WindowName { get; }
|
||||
|
||||
public ModuleInfo(string executableName, string windowName, string? subDirectory = null)
|
||||
{
|
||||
ExecutableName = executableName;
|
||||
WindowName = windowName;
|
||||
SubDirectory = subDirectory;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the relative development path for this module
|
||||
/// </summary>
|
||||
public string GetDevelopmentPath()
|
||||
{
|
||||
if (string.IsNullOrEmpty(SubDirectory))
|
||||
{
|
||||
return $@"\..\..\..\{ExecutableName}";
|
||||
}
|
||||
|
||||
return $@"\..\..\..\{SubDirectory}\{ExecutableName}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the installed path for this module based on the PowerToys install directory
|
||||
/// </summary>
|
||||
public string GetInstalledPath(string powerToysInstallPath)
|
||||
{
|
||||
if (string.IsNullOrEmpty(SubDirectory))
|
||||
{
|
||||
return Path.Combine(powerToysInstallPath, ExecutableName);
|
||||
}
|
||||
|
||||
return Path.Combine(powerToysInstallPath, SubDirectory, ExecutableName);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,13 +37,18 @@ namespace Microsoft.PowerToys.UITest
|
||||
private PowerToysModule scope;
|
||||
private string[]? commandLineArgs;
|
||||
|
||||
private bool UseInstallerForTest { get; }
|
||||
|
||||
[UnconditionalSuppressMessage("SingleFile", "IL3000:Avoid accessing Assembly file path when publishing as a single file", Justification = "<Pending>")]
|
||||
public SessionHelper(PowerToysModule scope, string[]? commandLineArgs = null)
|
||||
{
|
||||
this.scope = scope;
|
||||
this.commandLineArgs = commandLineArgs;
|
||||
this.sessionPath = ModuleConfigData.Instance.GetModulePath(scope);
|
||||
this.locationPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
|
||||
string? useInstallerForTestEnv =
|
||||
Environment.GetEnvironmentVariable("useInstallerForTest") ?? Environment.GetEnvironmentVariable("USEINSTALLERFORTEST");
|
||||
UseInstallerForTest = !string.IsNullOrEmpty(useInstallerForTestEnv) && bool.TryParse(useInstallerForTestEnv, out bool result) && result;
|
||||
this.locationPath = UseInstallerForTest ? string.Empty : Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
|
||||
|
||||
CheckWinAppDriverAndRoot();
|
||||
}
|
||||
@@ -156,7 +161,7 @@ namespace Microsoft.PowerToys.UITest
|
||||
|
||||
if (root != null)
|
||||
{
|
||||
const int maxRetries = 3;
|
||||
const int maxRetries = 5;
|
||||
const int delayMs = 5000;
|
||||
var windowName = "PowerToys Settings";
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<RunVSTest>false</RunVSTest>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\tests\Hosts.UITests\</OutputPath>
|
||||
<OutputPath>..\..\..\..\$(Platform)\$(Configuration)\tests\Hosts.UITests\</OutputPath>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Baseline\HostModuleTests_TestAddingEntry_arm64.png" />
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
|
||||
public record BeginInvokeMessage;
|
||||
@@ -0,0 +1,7 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
|
||||
public record CmdPalInvokeResultMessage(Microsoft.CommandPalette.Extensions.CommandResultKind Kind);
|
||||
@@ -0,0 +1,10 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
|
||||
public record GoBackMessage(bool WithAnimation = true, bool FocusSearch = true)
|
||||
{
|
||||
// TODO! sticking these properties here feels like leaking the UI into the models
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
|
||||
public record GoHomeMessage()
|
||||
// TODO! sticking these properties here feels like leaking the UI into the models
|
||||
public record GoHomeMessage(bool WithAnimation = true, bool FocusSearch = true)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
|
||||
public record NavigateToPageMessage(PageViewModel Page, bool WithAnimation)
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
|
||||
public record ShowConfirmationMessage(Microsoft.CommandPalette.Extensions.IConfirmationArgs? Args)
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
|
||||
public record ShowToastMessage(string Message)
|
||||
{
|
||||
}
|
||||
@@ -18,8 +18,14 @@ using WinRT;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public partial class ShellViewModel(IServiceProvider _serviceProvider, TaskScheduler _scheduler) : ObservableObject
|
||||
public partial class ShellViewModel : ObservableObject,
|
||||
IRecipient<PerformCommandMessage>
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly TaskScheduler _scheduler;
|
||||
private readonly Lock _invokeLock = new();
|
||||
private Task? _handleInvokeTask;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsLoaded { get; set; } = false;
|
||||
|
||||
@@ -29,7 +35,7 @@ public partial class ShellViewModel(IServiceProvider _serviceProvider, TaskSched
|
||||
[ObservableProperty]
|
||||
public partial bool IsDetailsVisible { get; set; }
|
||||
|
||||
private PageViewModel _currentPage = new LoadingPageViewModel(null, _scheduler);
|
||||
private PageViewModel _currentPage;
|
||||
|
||||
public PageViewModel CurrentPage
|
||||
{
|
||||
@@ -57,6 +63,19 @@ public partial class ShellViewModel(IServiceProvider _serviceProvider, TaskSched
|
||||
private MainListPage? _mainListPage;
|
||||
|
||||
private IExtensionWrapper? _activeExtension;
|
||||
private bool _isNested;
|
||||
|
||||
public bool IsNested { get => _isNested; }
|
||||
|
||||
public ShellViewModel(IServiceProvider serviceProvider, TaskScheduler scheduler)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
_scheduler = scheduler;
|
||||
_currentPage = new LoadingPageViewModel(null, _scheduler);
|
||||
|
||||
// Register to receive messages
|
||||
WeakReferenceMessenger.Default.Register<PerformCommandMessage>(this);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public async Task<bool> LoadAsync()
|
||||
@@ -164,6 +183,241 @@ public partial class ShellViewModel(IServiceProvider _serviceProvider, TaskSched
|
||||
}
|
||||
}
|
||||
|
||||
public void Receive(PerformCommandMessage message)
|
||||
{
|
||||
PerformCommand(message);
|
||||
}
|
||||
|
||||
private void PerformCommand(PerformCommandMessage message)
|
||||
{
|
||||
var command = message.Command.Unsafe;
|
||||
if (command == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!CurrentPage.IsNested)
|
||||
{
|
||||
// on the main page here
|
||||
PerformTopLevelCommand(message);
|
||||
}
|
||||
|
||||
IExtensionWrapper? extension = null;
|
||||
|
||||
try
|
||||
{
|
||||
// In the case that we're coming from a top-level command, the
|
||||
// current page's host is the global instance. We only really want
|
||||
// to use that as the host of last resort.
|
||||
var pageHost = CurrentPage?.ExtensionHost;
|
||||
if (pageHost == CommandPaletteHost.Instance)
|
||||
{
|
||||
pageHost = null;
|
||||
}
|
||||
|
||||
var messageHost = message.ExtensionHost;
|
||||
|
||||
// Use the host from the current page if it has one, else use the
|
||||
// one specified in the PerformMessage for a top-level command,
|
||||
// else just use the global one.
|
||||
CommandPaletteHost host;
|
||||
|
||||
// Top level items can come through without a Extension set on the
|
||||
// message. In that case, the `Context` is actually the
|
||||
// TopLevelViewModel itself, and we can use that to get at the
|
||||
// extension object.
|
||||
extension = pageHost?.Extension ?? messageHost?.Extension ?? null;
|
||||
if (extension == null && message.Context is TopLevelViewModel topLevelViewModel)
|
||||
{
|
||||
extension = topLevelViewModel.ExtensionHost?.Extension;
|
||||
host = pageHost ?? messageHost ?? topLevelViewModel?.ExtensionHost ?? CommandPaletteHost.Instance;
|
||||
}
|
||||
else
|
||||
{
|
||||
host = pageHost ?? messageHost ?? CommandPaletteHost.Instance;
|
||||
}
|
||||
|
||||
if (extension != null)
|
||||
{
|
||||
if (messageHost != null)
|
||||
{
|
||||
Logger.LogDebug($"Activated top-level command from {extension.ExtensionDisplayName}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogDebug($"Activated command from {extension.ExtensionDisplayName}");
|
||||
}
|
||||
}
|
||||
|
||||
SetActiveExtension(extension);
|
||||
|
||||
if (command is IPage page)
|
||||
{
|
||||
Logger.LogDebug($"Navigating to page");
|
||||
|
||||
var isMainPage = command is MainListPage;
|
||||
|
||||
// Construct our ViewModel of the appropriate type and pass it the UI Thread context.
|
||||
var pageViewModel = GetViewModelForPage(page, !isMainPage, host);
|
||||
if (pageViewModel == null)
|
||||
{
|
||||
Logger.LogError($"Failed to create ViewModel for page {page.GetType().Name}");
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
// Kick off async loading of our ViewModel
|
||||
LoadPageViewModel(pageViewModel);
|
||||
_isNested = !isMainPage;
|
||||
|
||||
OnUIThread(() => { WeakReferenceMessenger.Default.Send<UpdateCommandBarMessage>(new(null)); });
|
||||
WeakReferenceMessenger.Default.Send<NavigateToPageMessage>(new(pageViewModel, message.WithAnimation));
|
||||
|
||||
// Note: Originally we set our page back in the ViewModel here, but that now happens in response to the Frame navigating triggered from the above
|
||||
// See RootFrame_Navigated event handler.
|
||||
}
|
||||
else if (command is IInvokableCommand invokable)
|
||||
{
|
||||
Logger.LogDebug($"Invoking command");
|
||||
|
||||
WeakReferenceMessenger.Default.Send<BeginInvokeMessage>();
|
||||
StartInvoke(message, invokable);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// TODO: It would be better to do this as a page exception, rather
|
||||
// than a silent log message.
|
||||
CommandPaletteHost.Instance.Log(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private void StartInvoke(PerformCommandMessage message, IInvokableCommand invokable)
|
||||
{
|
||||
// TODO GH #525 This needs more better locking.
|
||||
lock (_invokeLock)
|
||||
{
|
||||
if (_handleInvokeTask != null)
|
||||
{
|
||||
// do nothing - a command is already doing a thing
|
||||
}
|
||||
else
|
||||
{
|
||||
_handleInvokeTask = Task.Run(() =>
|
||||
{
|
||||
SafeHandleInvokeCommandSynchronous(message, invokable);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SafeHandleInvokeCommandSynchronous(PerformCommandMessage message, IInvokableCommand invokable)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Call out to extension process.
|
||||
// * May fail!
|
||||
// * May never return!
|
||||
var result = invokable.Invoke(message.Context);
|
||||
|
||||
// But if it did succeed, we need to handle the result.
|
||||
UnsafeHandleCommandResult(result);
|
||||
|
||||
_handleInvokeTask = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_handleInvokeTask = null;
|
||||
|
||||
// TODO: It would be better to do this as a page exception, rather
|
||||
// than a silent log message.
|
||||
CommandPaletteHost.Instance.Log(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private void UnsafeHandleCommandResult(ICommandResult? result)
|
||||
{
|
||||
if (result == null)
|
||||
{
|
||||
// No result, nothing to do.
|
||||
return;
|
||||
}
|
||||
|
||||
var kind = result.Kind;
|
||||
Logger.LogDebug($"handling {kind.ToString()}");
|
||||
|
||||
WeakReferenceMessenger.Default.Send<CmdPalInvokeResultMessage>(new(kind));
|
||||
switch (kind)
|
||||
{
|
||||
case CommandResultKind.Dismiss:
|
||||
{
|
||||
// Reset the palette to the main page and dismiss
|
||||
GoHome(withAnimation: false, focusSearch: false);
|
||||
WeakReferenceMessenger.Default.Send<DismissMessage>();
|
||||
break;
|
||||
}
|
||||
|
||||
case CommandResultKind.GoHome:
|
||||
{
|
||||
// Go back to the main page, but keep it open
|
||||
GoHome();
|
||||
break;
|
||||
}
|
||||
|
||||
case CommandResultKind.GoBack:
|
||||
{
|
||||
GoBack();
|
||||
break;
|
||||
}
|
||||
|
||||
case CommandResultKind.Hide:
|
||||
{
|
||||
// Keep this page open, but hide the palette.
|
||||
WeakReferenceMessenger.Default.Send<DismissMessage>();
|
||||
break;
|
||||
}
|
||||
|
||||
case CommandResultKind.KeepOpen:
|
||||
{
|
||||
// Do nothing.
|
||||
break;
|
||||
}
|
||||
|
||||
case CommandResultKind.Confirm:
|
||||
{
|
||||
if (result.Args is IConfirmationArgs a)
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<ShowConfirmationMessage>(new(a));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case CommandResultKind.ShowToast:
|
||||
{
|
||||
if (result.Args is IToastArgs a)
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<ShowToastMessage>(new(a.Message));
|
||||
UnsafeHandleCommandResult(a.Result);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private PageViewModel? GetViewModelForPage(IPage page, bool nested, CommandPaletteHost host)
|
||||
{
|
||||
return page switch
|
||||
{
|
||||
IListPage listPage => new ListViewModel(listPage, _scheduler, host)
|
||||
{
|
||||
IsNested = nested,
|
||||
},
|
||||
IContentPage contentPage => new ContentPageViewModel(contentPage, _scheduler, host),
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
public void SetActiveExtension(IExtensionWrapper? extension)
|
||||
{
|
||||
if (extension != _activeExtension)
|
||||
@@ -196,9 +450,15 @@ public partial class ShellViewModel(IServiceProvider _serviceProvider, TaskSched
|
||||
}
|
||||
}
|
||||
|
||||
public void GoHome()
|
||||
public void GoHome(bool withAnimation = true, bool focusSearch = true)
|
||||
{
|
||||
SetActiveExtension(null);
|
||||
WeakReferenceMessenger.Default.Send<GoHomeMessage>(new(withAnimation, focusSearch));
|
||||
}
|
||||
|
||||
public void GoBack(bool withAnimation = true, bool focusSearch = true)
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<GoBackMessage>(new(withAnimation, focusSearch));
|
||||
}
|
||||
|
||||
// You may ask yourself, why aren't we using CsWin32 for this?
|
||||
@@ -214,4 +474,13 @@ public partial class ShellViewModel(IServiceProvider _serviceProvider, TaskSched
|
||||
[SupportedOSPlatform("windows5.0")]
|
||||
internal static extern unsafe global::Windows.Win32.Foundation.HRESULT CoAllowSetForegroundWindow(nint pUnk, [Optional] void* lpvReserved);
|
||||
}
|
||||
|
||||
private void OnUIThread(Action action)
|
||||
{
|
||||
_ = Task.Factory.StartNew(
|
||||
action,
|
||||
CancellationToken.None,
|
||||
TaskCreationOptions.None,
|
||||
_scheduler);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ using Microsoft.CmdPal.Common.Services;
|
||||
using Microsoft.CmdPal.Ext.Apps;
|
||||
using Microsoft.CmdPal.Ext.Bookmarks;
|
||||
using Microsoft.CmdPal.Ext.Calc;
|
||||
using Microsoft.CmdPal.Ext.ClipboardHistory;
|
||||
using Microsoft.CmdPal.Ext.Indexer;
|
||||
using Microsoft.CmdPal.Ext.Registry;
|
||||
using Microsoft.CmdPal.Ext.Shell;
|
||||
@@ -104,6 +105,7 @@ public partial class App : Application
|
||||
|
||||
services.AddSingleton<ICommandProvider, WindowWalkerCommandsProvider>();
|
||||
services.AddSingleton<ICommandProvider, WebSearchCommandsProvider>();
|
||||
services.AddSingleton<ICommandProvider, ClipboardHistoryCommandsProvider>();
|
||||
|
||||
// GH #38440: Users might not have WinGet installed! Or they might have
|
||||
// a ridiculously old version. Or might be running as admin.
|
||||
@@ -142,6 +144,8 @@ public partial class App : Application
|
||||
services.AddSingleton<IExtensionService, ExtensionService>();
|
||||
services.AddSingleton<TrayIconService>();
|
||||
|
||||
services.AddSingleton(new TelemetryForwarder());
|
||||
|
||||
// ViewModels
|
||||
services.AddSingleton<ShellViewModel>();
|
||||
|
||||
|
||||
@@ -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 CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.CmdPal.UI.Events;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.PowerToys.Telemetry;
|
||||
|
||||
namespace Microsoft.CmdPal.UI;
|
||||
|
||||
/// <summary>
|
||||
/// TelemetryForwarder is responsible for forwarding telemetry events from the
|
||||
/// command palette core to PowerToys Telemetry.
|
||||
/// This allows us to emit telemetry events as messages from the core,
|
||||
/// and then handle them by logging to our PT telemetry provider.
|
||||
///
|
||||
/// We may in the future want to replace this with a more generic "ITelemetryService"
|
||||
/// or something similar, but this works for now.
|
||||
/// </summary>
|
||||
internal sealed class TelemetryForwarder :
|
||||
IRecipient<BeginInvokeMessage>,
|
||||
IRecipient<CmdPalInvokeResultMessage>
|
||||
{
|
||||
public TelemetryForwarder()
|
||||
{
|
||||
WeakReferenceMessenger.Default.Register<BeginInvokeMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<CmdPalInvokeResultMessage>(this);
|
||||
}
|
||||
|
||||
public void Receive(CmdPalInvokeResultMessage message)
|
||||
{
|
||||
PowerToysTelemetry.Log.WriteEvent(new CmdPalInvokeResult(message.Kind));
|
||||
}
|
||||
|
||||
public void Receive(BeginInvokeMessage message)
|
||||
{
|
||||
PowerToysTelemetry.Log.WriteEvent(new BeginInvoke());
|
||||
}
|
||||
}
|
||||
@@ -107,6 +107,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\common\ManagedTelemetry\Telemetry\ManagedTelemetry.csproj" />
|
||||
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.ClipboardHistory\Microsoft.CmdPal.Ext.ClipboardHistory.csproj" />
|
||||
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.System\Microsoft.CmdPal.Ext.System.csproj" />
|
||||
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.WebSearch\Microsoft.CmdPal.Ext.WebSearch.csproj" />
|
||||
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.Indexer\Microsoft.CmdPal.Ext.Indexer.csproj" />
|
||||
|
||||
@@ -6,11 +6,9 @@ using System.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using CommunityToolkit.WinUI;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.Common.Services;
|
||||
using Microsoft.CmdPal.UI.Events;
|
||||
using Microsoft.CmdPal.UI.Settings;
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels.MainPage;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -28,15 +26,18 @@ namespace Microsoft.CmdPal.UI.Pages;
|
||||
/// </summary>
|
||||
public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
IRecipient<NavigateBackMessage>,
|
||||
IRecipient<PerformCommandMessage>,
|
||||
IRecipient<OpenSettingsMessage>,
|
||||
IRecipient<HotkeySummonMessage>,
|
||||
IRecipient<ShowDetailsMessage>,
|
||||
IRecipient<HideDetailsMessage>,
|
||||
IRecipient<ClearSearchMessage>,
|
||||
IRecipient<HandleCommandResultMessage>,
|
||||
IRecipient<LaunchUriMessage>,
|
||||
IRecipient<SettingsWindowClosedMessage>,
|
||||
IRecipient<GoHomeMessage>,
|
||||
IRecipient<GoBackMessage>,
|
||||
IRecipient<ShowConfirmationMessage>,
|
||||
IRecipient<ShowToastMessage>,
|
||||
IRecipient<NavigateToPageMessage>,
|
||||
INotifyPropertyChanged
|
||||
{
|
||||
private readonly DispatcherQueue _queue = DispatcherQueue.GetForCurrentThread();
|
||||
@@ -50,8 +51,6 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
|
||||
private readonly ToastWindow _toast = new();
|
||||
|
||||
private readonly Lock _invokeLock = new();
|
||||
private Task? _handleInvokeTask;
|
||||
private SettingsWindow? _settingsWindow;
|
||||
|
||||
public ShellViewModel ViewModel { get; private set; } = App.Current.Services.GetService<ShellViewModel>()!;
|
||||
@@ -64,8 +63,6 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
|
||||
// how we are doing navigation around
|
||||
WeakReferenceMessenger.Default.Register<NavigateBackMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<PerformCommandMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<HandleCommandResultMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<OpenSettingsMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<HotkeySummonMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<SettingsWindowClosedMessage>(this);
|
||||
@@ -76,6 +73,12 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
WeakReferenceMessenger.Default.Register<ClearSearchMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<LaunchUriMessage>(this);
|
||||
|
||||
WeakReferenceMessenger.Default.Register<GoHomeMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<GoBackMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<ShowConfirmationMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<ShowToastMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<NavigateToPageMessage>(this);
|
||||
|
||||
RootFrame.Navigate(typeof(LoadingPage), ViewModel);
|
||||
}
|
||||
|
||||
@@ -103,195 +106,72 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
}
|
||||
}
|
||||
|
||||
public void Receive(PerformCommandMessage message)
|
||||
public void Receive(NavigateToPageMessage message)
|
||||
{
|
||||
PerformCommand(message);
|
||||
// TODO GH #526 This needs more better locking too
|
||||
_ = _queue.TryEnqueue(() =>
|
||||
{
|
||||
// Also hide our details pane about here, if we had one
|
||||
HideDetails();
|
||||
|
||||
// Navigate to the appropriate host page for that VM
|
||||
RootFrame.Navigate(
|
||||
message.Page switch
|
||||
{
|
||||
ListViewModel => typeof(ListPage),
|
||||
ContentPageViewModel => typeof(ContentPage),
|
||||
_ => throw new NotSupportedException(),
|
||||
},
|
||||
message.Page,
|
||||
message.WithAnimation ? _slideRightTransition : _noAnimation);
|
||||
|
||||
PowerToysTelemetry.Log.WriteEvent(new OpenPage(RootFrame.BackStackDepth));
|
||||
|
||||
// Refocus on the Search for continual typing on the next search request
|
||||
SearchBox.Focus(Microsoft.UI.Xaml.FocusState.Programmatic);
|
||||
|
||||
if (!ViewModel.IsNested)
|
||||
{
|
||||
// todo BODGY
|
||||
RootFrame.BackStack.Clear();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void PerformCommand(PerformCommandMessage message)
|
||||
public void Receive(ShowConfirmationMessage message)
|
||||
{
|
||||
var command = message.Command.Unsafe;
|
||||
if (command == null)
|
||||
DispatcherQueue.TryEnqueue(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await HandleConfirmArgsOnUiThread(message.Args);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex.ToString());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void Receive(ShowToastMessage message)
|
||||
{
|
||||
DispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
_toast.ShowToast(message.Message);
|
||||
});
|
||||
}
|
||||
|
||||
// This gets called from the UI thread
|
||||
private async Task HandleConfirmArgsOnUiThread(IConfirmationArgs? args)
|
||||
{
|
||||
if (args == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ViewModel.CurrentPage.IsNested)
|
||||
{
|
||||
// on the main page here
|
||||
ViewModel.PerformTopLevelCommand(message);
|
||||
}
|
||||
|
||||
IExtensionWrapper? extension = null;
|
||||
|
||||
// TODO: Actually loading up the page, or invoking the command -
|
||||
// that might belong in the model, not the view?
|
||||
// Especially considering the try/catch concerns around the fact that the
|
||||
// COM call might just fail.
|
||||
// Or the command may be a stub. Future us problem.
|
||||
try
|
||||
{
|
||||
// In the case that we're coming from a top-level command, the
|
||||
// current page's host is the global instance. We only really want
|
||||
// to use that as the host of last resort.
|
||||
var pageHost = ViewModel.CurrentPage?.ExtensionHost;
|
||||
if (pageHost == CommandPaletteHost.Instance)
|
||||
{
|
||||
pageHost = null;
|
||||
}
|
||||
|
||||
var messageHost = message.ExtensionHost;
|
||||
|
||||
// Use the host from the current page if it has one, else use the
|
||||
// one specified in the PerformMessage for a top-level command,
|
||||
// else just use the global one.
|
||||
CommandPaletteHost host;
|
||||
|
||||
// Top level items can come through without a Extension set on the
|
||||
// message. In that case, the `Context` is actually the
|
||||
// TopLevelViewModel itself, and we can use that to get at the
|
||||
// extension object.
|
||||
extension = pageHost?.Extension ?? messageHost?.Extension ?? null;
|
||||
if (extension == null && message.Context is TopLevelViewModel topLevelViewModel)
|
||||
{
|
||||
extension = topLevelViewModel.ExtensionHost?.Extension;
|
||||
host = pageHost ?? messageHost ?? topLevelViewModel?.ExtensionHost ?? CommandPaletteHost.Instance;
|
||||
}
|
||||
else
|
||||
{
|
||||
host = pageHost ?? messageHost ?? CommandPaletteHost.Instance;
|
||||
}
|
||||
|
||||
if (extension != null)
|
||||
{
|
||||
if (messageHost != null)
|
||||
{
|
||||
Logger.LogDebug($"Activated top-level command from {extension.ExtensionDisplayName}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogDebug($"Activated command from {extension.ExtensionDisplayName}");
|
||||
}
|
||||
}
|
||||
|
||||
ViewModel.SetActiveExtension(extension);
|
||||
|
||||
if (command is IPage page)
|
||||
{
|
||||
Logger.LogDebug($"Navigating to page");
|
||||
|
||||
// TODO GH #526 This needs more better locking too
|
||||
_ = _queue.TryEnqueue(() =>
|
||||
{
|
||||
// Also hide our details pane about here, if we had one
|
||||
HideDetails();
|
||||
|
||||
WeakReferenceMessenger.Default.Send<UpdateCommandBarMessage>(new(null));
|
||||
|
||||
var isMainPage = command is MainListPage;
|
||||
|
||||
// Construct our ViewModel of the appropriate type and pass it the UI Thread context.
|
||||
PageViewModel pageViewModel = page switch
|
||||
{
|
||||
IListPage listPage => new ListViewModel(listPage, _mainTaskScheduler, host)
|
||||
{
|
||||
IsNested = !isMainPage,
|
||||
},
|
||||
IContentPage contentPage => new ContentPageViewModel(contentPage, _mainTaskScheduler, host),
|
||||
_ => throw new NotSupportedException(),
|
||||
};
|
||||
|
||||
// Kick off async loading of our ViewModel
|
||||
ViewModel.LoadPageViewModel(pageViewModel);
|
||||
|
||||
// Navigate to the appropriate host page for that VM
|
||||
RootFrame.Navigate(
|
||||
page switch
|
||||
{
|
||||
IListPage => typeof(ListPage),
|
||||
IContentPage => typeof(ContentPage),
|
||||
_ => throw new NotSupportedException(),
|
||||
},
|
||||
pageViewModel,
|
||||
message.WithAnimation ? _slideRightTransition : _noAnimation);
|
||||
|
||||
PowerToysTelemetry.Log.WriteEvent(new OpenPage(RootFrame.BackStackDepth));
|
||||
|
||||
// Refocus on the Search for continual typing on the next search request
|
||||
SearchBox.Focus(Microsoft.UI.Xaml.FocusState.Programmatic);
|
||||
|
||||
if (isMainPage)
|
||||
{
|
||||
// todo BODGY
|
||||
RootFrame.BackStack.Clear();
|
||||
}
|
||||
|
||||
// Note: Originally we set our page back in the ViewModel here, but that now happens in response to the Frame navigating triggered from the above
|
||||
// See RootFrame_Navigated event handler.
|
||||
});
|
||||
}
|
||||
else if (command is IInvokableCommand invokable)
|
||||
{
|
||||
Logger.LogDebug($"Invoking command");
|
||||
PowerToysTelemetry.Log.WriteEvent(new BeginInvoke());
|
||||
HandleInvokeCommand(message, invokable);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// TODO: It would be better to do this as a page exception, rather
|
||||
// than a silent log message.
|
||||
CommandPaletteHost.Instance.Log(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleInvokeCommand(PerformCommandMessage message, IInvokableCommand invokable)
|
||||
{
|
||||
// TODO GH #525 This needs more better locking.
|
||||
lock (_invokeLock)
|
||||
{
|
||||
if (_handleInvokeTask != null)
|
||||
{
|
||||
// do nothing - a command is already doing a thing
|
||||
}
|
||||
else
|
||||
{
|
||||
_handleInvokeTask = Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = invokable.Invoke(message.Context);
|
||||
DispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
HandleCommandResultOnUiThread(result);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_handleInvokeTask = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_handleInvokeTask = null;
|
||||
|
||||
// TODO: It would be better to do this as a page exception, rather
|
||||
// than a silent log message.
|
||||
CommandPaletteHost.Instance.Log(ex.Message);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This gets called from the UI thread
|
||||
private void HandleConfirmArgs(IConfirmationArgs args)
|
||||
{
|
||||
ConfirmResultViewModel vm = new(args, new(ViewModel.CurrentPage));
|
||||
var initializeDialogTask = Task.Run(() => { InitializeConfirmationDialog(vm); });
|
||||
initializeDialogTask.Wait();
|
||||
await initializeDialogTask;
|
||||
|
||||
var resourceLoader = Microsoft.CmdPal.UI.Helpers.ResourceLoaderInstance.ResourceLoader;
|
||||
var confirmText = resourceLoader.GetString("ConfirmationDialog_ConfirmButtonText");
|
||||
@@ -322,19 +202,16 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
// };
|
||||
}
|
||||
|
||||
DispatcherQueue.TryEnqueue(async () =>
|
||||
var result = await dialog.ShowAsync();
|
||||
if (result == ContentDialogResult.Primary)
|
||||
{
|
||||
var result = await dialog.ShowAsync();
|
||||
if (result == ContentDialogResult.Primary)
|
||||
{
|
||||
var performMessage = new PerformCommandMessage(vm);
|
||||
PerformCommand(performMessage);
|
||||
}
|
||||
else
|
||||
{
|
||||
// cancel
|
||||
}
|
||||
});
|
||||
var performMessage = new PerformCommandMessage(vm);
|
||||
WeakReferenceMessenger.Default.Send(performMessage);
|
||||
}
|
||||
else
|
||||
{
|
||||
// cancel
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeConfirmationDialog(ConfirmResultViewModel vm)
|
||||
@@ -342,79 +219,6 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
vm.SafeInitializePropertiesSynchronous();
|
||||
}
|
||||
|
||||
private void HandleCommandResultOnUiThread(ICommandResult? result)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (result != null)
|
||||
{
|
||||
var kind = result.Kind;
|
||||
Logger.LogDebug($"handling {kind.ToString()}");
|
||||
PowerToysTelemetry.Log.WriteEvent(new CmdPalInvokeResult(kind));
|
||||
switch (kind)
|
||||
{
|
||||
case CommandResultKind.Dismiss:
|
||||
{
|
||||
// Reset the palette to the main page and dismiss
|
||||
GoHome(withAnimation: false, focusSearch: false);
|
||||
WeakReferenceMessenger.Default.Send<DismissMessage>();
|
||||
break;
|
||||
}
|
||||
|
||||
case CommandResultKind.GoHome:
|
||||
{
|
||||
// Go back to the main page, but keep it open
|
||||
GoHome();
|
||||
break;
|
||||
}
|
||||
|
||||
case CommandResultKind.GoBack:
|
||||
{
|
||||
GoBack();
|
||||
break;
|
||||
}
|
||||
|
||||
case CommandResultKind.Hide:
|
||||
{
|
||||
// Keep this page open, but hide the palette.
|
||||
WeakReferenceMessenger.Default.Send<DismissMessage>();
|
||||
break;
|
||||
}
|
||||
|
||||
case CommandResultKind.KeepOpen:
|
||||
{
|
||||
// Do nothing.
|
||||
break;
|
||||
}
|
||||
|
||||
case CommandResultKind.Confirm:
|
||||
{
|
||||
if (result.Args is IConfirmationArgs a)
|
||||
{
|
||||
HandleConfirmArgs(a);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case CommandResultKind.ShowToast:
|
||||
{
|
||||
if (result.Args is IToastArgs a)
|
||||
{
|
||||
_toast.ShowToast(a.Message);
|
||||
HandleCommandResultOnUiThread(a.Result);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public void Receive(OpenSettingsMessage message)
|
||||
{
|
||||
_ = DispatcherQueue.TryEnqueue(() =>
|
||||
@@ -467,14 +271,6 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
|
||||
public void Receive(LaunchUriMessage message) => _ = global::Windows.System.Launcher.LaunchUriAsync(message.Uri);
|
||||
|
||||
public void Receive(HandleCommandResultMessage message)
|
||||
{
|
||||
DispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
HandleCommandResultOnUiThread(message.Result.Unsafe);
|
||||
});
|
||||
}
|
||||
|
||||
private void HideDetails()
|
||||
{
|
||||
ViewModel.Details = null;
|
||||
@@ -554,6 +350,11 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
WeakReferenceMessenger.Default.Send<FocusSearchBoxMessage>();
|
||||
}
|
||||
|
||||
public void Receive(GoBackMessage message)
|
||||
{
|
||||
_ = DispatcherQueue.TryEnqueue(() => GoBack(message.WithAnimation, message.FocusSearch));
|
||||
}
|
||||
|
||||
private void GoBack(bool withAnimation = true, bool focusSearch = true)
|
||||
{
|
||||
HideDetails();
|
||||
@@ -591,14 +392,17 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
}
|
||||
}
|
||||
|
||||
public void Receive(GoHomeMessage message)
|
||||
{
|
||||
_ = DispatcherQueue.TryEnqueue(() => GoHome(withAnimation: message.WithAnimation, focusSearch: message.FocusSearch));
|
||||
}
|
||||
|
||||
private void GoHome(bool withAnimation = true, bool focusSearch = true)
|
||||
{
|
||||
while (RootFrame.CanGoBack)
|
||||
{
|
||||
GoBack(withAnimation, focusSearch);
|
||||
}
|
||||
|
||||
WeakReferenceMessenger.Default.Send<GoHomeMessage>();
|
||||
}
|
||||
|
||||
private void BackButton_Tapped(object sender, Microsoft.UI.Xaml.Input.TappedRoutedEventArgs e) => WeakReferenceMessenger.Default.Send<NavigateBackMessage>(new());
|
||||
|
||||
@@ -16,12 +16,13 @@ public partial class ClipboardHistoryCommandsProvider : CommandProvider
|
||||
{
|
||||
_clipboardHistoryListItem = new ListItem(new ClipboardHistoryListPage())
|
||||
{
|
||||
Title = "Search Clipboard History",
|
||||
Icon = new IconInfo("\xE8C8"), // Copy icon
|
||||
Title = Properties.Resources.list_item_title,
|
||||
Subtitle = Properties.Resources.list_item_subtitle,
|
||||
Icon = Icons.ClipboardList,
|
||||
};
|
||||
|
||||
DisplayName = $"Clipboard History";
|
||||
Icon = new IconInfo("\xE8C8"); // Copy icon
|
||||
DisplayName = Properties.Resources.provider_display_name;
|
||||
Icon = Icons.ClipboardList;
|
||||
Id = "Windows.ClipboardHistory";
|
||||
}
|
||||
|
||||
|
||||
@@ -16,20 +16,20 @@ internal sealed partial class CopyCommand : InvokableCommand
|
||||
{
|
||||
_clipboardItem = clipboardItem;
|
||||
_clipboardFormat = clipboardFormat;
|
||||
Name = "Copy";
|
||||
Name = Properties.Resources.copy_command_name;
|
||||
if (clipboardFormat == ClipboardFormat.Text)
|
||||
{
|
||||
Icon = new("\xE8C8"); // Copy icon
|
||||
Icon = Icons.Copy;
|
||||
}
|
||||
else
|
||||
{
|
||||
Icon = new("\xE8B9"); // Picture icon
|
||||
Icon = Icons.Picture;
|
||||
}
|
||||
}
|
||||
|
||||
public override CommandResult Invoke()
|
||||
{
|
||||
ClipboardHelper.SetClipboardContent(_clipboardItem, _clipboardFormat);
|
||||
return CommandResult.ShowToast("Copied to clipboard");
|
||||
return CommandResult.ShowToast(Properties.Resources.copied_toast_text);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.CmdPal.Common.Messages;
|
||||
using Microsoft.CmdPal.Ext.ClipboardHistory.Models;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
using Windows.ApplicationModel.DataTransfer;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.ClipboardHistory.Commands;
|
||||
@@ -20,8 +19,8 @@ internal sealed partial class PasteCommand : InvokableCommand
|
||||
{
|
||||
_clipboardItem = clipboardItem;
|
||||
_clipboardFormat = clipboardFormat;
|
||||
Name = "Paste";
|
||||
Icon = new("\xE8C8"); // Copy icon
|
||||
Name = Properties.Resources.paste_command_name;
|
||||
Icon = Icons.Paste;
|
||||
}
|
||||
|
||||
private void HideWindow()
|
||||
@@ -37,8 +36,10 @@ internal sealed partial class PasteCommand : InvokableCommand
|
||||
{
|
||||
ClipboardHelper.SetClipboardContent(_clipboardItem, _clipboardFormat);
|
||||
HideWindow();
|
||||
|
||||
ClipboardHelper.SendPasteKeyCombination();
|
||||
|
||||
Clipboard.DeleteItemFromHistory(_clipboardItem.Item);
|
||||
return CommandResult.ShowToast("Pasting");
|
||||
return CommandResult.ShowToast(Properties.Resources.paste_toast_text);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,10 +59,10 @@ internal static class ClipboardHelper
|
||||
output.SetText(text);
|
||||
try
|
||||
{
|
||||
// Clipboard.SetContentWithOptions(output, null);
|
||||
ClipboardThreadQueue.EnqueueTask(() =>
|
||||
{
|
||||
Clipboard.SetContent(output);
|
||||
|
||||
Flush();
|
||||
ExtensionHost.LogMessage(new LogMessage() { Message = "Copied text to clipboard" });
|
||||
});
|
||||
@@ -87,7 +87,7 @@ internal static class ClipboardHelper
|
||||
{
|
||||
try
|
||||
{
|
||||
Task.Run(Clipboard.Flush).Wait();
|
||||
Clipboard.Flush();
|
||||
return;
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.ClipboardHistory;
|
||||
|
||||
internal sealed class Icons
|
||||
{
|
||||
internal static IconInfo Copy { get; } = new("\xE8C8");
|
||||
|
||||
internal static IconInfo Picture { get; } = new("\xE8B9");
|
||||
|
||||
internal static IconInfo Paste { get; } = new("\uE77F");
|
||||
|
||||
internal static IconInfo ClipboardList { get; } = new("\uF0E3");
|
||||
}
|
||||
@@ -15,4 +15,19 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- String resources -->
|
||||
<ItemGroup>
|
||||
<Compile Update="Properties\Resources.Designer.cs">
|
||||
<DependentUpon>Resources.resx</DependentUpon>
|
||||
<DesignTime>True</DesignTime>
|
||||
<AutoGen>True</AutoGen>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Update="Properties\Resources.resx">
|
||||
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
|
||||
<Generator>PublicResXFileCodeGenerator</Generator>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -24,8 +24,8 @@ internal sealed partial class ClipboardHistoryListPage : ListPage
|
||||
{
|
||||
clipboardHistory = [];
|
||||
_defaultIconPath = string.Empty;
|
||||
Icon = new("\uF0E3"); // ClipboardList icon
|
||||
Name = "Clipboard History";
|
||||
Icon = Icons.ClipboardList;
|
||||
Name = Properties.Resources.clipboard_history_page_name;
|
||||
Id = "com.microsoft.cmdpal.clipboardHistory";
|
||||
ShowDetails = true;
|
||||
|
||||
@@ -113,7 +113,7 @@ internal sealed partial class ClipboardHistoryListPage : ListPage
|
||||
{
|
||||
// TODO GH #108 We need to figure out some logging
|
||||
// Logger.LogError("Loading clipboard history failed", ex);
|
||||
ExtensionHost.ShowStatus(new StatusMessage() { Message = "Loading clipboard history failed", State = MessageState.Error }, StatusContext.Page);
|
||||
ExtensionHost.ShowStatus(new StatusMessage() { Message = Properties.Resources.clipboard_failed_to_load, State = MessageState.Error }, StatusContext.Page);
|
||||
ExtensionHost.LogMessage(ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
144
src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Properties/Resources.Designer.cs
generated
Normal file
144
src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Properties/Resources.Designer.cs
generated
Normal file
@@ -0,0 +1,144 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
// Runtime Version:4.0.30319.42000
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.ClipboardHistory.Properties {
|
||||
using System;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// A strongly-typed resource class, for looking up localized strings, etc.
|
||||
/// </summary>
|
||||
// This class was auto-generated by the StronglyTypedResourceBuilder
|
||||
// class via a tool like ResGen or Visual Studio.
|
||||
// To add or remove a member, edit your .ResX file then rerun ResGen
|
||||
// with the /str option, or rebuild your VS project.
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
public class Resources {
|
||||
|
||||
private static global::System.Resources.ResourceManager resourceMan;
|
||||
|
||||
private static global::System.Globalization.CultureInfo resourceCulture;
|
||||
|
||||
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
|
||||
internal Resources() {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the cached ResourceManager instance used by this class.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
public static global::System.Resources.ResourceManager ResourceManager {
|
||||
get {
|
||||
if (object.ReferenceEquals(resourceMan, null)) {
|
||||
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.CmdPal.Ext.ClipboardHistory.Properties.Resources", typeof(Resources).Assembly);
|
||||
resourceMan = temp;
|
||||
}
|
||||
return resourceMan;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Overrides the current thread's CurrentUICulture property for all
|
||||
/// resource lookups using this strongly typed resource class.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
public static global::System.Globalization.CultureInfo Culture {
|
||||
get {
|
||||
return resourceCulture;
|
||||
}
|
||||
set {
|
||||
resourceCulture = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Loading clipboard history failed.
|
||||
/// </summary>
|
||||
public static string clipboard_failed_to_load {
|
||||
get {
|
||||
return ResourceManager.GetString("clipboard_failed_to_load", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Open.
|
||||
/// </summary>
|
||||
public static string clipboard_history_page_name {
|
||||
get {
|
||||
return ResourceManager.GetString("clipboard_history_page_name", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Copied to clipboard.
|
||||
/// </summary>
|
||||
public static string copied_toast_text {
|
||||
get {
|
||||
return ResourceManager.GetString("copied_toast_text", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Copy.
|
||||
/// </summary>
|
||||
public static string copy_command_name {
|
||||
get {
|
||||
return ResourceManager.GetString("copy_command_name", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Copy, paste, and search items on the clipboard.
|
||||
/// </summary>
|
||||
public static string list_item_subtitle {
|
||||
get {
|
||||
return ResourceManager.GetString("list_item_subtitle", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Clipboard History.
|
||||
/// </summary>
|
||||
public static string list_item_title {
|
||||
get {
|
||||
return ResourceManager.GetString("list_item_title", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Paste.
|
||||
/// </summary>
|
||||
public static string paste_command_name {
|
||||
get {
|
||||
return ResourceManager.GetString("paste_command_name", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Pasting.
|
||||
/// </summary>
|
||||
public static string paste_toast_text {
|
||||
get {
|
||||
return ResourceManager.GetString("paste_toast_text", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Clipboard History.
|
||||
/// </summary>
|
||||
public static string provider_display_name {
|
||||
get {
|
||||
return ResourceManager.GetString("provider_display_name", resourceCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="copy_command_name" xml:space="preserve">
|
||||
<value>Copy</value>
|
||||
</data>
|
||||
<data name="paste_command_name" xml:space="preserve">
|
||||
<value>Paste</value>
|
||||
</data>
|
||||
<data name="paste_toast_text" xml:space="preserve">
|
||||
<value>Pasting</value>
|
||||
</data>
|
||||
<data name="copied_toast_text" xml:space="preserve">
|
||||
<value>Copied to clipboard</value>
|
||||
</data>
|
||||
<data name="list_item_title" xml:space="preserve">
|
||||
<value>Clipboard History</value>
|
||||
</data>
|
||||
<data name="list_item_subtitle" xml:space="preserve">
|
||||
<value>Copy, paste, and search items on the clipboard</value>
|
||||
</data>
|
||||
<data name="provider_display_name" xml:space="preserve">
|
||||
<value>Clipboard History</value>
|
||||
</data>
|
||||
<data name="clipboard_history_page_name" xml:space="preserve">
|
||||
<value>Open</value>
|
||||
</data>
|
||||
<data name="clipboard_failed_to_load" xml:space="preserve">
|
||||
<value>Loading clipboard history failed</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -133,13 +133,15 @@
|
||||
VerticalAlignment="Top"
|
||||
Background="Transparent"
|
||||
Header1FontSize="20"
|
||||
Header1Margin="0,16,0,0"
|
||||
Header2FontSize="17"
|
||||
Header2FontWeight="SemiBold"
|
||||
Header4FontSize="14"
|
||||
Header1FontWeight="SemiBold"
|
||||
Header1Margin="0,0,0,4"
|
||||
Header3FontSize="16"
|
||||
Header3FontWeight="SemiBold"
|
||||
Header4FontSize="16"
|
||||
Header4FontWeight="SemiBold"
|
||||
HorizontalRuleMargin="24"
|
||||
LinkClicked="ReleaseNotesMarkdown_LinkClicked"
|
||||
ListMargin="-18,4,0,12"
|
||||
ParagraphMargin="0,0,0,0"
|
||||
TableMargin="24"
|
||||
Visibility="Collapsed" />
|
||||
|
||||
@@ -112,7 +112,7 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
|
||||
foreach (var release in latestReleases)
|
||||
{
|
||||
releaseNotesHtmlBuilder.AppendLine("# " + release.Name);
|
||||
var notes = removeHashRegex.Replace(release.ReleaseNotes, "\r\n## Highlights");
|
||||
var notes = removeHashRegex.Replace(release.ReleaseNotes, "\r\n### Highlights");
|
||||
|
||||
// Add a unique counter to [github-current-release-work] to distinguish each release,
|
||||
// since this variable is used for all latest releases when they are merged.
|
||||
|
||||
Reference in New Issue
Block a user