mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-01-03 19:06:41 +01:00
Compare commits
83 Commits
dev/vanzue
...
shawn/quic
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa262cdf20 | ||
|
|
bb3dee489b | ||
|
|
01aa491962 | ||
|
|
ee0d931f39 | ||
|
|
eceb55ca9f | ||
|
|
37bd24db36 | ||
|
|
557a07589d | ||
|
|
2603efc8a9 | ||
|
|
dd138fb94b | ||
|
|
7cd201d355 | ||
|
|
9aab0f3893 | ||
|
|
91b7a99e76 | ||
|
|
d38edf798d | ||
|
|
17668047bf | ||
|
|
e941ea4ebf | ||
|
|
7b0b284d40 | ||
|
|
9aca6d136f | ||
|
|
4b2ee60b42 | ||
|
|
e37a328624 | ||
|
|
b1cf7fa9da | ||
|
|
66e96bbe9d | ||
|
|
e13d6a78aa | ||
|
|
73786cd2be | ||
|
|
346c99543a | ||
|
|
1211e2c92f | ||
|
|
9f99727bf5 | ||
|
|
fee146b084 | ||
|
|
4de4d5f310 | ||
|
|
f8c5ff8c0c | ||
|
|
d32ea86314 | ||
|
|
177f144e6d | ||
|
|
7b469f6327 | ||
|
|
94ace730c8 | ||
|
|
a08fc0921f | ||
|
|
995bbdc62d | ||
|
|
91cceb70d5 | ||
|
|
b73908c413 | ||
|
|
f822826cf1 | ||
|
|
062589b9f7 | ||
|
|
ff21b77b7b | ||
|
|
d6ae07c040 | ||
|
|
e038a09357 | ||
|
|
97c1de8bf6 | ||
|
|
f6e6fe676a | ||
|
|
0f8cf94d90 | ||
|
|
620f67a3ba | ||
|
|
debf4322df | ||
|
|
4a823320c4 | ||
|
|
23bc278cc8 | ||
|
|
e89d6d8f5a | ||
|
|
2ca7531ae7 | ||
|
|
6ba33b0d91 | ||
|
|
273eebda25 | ||
|
|
fdd34bca09 | ||
|
|
dd9643dd38 | ||
|
|
eb69549320 | ||
|
|
8f6bd72679 | ||
|
|
4865b44de1 | ||
|
|
3381e82fc1 | ||
|
|
a40be6f9be | ||
|
|
90e4f1ca41 | ||
|
|
1fcb8e7515 | ||
|
|
3ed9641072 | ||
|
|
0428cf45af | ||
|
|
5fc0ae7e42 | ||
|
|
8cdd8574d6 | ||
|
|
f564cf9189 | ||
|
|
e826cfb491 | ||
|
|
45e94f2ce0 | ||
|
|
b3b99d6d11 | ||
|
|
bb180a166a | ||
|
|
7e62c76c18 | ||
|
|
68dd7a46f0 | ||
|
|
a67326d86d | ||
|
|
16733718c3 | ||
|
|
5bf797d9f6 | ||
|
|
8de7110918 | ||
|
|
ed254b60cc | ||
|
|
ea0a13be3a | ||
|
|
8e6f40ffe9 | ||
|
|
891dd41cc2 | ||
|
|
d7e727fa1a | ||
|
|
709ceed137 |
4
.github/actions/spell-check/allow/code.txt
vendored
4
.github/actions/spell-check/allow/code.txt
vendored
@@ -335,3 +335,7 @@ azp
|
||||
feedbackhub
|
||||
needinfo
|
||||
reportbug
|
||||
|
||||
#ffmpeg
|
||||
crf
|
||||
nostdin
|
||||
|
||||
21
.github/actions/spell-check/expect.txt
vendored
21
.github/actions/spell-check/expect.txt
vendored
@@ -141,8 +141,11 @@ BITSPIXEL
|
||||
bla
|
||||
BLACKFRAME
|
||||
BLENDFUNCTION
|
||||
blittable
|
||||
Blockquotes
|
||||
blt
|
||||
bluelightreduction
|
||||
bluelightreductionstate
|
||||
BLURBEHIND
|
||||
BLURREGION
|
||||
bmi
|
||||
@@ -221,7 +224,6 @@ clientside
|
||||
CLIPBOARDUPDATE
|
||||
CLIPCHILDREN
|
||||
CLIPSIBLINGS
|
||||
CLITo
|
||||
closesocket
|
||||
clp
|
||||
CLSCTX
|
||||
@@ -250,6 +252,7 @@ colorformat
|
||||
colorhistory
|
||||
colorhistorylimit
|
||||
COLORKEY
|
||||
colorref
|
||||
comctl
|
||||
comdlg
|
||||
comexp
|
||||
@@ -321,7 +324,6 @@ CUSTOMFORMATPLACEHOLDER
|
||||
CVal
|
||||
cvd
|
||||
CVirtual
|
||||
CVS
|
||||
CWMO
|
||||
CXSCREEN
|
||||
CXSMICON
|
||||
@@ -361,6 +363,7 @@ DEFAULTICON
|
||||
defaultlib
|
||||
DEFAULTONLY
|
||||
DEFAULTTONEAREST
|
||||
Defaulttonearest
|
||||
DEFAULTTONULL
|
||||
DEFAULTTOPRIMARY
|
||||
DEFERERASE
|
||||
@@ -854,6 +857,7 @@ lastcodeanalysissucceeded
|
||||
LASTEXITCODE
|
||||
LAYOUTRTL
|
||||
lbl
|
||||
Lbuttondown
|
||||
LCh
|
||||
lcid
|
||||
LCIDTo
|
||||
@@ -974,6 +978,7 @@ maxversiontested
|
||||
mber
|
||||
MBM
|
||||
MBR
|
||||
Mbuttondown
|
||||
MDICHILD
|
||||
MDL
|
||||
mdtext
|
||||
@@ -1074,7 +1079,6 @@ MVVMTK
|
||||
MWBEx
|
||||
MYICON
|
||||
NAMECHANGE
|
||||
Notavailable
|
||||
namespaceanddescendants
|
||||
nao
|
||||
NCACTIVATE
|
||||
@@ -1113,6 +1117,7 @@ NEWPLUSSHELLEXTENSIONWIN
|
||||
newrow
|
||||
nicksnettravels
|
||||
NIF
|
||||
nightlight
|
||||
NLog
|
||||
NLSTEXT
|
||||
NMAKE
|
||||
@@ -1426,6 +1431,7 @@ RAWINPUTHEADER
|
||||
RAWMODE
|
||||
RAWPATH
|
||||
rbhid
|
||||
Rbuttondown
|
||||
rclsid
|
||||
RCZOOMIT
|
||||
remotedesktop
|
||||
@@ -1725,6 +1731,7 @@ svgz
|
||||
SVSI
|
||||
SWFO
|
||||
SWP
|
||||
Swp
|
||||
SWPNOSIZE
|
||||
SWPNOZORDER
|
||||
SWRESTORE
|
||||
@@ -1744,6 +1751,7 @@ syskeydown
|
||||
SYSKEYUP
|
||||
SYSLIB
|
||||
SYSMENU
|
||||
Sysmenu
|
||||
systemai
|
||||
SYSTEMAPPS
|
||||
SYSTEMMODAL
|
||||
@@ -1800,7 +1808,6 @@ tlbimp
|
||||
tlc
|
||||
tmain
|
||||
TNP
|
||||
toolgood
|
||||
Toolhelp
|
||||
toolwindow
|
||||
TOPDOWNDIB
|
||||
@@ -1849,6 +1856,8 @@ uitests
|
||||
UITo
|
||||
ULONGLONG
|
||||
ums
|
||||
UMax
|
||||
UMin
|
||||
uncompilable
|
||||
UNCPRIORITY
|
||||
UNDNAME
|
||||
@@ -1862,6 +1871,7 @@ unittests
|
||||
UNLEN
|
||||
UNORM
|
||||
unremapped
|
||||
Unsubscribes
|
||||
unvirtualized
|
||||
unwide
|
||||
unzoom
|
||||
@@ -1946,7 +1956,7 @@ vswhere
|
||||
Vtbl
|
||||
WANTNUKEWARNING
|
||||
WANTPALM
|
||||
wasdk
|
||||
WASDK
|
||||
wbem
|
||||
WBounds
|
||||
Wca
|
||||
@@ -2067,6 +2077,7 @@ Wwanpp
|
||||
xap
|
||||
XAxis
|
||||
XButton
|
||||
Xbuttondown
|
||||
xclip
|
||||
xcopy
|
||||
XDeployment
|
||||
|
||||
@@ -353,6 +353,11 @@
|
||||
"Microsoft.SemanticKernel.Connectors.Ollama.dll",
|
||||
"OllamaSharp.dll",
|
||||
|
||||
"boost_regex-vc143-mt-gd-x32-1_87.dll",
|
||||
"boost_regex-vc143-mt-gd-x64-1_87.dll",
|
||||
"boost_regex-vc143-mt-x32-1_87.dll",
|
||||
"boost_regex-vc143-mt-x64-1_87.dll",
|
||||
|
||||
"UnitsNet.dll",
|
||||
"UtfUnknown.dll",
|
||||
"Wpf.Ui.dll"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
Param(
|
||||
# Using the default value of 1.7 for winAppSdkVersionNumber and useExperimentalVersion as false
|
||||
[Parameter(Mandatory=$False,Position=1)]
|
||||
[string]$winAppSdkVersionNumber = "1.7",
|
||||
[string]$winAppSdkVersionNumber = "1.8",
|
||||
|
||||
# When the pipeline calls the PS1 file, the passed parameters are converted to string type
|
||||
[Parameter(Mandatory=$False,Position=2)]
|
||||
@@ -16,32 +16,7 @@ Param(
|
||||
[string]$sourceLink = "https://microsoft.pkgs.visualstudio.com/ProjectReunion/_packaging/Project.Reunion.nuget.internal/nuget/v3/index.json"
|
||||
)
|
||||
|
||||
function Update-NugetConfig {
|
||||
param (
|
||||
[string]$filePath = [System.IO.Path]::Combine($rootPath, "nuget.config")
|
||||
)
|
||||
|
||||
Write-Host "Updating nuget.config file"
|
||||
[xml]$xml = Get-Content -Path $filePath
|
||||
|
||||
# Add localpackages source into nuget.config
|
||||
$packageSourcesNode = $xml.configuration.packageSources
|
||||
$addNode = $xml.CreateElement("add")
|
||||
$addNode.SetAttribute("key", "localpackages")
|
||||
$addNode.SetAttribute("value", "localpackages")
|
||||
$packageSourcesNode.AppendChild($addNode) | Out-Null
|
||||
|
||||
# Remove <packageSourceMapping> tag and its content
|
||||
$packageSourceMappingNode = $xml.configuration.packageSourceMapping
|
||||
if ($packageSourceMappingNode) {
|
||||
$xml.configuration.RemoveChild($packageSourceMappingNode) | Out-Null
|
||||
}
|
||||
|
||||
# print nuget.config after modification
|
||||
$xml.OuterXml
|
||||
# Save the modified nuget.config file
|
||||
$xml.Save($filePath)
|
||||
}
|
||||
|
||||
function Read-FileWithEncoding {
|
||||
param (
|
||||
@@ -71,6 +46,132 @@ function Write-FileWithEncoding {
|
||||
$writer.Close()
|
||||
}
|
||||
|
||||
|
||||
function Add-NuGetSourceAndMapping {
|
||||
param (
|
||||
[xml]$Xml,
|
||||
[string]$Key,
|
||||
[string]$Value,
|
||||
[string[]]$Patterns
|
||||
)
|
||||
|
||||
# Ensure packageSources exists
|
||||
if (-not $Xml.configuration.packageSources) {
|
||||
$Xml.configuration.AppendChild($Xml.CreateElement("packageSources")) | Out-Null
|
||||
}
|
||||
$sources = $Xml.configuration.packageSources
|
||||
|
||||
# Add/Update Source
|
||||
$sourceNode = $sources.SelectSingleNode("add[@key='$Key']")
|
||||
if (-not $sourceNode) {
|
||||
$sourceNode = $Xml.CreateElement("add")
|
||||
$sourceNode.SetAttribute("key", $Key)
|
||||
$sources.AppendChild($sourceNode) | Out-Null
|
||||
}
|
||||
$sourceNode.SetAttribute("value", $Value)
|
||||
|
||||
# Ensure packageSourceMapping exists
|
||||
if (-not $Xml.configuration.packageSourceMapping) {
|
||||
$Xml.configuration.AppendChild($Xml.CreateElement("packageSourceMapping")) | Out-Null
|
||||
}
|
||||
$mapping = $Xml.configuration.packageSourceMapping
|
||||
|
||||
# Remove invalid packageSource nodes (missing key or empty key)
|
||||
$invalidNodes = $mapping.SelectNodes("packageSource[not(@key) or @key='']")
|
||||
if ($invalidNodes) {
|
||||
foreach ($node in $invalidNodes) {
|
||||
$mapping.RemoveChild($node) | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
# Add/Update Mapping Source
|
||||
$mappingSource = $mapping.SelectSingleNode("packageSource[@key='$Key']")
|
||||
if (-not $mappingSource) {
|
||||
$mappingSource = $Xml.CreateElement("packageSource")
|
||||
$mappingSource.SetAttribute("key", $Key)
|
||||
# Insert at top for priority
|
||||
if ($mapping.HasChildNodes) {
|
||||
$mapping.InsertBefore($mappingSource, $mapping.FirstChild) | Out-Null
|
||||
} else {
|
||||
$mapping.AppendChild($mappingSource) | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
# Double check and force attribute
|
||||
if (-not $mappingSource.HasAttribute("key")) {
|
||||
$mappingSource.SetAttribute("key", $Key)
|
||||
}
|
||||
|
||||
# Update Patterns
|
||||
# RemoveAll() removes all child nodes AND attributes, so we must re-set the key afterwards
|
||||
$mappingSource.RemoveAll()
|
||||
$mappingSource.SetAttribute("key", $Key)
|
||||
|
||||
foreach ($pattern in $Patterns) {
|
||||
$pkg = $Xml.CreateElement("package")
|
||||
$pkg.SetAttribute("pattern", $pattern)
|
||||
$mappingSource.AppendChild($pkg) | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
function Resolve-WinAppSdkSplitDependencies {
|
||||
Write-Host "Version $WinAppSDKVersion detected. Resolving split dependencies..."
|
||||
$installDir = Join-Path $rootPath "localpackages\output"
|
||||
New-Item -ItemType Directory -Path $installDir -Force | Out-Null
|
||||
|
||||
# Create a temporary nuget.config to avoid interference from the repo's config
|
||||
$tempConfig = Join-Path $env:TEMP "nuget_$(Get-Random).config"
|
||||
Set-Content -Path $tempConfig -Value "<?xml version='1.0' encoding='utf-8'?><configuration><packageSources><clear /><add key='TempSource' value='$sourceLink' /></packageSources></configuration>"
|
||||
|
||||
try {
|
||||
# Extract BuildTools version from Directory.Packages.props to ensure we have the required version
|
||||
$dirPackagesProps = Join-Path $rootPath "Directory.Packages.props"
|
||||
if (Test-Path $dirPackagesProps) {
|
||||
$propsContent = Get-Content $dirPackagesProps -Raw
|
||||
if ($propsContent -match '<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="([^"]+)"') {
|
||||
$buildToolsVersion = $Matches[1]
|
||||
Write-Host "Downloading Microsoft.Windows.SDK.BuildTools version $buildToolsVersion..."
|
||||
$nugetArgsBuildTools = "install Microsoft.Windows.SDK.BuildTools -Version $buildToolsVersion -ConfigFile $tempConfig -OutputDirectory $installDir -NonInteractive -NoCache"
|
||||
Invoke-Expression "nuget $nugetArgsBuildTools" | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
# Download package to inspect nuspec and keep it for the build
|
||||
$nugetArgs = "install Microsoft.WindowsAppSDK -Version $WinAppSDKVersion -ConfigFile $tempConfig -OutputDirectory $installDir -NonInteractive -NoCache"
|
||||
Invoke-Expression "nuget $nugetArgs" | Out-Null
|
||||
|
||||
# Parse dependencies from the installed folders
|
||||
# Folder structure is typically {PackageId}.{Version}
|
||||
$directories = Get-ChildItem -Path $installDir -Directory
|
||||
$allLocalPackages = @()
|
||||
foreach ($dir in $directories) {
|
||||
# Match any package pattern: PackageId.Version
|
||||
if ($dir.Name -match "^(.+?)\.(\d+\..*)$") {
|
||||
$pkgId = $Matches[1]
|
||||
$pkgVer = $Matches[2]
|
||||
$allLocalPackages += $pkgId
|
||||
|
||||
$packageVersions[$pkgId] = $pkgVer
|
||||
Write-Host "Found dependency: $pkgId = $pkgVer"
|
||||
}
|
||||
}
|
||||
|
||||
# Update repo's nuget.config to use localpackages
|
||||
$nugetConfig = Join-Path $rootPath "nuget.config"
|
||||
$configData = Read-FileWithEncoding -Path $nugetConfig
|
||||
[xml]$xml = $configData.Content
|
||||
|
||||
Add-NuGetSourceAndMapping -Xml $xml -Key "localpackages" -Value $installDir -Patterns $allLocalPackages
|
||||
|
||||
$xml.Save($nugetConfig)
|
||||
Write-Host "Updated nuget.config with localpackages mapping."
|
||||
} catch {
|
||||
Write-Warning "Failed to resolve dependencies: $_"
|
||||
} finally {
|
||||
Remove-Item $tempConfig -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
# Execute nuget list and capture the output
|
||||
if ($useExperimentalVersion) {
|
||||
# The nuget list for experimental versions will cost more time
|
||||
@@ -112,56 +213,36 @@ if ($latestVersion) {
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Update packages.config files
|
||||
Get-ChildItem -Path $rootPath -Recurse packages.config | ForEach-Object {
|
||||
$file = Read-FileWithEncoding -Path $_.FullName
|
||||
$content = $file.Content
|
||||
if ($content -match 'package id="Microsoft.WindowsAppSDK"') {
|
||||
$newVersionString = 'package id="Microsoft.WindowsAppSDK" version="' + $WinAppSDKVersion + '"'
|
||||
$oldVersionString = 'package id="Microsoft.WindowsAppSDK" version="[-.0-9a-zA-Z]*"'
|
||||
$content = $content -replace $oldVersionString, $newVersionString
|
||||
Write-FileWithEncoding -Path $_.FullName -Content $content -Encoding $file.encoding
|
||||
Write-Host "Modified " $_.FullName
|
||||
}
|
||||
}
|
||||
# Resolve dependencies for 1.8+
|
||||
$packageVersions = @{ "Microsoft.WindowsAppSDK" = $WinAppSDKVersion }
|
||||
|
||||
Resolve-WinAppSdkSplitDependencies
|
||||
|
||||
# Update Directory.Packages.props file
|
||||
Get-ChildItem -Path $rootPath -Recurse "Directory.Packages.props" | ForEach-Object {
|
||||
$file = Read-FileWithEncoding -Path $_.FullName
|
||||
$content = $file.Content
|
||||
if ($content -match '<PackageVersion Include="Microsoft.WindowsAppSDK"') {
|
||||
$newVersionString = '<PackageVersion Include="Microsoft.WindowsAppSDK" Version="' + $WinAppSDKVersion + '" />'
|
||||
$oldVersionString = '<PackageVersion Include="Microsoft.WindowsAppSDK" Version="[-.0-9a-zA-Z]*" />'
|
||||
$content = $content -replace $oldVersionString, $newVersionString
|
||||
$isModified = $false
|
||||
|
||||
foreach ($pkgId in $packageVersions.Keys) {
|
||||
$ver = $packageVersions[$pkgId]
|
||||
# Escape dots in package ID for regex
|
||||
$pkgIdRegex = $pkgId -replace '\.', '\.'
|
||||
|
||||
$newVersionString = "<PackageVersion Include=""$pkgId"" Version=""$ver"" />"
|
||||
$oldVersionString = "<PackageVersion Include=""$pkgIdRegex"" Version=""[-.0-9a-zA-Z]*"" />"
|
||||
|
||||
if ($content -match "<PackageVersion Include=""$pkgIdRegex""") {
|
||||
# Update existing package
|
||||
if ($content -notmatch [regex]::Escape($newVersionString)) {
|
||||
$content = $content -replace $oldVersionString, $newVersionString
|
||||
$isModified = $true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($isModified) {
|
||||
Write-FileWithEncoding -Path $_.FullName -Content $content -Encoding $file.encoding
|
||||
Write-Host "Modified " $_.FullName
|
||||
}
|
||||
}
|
||||
|
||||
# Update .vcxproj files
|
||||
Get-ChildItem -Path $rootPath -Recurse *.vcxproj | ForEach-Object {
|
||||
$file = Read-FileWithEncoding -Path $_.FullName
|
||||
$content = $file.Content
|
||||
if ($content -match '\\Microsoft.WindowsAppSDK.') {
|
||||
$newVersionString = '\Microsoft.WindowsAppSDK.' + $WinAppSDKVersion
|
||||
$oldVersionString = '\\Microsoft.WindowsAppSDK.(?=[-.0-9a-zA-Z]*\d)[-.0-9a-zA-Z]*' #positive lookahead for at least a digit
|
||||
$content = $content -replace $oldVersionString, $newVersionString
|
||||
Write-FileWithEncoding -Path $_.FullName -Content $content -Encoding $file.encoding
|
||||
Write-Host "Modified " $_.FullName
|
||||
}
|
||||
}
|
||||
|
||||
# Update .csproj files
|
||||
Get-ChildItem -Path $rootPath -Recurse *.csproj | ForEach-Object {
|
||||
$file = Read-FileWithEncoding -Path $_.FullName
|
||||
$content = $file.Content
|
||||
if ($content -match 'PackageReference Include="Microsoft.WindowsAppSDK"') {
|
||||
$newVersionString = 'PackageReference Include="Microsoft.WindowsAppSDK" Version="'+ $WinAppSDKVersion + '"'
|
||||
$oldVersionString = 'PackageReference Include="Microsoft.WindowsAppSDK" Version="[-.0-9a-zA-Z]*"'
|
||||
$content = $content -replace $oldVersionString, $newVersionString
|
||||
Write-FileWithEncoding -Path $_.FullName -Content $content -Encoding $file.encoding
|
||||
Write-Host "Modified " $_.FullName
|
||||
}
|
||||
}
|
||||
|
||||
Update-NugetConfig
|
||||
|
||||
@@ -19,7 +19,7 @@ parameters:
|
||||
- name: enableMsBuildCaching
|
||||
type: boolean
|
||||
displayName: "Enable MSBuild Caching"
|
||||
default: true
|
||||
default: false
|
||||
- name: runTests
|
||||
type: boolean
|
||||
displayName: "Run Tests"
|
||||
@@ -33,7 +33,7 @@ parameters:
|
||||
default: true
|
||||
- name: winAppSDKVersionNumber
|
||||
type: string
|
||||
default: 1.7
|
||||
default: 1.8
|
||||
- name: useExperimentalVersion
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
@@ -129,7 +129,7 @@ jobs:
|
||||
MSBuildMainBuildTargets: Build
|
||||
${{ insert }}: ${{ parameters.variables }}
|
||||
${{ if eq(parameters.useLatestWinAppSDK, true) }}:
|
||||
RestoreAdditionalProjectSourcesArg: '/p:RestoreAdditionalProjectSources="$(Build.SourcesDirectory)\localpackages\NugetPackages"'
|
||||
RestoreAdditionalProjectSourcesArg: '/p:RestoreAdditionalProjectSources="$(Build.SourcesDirectory)\localpackages\NugetPackages" /p:IgnoreExperimentalWarnings=true'
|
||||
${{ else }}:
|
||||
RestoreAdditionalProjectSourcesArg: ''
|
||||
displayName: Build
|
||||
|
||||
@@ -19,48 +19,20 @@ steps:
|
||||
-useExperimentalVersion $${{ parameters.useExperimentalVersion }}
|
||||
-rootPath "$(build.sourcesdirectory)"
|
||||
|
||||
- script: echo $(WinAppSDKVersion)
|
||||
displayName: 'Display WinAppSDK Version Found'
|
||||
# - task: NuGetCommand@2
|
||||
# displayName: 'Restore NuGet packages (slnx)'
|
||||
# inputs:
|
||||
# command: 'restore'
|
||||
# feedsToUse: 'config'
|
||||
# nugetConfigPath: '$(build.sourcesdirectory)\nuget.config'
|
||||
# restoreSolution: '$(build.sourcesdirectory)\**\*.slnx'
|
||||
# includeNuGetOrg: false
|
||||
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: 'Download WindowsAppSDK'
|
||||
inputs:
|
||||
buildType: 'specific'
|
||||
project: '55e8140e-57ac-4e5f-8f9c-c7c15b51929d'
|
||||
definition: '104083'
|
||||
buildVersionToDownload: 'latestFromBranch'
|
||||
branchName: 'refs/heads/release/${{ parameters.versionNumber }}-stable'
|
||||
artifactName: 'WindowsAppSDK_Nuget_And_MSIX'
|
||||
targetPath: '$(Build.SourcesDirectory)\localpackages'
|
||||
|
||||
- script: dir $(Build.SourcesDirectory)\localpackages\NugetPackages
|
||||
displayName: 'List downloaded packages'
|
||||
|
||||
- task: NuGetCommand@2
|
||||
displayName: 'Install WindowsAppSDK'
|
||||
inputs:
|
||||
command: 'custom'
|
||||
arguments: >
|
||||
install "Microsoft.WindowsAppSDK"
|
||||
-Source "$(Build.SourcesDirectory)\localpackages\NugetPackages"
|
||||
-Version "$(WinAppSDKVersion)"
|
||||
-OutputDirectory "$(Build.SourcesDirectory)\localpackages\output"
|
||||
-FallbackSource "https://microsoft.pkgs.visualstudio.com/ProjectReunion/_packaging/Project.Reunion.nuget.internal/nuget/v3/index.json"
|
||||
|
||||
- task: NuGetCommand@2
|
||||
displayName: 'Restore NuGet packages'
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: 'Restore NuGet packages (dotnet)'
|
||||
inputs:
|
||||
command: 'restore'
|
||||
projects: '$(build.sourcesdirectory)\**\*.slnx'
|
||||
feedsToUse: 'config'
|
||||
nugetConfigPath: '$(build.sourcesdirectory)\nuget.config'
|
||||
restoreSolution: '$(build.sourcesdirectory)\**\*.sln'
|
||||
includeNuGetOrg: false
|
||||
|
||||
- task: NuGetCommand@2
|
||||
displayName: 'Restore NuGet packages (slnx)'
|
||||
inputs:
|
||||
command: 'restore'
|
||||
feedsToUse: 'config'
|
||||
nugetConfigPath: '$(build.sourcesdirectory)\nuget.config'
|
||||
restoreSolution: '$(build.sourcesdirectory)\**\*.slnx'
|
||||
includeNuGetOrg: false
|
||||
workingDirectory: '$(build.sourcesdirectory)'
|
||||
|
||||
@@ -27,7 +27,8 @@ $versionExceptions = @(
|
||||
"WyHash.dll",
|
||||
"Microsoft.Recognizers.Text.DataTypes.TimexExpression.dll",
|
||||
"ObjectModelCsProjection.dll",
|
||||
"RendererCsProjection.dll") -join '|';
|
||||
"RendererCsProjection.dll",
|
||||
"Microsoft.ML.OnnxRuntime.dll") -join '|';
|
||||
$nullVersionExceptions = @(
|
||||
"SkiaSharp.Views.WinUI.Native.dll",
|
||||
"libSkiaSharp.dll",
|
||||
@@ -52,7 +53,12 @@ $nullVersionExceptions = @(
|
||||
"System.Diagnostics.EventLog.Messages.dll",
|
||||
"Microsoft.Windows.Widgets.dll",
|
||||
"AdaptiveCards.ObjectModel.WinUI3.dll",
|
||||
"AdaptiveCards.Rendering.WinUI3.dll") -join '|';
|
||||
"AdaptiveCards.Rendering.WinUI3.dll",
|
||||
"boost_regex_vc143_mt_gd_x32_1_87.dll",
|
||||
"boost_regex_vc143_mt_gd_x64_1_87.dll",
|
||||
"boost_regex_vc143_mt_x32_1_87.dll",
|
||||
"boost_regex_vc143_mt_x64_1_87.dll"
|
||||
) -join '|';
|
||||
$totalFailure = 0;
|
||||
|
||||
Write-Host $DirPath;
|
||||
|
||||
@@ -121,6 +121,9 @@ PowerToys Awake is a tool to keep your computer awake.
|
||||
|
||||
Randy contributed Registry Preview and some very early conversations about keyboard remapping.
|
||||
|
||||
### [@cinnamon-msft](https://github.com/cinnamon-msft) - Kayla Cinnamon
|
||||
Kayla was a former lead for PowerToys and helped create multiple utilities, maintained the GitHub repo, and collaborated with the community to improve the overall product
|
||||
|
||||
### [@oldnewthing](https://github.com/oldnewthing) - Raymond Chen
|
||||
|
||||
Find My Mouse is based on Raymond Chen's SuperSonar.
|
||||
@@ -180,7 +183,6 @@ ZoomIt source code was originally implemented by [Sysinternals](https://sysinter
|
||||
|
||||
## PowerToys core team
|
||||
|
||||
- [@cinnamon-msft](https://github.com/cinnamon-msft) - Kayla Cinnamon - Lead
|
||||
- [@craigloewen-msft](https://github.com/craigloewen-msft) - Craig Loewen - Product Manager
|
||||
- [@niels9001](https://github.com/niels9001/) - Niels Laute - Product Manager
|
||||
- [@dhowett](https://github.com/dhowett) - Dustin Howett - Dev Lead
|
||||
@@ -209,6 +211,7 @@ ZoomIt source code was originally implemented by [Sysinternals](https://sysinter
|
||||
## Former PowerToys core team members
|
||||
|
||||
- [@indierawk2k2](https://github.com/indierawk2k2) - Mike Harsh - Product Manager
|
||||
- [@cinnamon-msft](https://github.com/cinnamon-msft) - Kayla Cinnamon - Product Manager
|
||||
- [@ethanfangg](https://github.com/ethanfangg) - Ethan Fang - Product Manager
|
||||
- [@plante-msft](https://github.com/plante-msft) - Connor Plante - Product Manager
|
||||
- [@joadoumie](https://github.com/joadoumie) - Jordi Adoumie - Product Manager
|
||||
|
||||
@@ -262,6 +262,7 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
</table>
|
||||
|
||||
### Command Palette
|
||||
|
||||
<table style="width:100%">
|
||||
<tr>
|
||||
<th>Event Name</th>
|
||||
@@ -315,6 +316,14 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
<td>Microsoft.PowerToys.CmdPalProcessStarted</td>
|
||||
<td>Triggered when the Command Palette process is started.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.CmdPal_ExtensionInvoked</td>
|
||||
<td>Tracks extension usage including extension ID, command details, success status, and execution time.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.CmdPal_SessionDuration</td>
|
||||
<td>Logs session metrics from launch to dismissal including duration, commands executed, pages visited, search queries, navigation depth, and errors.</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
### Crop And Lock
|
||||
|
||||
@@ -8,4 +8,24 @@
|
||||
<PropertyGroup Label="ManifestToolOverride">
|
||||
<ManifestTool Condition="Exists('$(WindowsSdkDir)bin\x64\mt.exe')">$(WindowsSdkDir)bin\x64\mt.exe</ManifestTool>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Auto-restore NuGet for native vcxproj (PackageReference) when building inside VS -->
|
||||
<Target Name="EnsureNuGetRestoreForVcxproj" BeforeTargets="PrepareForBuild" Condition="
|
||||
'$(BuildingInsideVisualStudio)' == 'true'
|
||||
and '$(DesignTimeBuild)' != 'true'
|
||||
and '$(RestoreInProgress)' != 'true'
|
||||
and '$(MSBuildProjectExtension)' == '.vcxproj'
|
||||
and '$(RestoreProjectStyle)' == 'PackageReference'
|
||||
and '$(MSBuildProjectExtensionsPath)' != ''
|
||||
and !Exists('$(MSBuildProjectExtensionsPath)project.assets.json')
|
||||
">
|
||||
|
||||
<Message Importance="normal" Text="NuGet assets missing for $(MSBuildProjectName); running Restore...; IntDir=$(IntDir); BaseIntermediateOutputPath=$(BaseIntermediateOutputPath)" />
|
||||
|
||||
<MSBuild Projects="$(MSBuildProjectFullPath)" Targets="Restore" Properties="RestoreInProgress=true" BuildInParallel="false" />
|
||||
</Target>
|
||||
|
||||
<PropertyGroup Condition="'$(IgnoreExperimentalWarnings)' == 'true'">
|
||||
<NoWarn>$(NoWarn);CS8305;SA1500;CA1852</NoWarn>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -40,6 +40,7 @@
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.10" />
|
||||
<!-- Including Microsoft.Bcl.AsyncInterfaces to force version, since it's used by Microsoft.SemanticKernel. -->
|
||||
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Graphics.Win2D" Version="1.3.2" />
|
||||
<PackageVersion Include="Microsoft.Windows.CppWinRT" Version="2.0.240111.5" />
|
||||
<PackageVersion Include="Microsoft.Diagnostics.Tracing.TraceEvent" Version="3.1.16" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI" Version="9.9.1" />
|
||||
|
||||
@@ -976,6 +976,14 @@
|
||||
<Project Path="src/modules/ZoomIt/ZoomItSettingsInterop/ZoomItSettingsInterop.vcxproj" Id="ca7d8106-30b9-4aec-9d05-b69b31b8c461" />
|
||||
</Folder>
|
||||
<Folder Name="/settings-ui/">
|
||||
<Project Path="src/settings-ui/QuickAccess.UI/PowerToys.QuickAccess.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
<Project Path="src/settings-ui/Settings.UI.Controls/Settings.UI.Controls.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
<Project Path="src/settings-ui/Settings.UI.Library/Settings.UI.Library.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
<Custom Action="SetBundleInstallLocation" After="InstallFiles" Condition="NOT Installed OR WIX_UPGRADE_DETECTED" />
|
||||
<Custom Action="ApplyModulesRegistryChangeSets" After="InstallFiles" Condition="NOT Installed" />
|
||||
<Custom Action="InstallCmdPalPackage" After="InstallFiles" Condition="NOT Installed" />
|
||||
<Custom Action="InstallPackageIdentityMSIX" After="InstallFiles" Condition="NOT Installed" />
|
||||
<Custom Action="InstallPackageIdentityMSIX" After="InstallFiles" Condition="NOT Installed AND WINDOWSBUILDNUMBER >= 22000" />
|
||||
<Custom Action="override Wix4CloseApplications_$(sys.BUILDARCHSHORT)" Before="RemoveFiles" />
|
||||
<Custom Action="RemovePowerToysSchTasks" After="RemoveFiles" />
|
||||
<!-- TODO: Use to activate embedded MSIX -->
|
||||
|
||||
@@ -36,5 +36,6 @@ namespace ManagedCommon
|
||||
PowerOCR,
|
||||
Workspaces,
|
||||
ZoomIt,
|
||||
GeneralSettings,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,4 +16,5 @@ public static partial class CLSID
|
||||
public static readonly Guid CollatorDataSource = new Guid("9E175B8B-F52A-11D8-B9A5-505054503030");
|
||||
public static readonly Guid ApplicationActivationManager = new Guid("45BA127D-10A8-46EA-8AB7-56EA9078943C");
|
||||
public static readonly Guid VirtualDesktopManager = new("aa509086-5ca9-4c25-8f95-589d3c07b48a");
|
||||
public static readonly Guid DesktopWallpaper = new("C2CF3110-460E-4FC1-B9D0-8A1C0C9CC4BD");
|
||||
}
|
||||
|
||||
@@ -16,6 +16,12 @@ public static partial class Ole32
|
||||
CLSCTX dwClsContext,
|
||||
ref Guid riid,
|
||||
out IntPtr rReturnedComObject);
|
||||
|
||||
[LibraryImport("ole32.dll")]
|
||||
internal static partial int CoInitializeEx(nint pvReserved, uint dwCoInit);
|
||||
|
||||
[LibraryImport("ole32.dll")]
|
||||
internal static partial void CoUninitialize();
|
||||
}
|
||||
|
||||
[Flags]
|
||||
|
||||
@@ -119,6 +119,16 @@ namespace PowerToysSettings
|
||||
class HotkeyObject
|
||||
{
|
||||
public:
|
||||
HotkeyObject() :
|
||||
m_json(json::JsonObject())
|
||||
{
|
||||
m_json.SetNamedValue(L"win", json::value(false));
|
||||
m_json.SetNamedValue(L"ctrl", json::value(false));
|
||||
m_json.SetNamedValue(L"alt", json::value(false));
|
||||
m_json.SetNamedValue(L"shift", json::value(false));
|
||||
m_json.SetNamedValue(L"code", json::value(0));
|
||||
m_json.SetNamedValue(L"key", json::value(L""));
|
||||
}
|
||||
static HotkeyObject from_json(json::JsonObject json)
|
||||
{
|
||||
return HotkeyObject(std::move(json));
|
||||
|
||||
399
src/common/UITestAutomation/ScreenRecording.cs
Normal file
399
src/common/UITestAutomation/ScreenRecording.cs
Normal file
@@ -0,0 +1,399 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Imaging;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.PowerToys.UITest
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides methods for recording the screen during UI tests.
|
||||
/// Requires FFmpeg to be installed and available in PATH.
|
||||
/// </summary>
|
||||
internal class ScreenRecording : IDisposable
|
||||
{
|
||||
private readonly string outputDirectory;
|
||||
private readonly string framesDirectory;
|
||||
private readonly string outputFilePath;
|
||||
private readonly List<string> capturedFrames;
|
||||
private readonly SemaphoreSlim recordingLock = new(1, 1);
|
||||
private readonly Stopwatch recordingStopwatch = new();
|
||||
private readonly string? ffmpegPath;
|
||||
private CancellationTokenSource? recordingCancellation;
|
||||
private Task? recordingTask;
|
||||
private bool isRecording;
|
||||
private int frameCount;
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr GetDC(IntPtr hWnd);
|
||||
|
||||
[DllImport("gdi32.dll")]
|
||||
private static extern int GetDeviceCaps(IntPtr hdc, int nIndex);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr ReleaseDC(IntPtr hWnd, IntPtr hDC);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool GetCursorInfo(out ScreenCapture.CURSORINFO pci);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool DrawIconEx(IntPtr hdc, int x, int y, IntPtr hIcon, int cx, int cy, int istepIfAniCur, IntPtr hbrFlickerFreeDraw, int diFlags);
|
||||
|
||||
private const int CURSORSHOWING = 0x00000001;
|
||||
private const int DESKTOPHORZRES = 118;
|
||||
private const int DESKTOPVERTRES = 117;
|
||||
private const int DINORMAL = 0x0003;
|
||||
private const int TargetFps = 15; // 15 FPS for good balance of quality and size
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ScreenRecording"/> class.
|
||||
/// </summary>
|
||||
/// <param name="outputDirectory">Directory where the recording will be saved.</param>
|
||||
public ScreenRecording(string outputDirectory)
|
||||
{
|
||||
this.outputDirectory = outputDirectory;
|
||||
string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
|
||||
framesDirectory = Path.Combine(outputDirectory, $"frames_{timestamp}");
|
||||
outputFilePath = Path.Combine(outputDirectory, $"recording_{timestamp}.mp4");
|
||||
capturedFrames = new List<string>();
|
||||
frameCount = 0;
|
||||
|
||||
// Check if FFmpeg is available
|
||||
ffmpegPath = FindFfmpeg();
|
||||
if (ffmpegPath == null)
|
||||
{
|
||||
Console.WriteLine("FFmpeg not found. Screen recording will be disabled.");
|
||||
Console.WriteLine("To enable video recording, install FFmpeg: https://ffmpeg.org/download.html");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether screen recording is available (FFmpeg found).
|
||||
/// </summary>
|
||||
public bool IsAvailable => ffmpegPath != null;
|
||||
|
||||
/// <summary>
|
||||
/// Starts recording the screen.
|
||||
/// </summary>
|
||||
/// <returns>A task representing the asynchronous operation.</returns>
|
||||
public async Task StartRecordingAsync()
|
||||
{
|
||||
await recordingLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
if (isRecording || !IsAvailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Create frames directory
|
||||
Directory.CreateDirectory(framesDirectory);
|
||||
|
||||
recordingCancellation = new CancellationTokenSource();
|
||||
isRecording = true;
|
||||
recordingStopwatch.Start();
|
||||
|
||||
// Start the recording task
|
||||
recordingTask = Task.Run(() => RecordFrames(recordingCancellation.Token));
|
||||
|
||||
Console.WriteLine($"Started screen recording at {TargetFps} FPS");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Failed to start recording: {ex.Message}");
|
||||
isRecording = false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
recordingLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops recording and encodes video.
|
||||
/// </summary>
|
||||
/// <returns>A task representing the asynchronous operation.</returns>
|
||||
public async Task StopRecordingAsync()
|
||||
{
|
||||
await recordingLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
if (!isRecording || recordingCancellation == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Signal cancellation
|
||||
recordingCancellation.Cancel();
|
||||
|
||||
// Wait for recording task to complete
|
||||
if (recordingTask != null)
|
||||
{
|
||||
await recordingTask;
|
||||
}
|
||||
|
||||
recordingStopwatch.Stop();
|
||||
isRecording = false;
|
||||
|
||||
double duration = recordingStopwatch.Elapsed.TotalSeconds;
|
||||
Console.WriteLine($"Recording stopped. Captured {capturedFrames.Count} frames in {duration:F2} seconds");
|
||||
|
||||
// Encode to video
|
||||
await EncodeToVideoAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error stopping recording: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Cleanup();
|
||||
recordingLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records frames from the screen.
|
||||
/// </summary>
|
||||
private void RecordFrames(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
int frameInterval = 1000 / TargetFps;
|
||||
var frameTimer = Stopwatch.StartNew();
|
||||
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
var frameStart = frameTimer.ElapsedMilliseconds;
|
||||
|
||||
try
|
||||
{
|
||||
CaptureFrame();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error capturing frame: {ex.Message}");
|
||||
}
|
||||
|
||||
// Sleep for remaining time to maintain target FPS
|
||||
var frameTime = frameTimer.ElapsedMilliseconds - frameStart;
|
||||
var sleepTime = Math.Max(0, frameInterval - (int)frameTime);
|
||||
|
||||
if (sleepTime > 0)
|
||||
{
|
||||
Thread.Sleep(sleepTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Expected when stopping
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error during recording: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Captures a single frame.
|
||||
/// </summary>
|
||||
private void CaptureFrame()
|
||||
{
|
||||
IntPtr hdc = GetDC(IntPtr.Zero);
|
||||
int screenWidth = GetDeviceCaps(hdc, DESKTOPHORZRES);
|
||||
int screenHeight = GetDeviceCaps(hdc, DESKTOPVERTRES);
|
||||
ReleaseDC(IntPtr.Zero, hdc);
|
||||
|
||||
Rectangle bounds = new Rectangle(0, 0, screenWidth, screenHeight);
|
||||
using (Bitmap bitmap = new Bitmap(bounds.Width, bounds.Height, PixelFormat.Format24bppRgb))
|
||||
{
|
||||
using (Graphics g = Graphics.FromImage(bitmap))
|
||||
{
|
||||
g.CopyFromScreen(bounds.Location, Point.Empty, bounds.Size);
|
||||
|
||||
ScreenCapture.CURSORINFO cursorInfo;
|
||||
cursorInfo.CbSize = Marshal.SizeOf<ScreenCapture.CURSORINFO>();
|
||||
if (GetCursorInfo(out cursorInfo) && cursorInfo.Flags == CURSORSHOWING)
|
||||
{
|
||||
IntPtr hdcDest = g.GetHdc();
|
||||
DrawIconEx(hdcDest, cursorInfo.PTScreenPos.X, cursorInfo.PTScreenPos.Y, cursorInfo.HCursor, 0, 0, 0, IntPtr.Zero, DINORMAL);
|
||||
g.ReleaseHdc(hdcDest);
|
||||
}
|
||||
}
|
||||
|
||||
string framePath = Path.Combine(framesDirectory, $"frame_{frameCount:D6}.jpg");
|
||||
bitmap.Save(framePath, ImageFormat.Jpeg);
|
||||
capturedFrames.Add(framePath);
|
||||
frameCount++;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encodes captured frames to video using ffmpeg.
|
||||
/// </summary>
|
||||
private async Task EncodeToVideoAsync()
|
||||
{
|
||||
if (capturedFrames.Count == 0)
|
||||
{
|
||||
Console.WriteLine("No frames captured");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Build ffmpeg command with proper non-interactive flags
|
||||
string inputPattern = Path.Combine(framesDirectory, "frame_%06d.jpg");
|
||||
|
||||
// -y: overwrite without asking
|
||||
// -nostdin: disable interaction
|
||||
// -loglevel error: only show errors
|
||||
// -stats: show encoding progress
|
||||
string args = $"-y -nostdin -loglevel error -stats -framerate {TargetFps} -i \"{inputPattern}\" -c:v libx264 -pix_fmt yuv420p -crf 23 \"{outputFilePath}\"";
|
||||
|
||||
Console.WriteLine($"Encoding {capturedFrames.Count} frames to video...");
|
||||
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = ffmpegPath!,
|
||||
Arguments = args,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
RedirectStandardInput = true, // Important: redirect stdin to prevent hanging
|
||||
CreateNoWindow = true,
|
||||
};
|
||||
|
||||
using var process = Process.Start(startInfo);
|
||||
if (process != null)
|
||||
{
|
||||
// Close stdin immediately to ensure FFmpeg doesn't wait for input
|
||||
process.StandardInput.Close();
|
||||
|
||||
// Read output streams asynchronously to prevent deadlock
|
||||
var outputTask = process.StandardOutput.ReadToEndAsync();
|
||||
var errorTask = process.StandardError.ReadToEndAsync();
|
||||
|
||||
// Wait for process to exit
|
||||
await process.WaitForExitAsync();
|
||||
|
||||
// Get the output
|
||||
string stdout = await outputTask;
|
||||
string stderr = await errorTask;
|
||||
|
||||
if (process.ExitCode == 0 && File.Exists(outputFilePath))
|
||||
{
|
||||
var fileInfo = new FileInfo(outputFilePath);
|
||||
Console.WriteLine($"Video created: {outputFilePath} ({fileInfo.Length / 1024 / 1024:F1} MB)");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"FFmpeg encoding failed with exit code {process.ExitCode}");
|
||||
if (!string.IsNullOrWhiteSpace(stderr))
|
||||
{
|
||||
Console.WriteLine($"FFmpeg error: {stderr}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error encoding video: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds ffmpeg executable.
|
||||
/// </summary>
|
||||
private static string? FindFfmpeg()
|
||||
{
|
||||
// Check if ffmpeg is in PATH
|
||||
var pathDirs = Environment.GetEnvironmentVariable("PATH")?.Split(Path.PathSeparator) ?? Array.Empty<string>();
|
||||
|
||||
foreach (var dir in pathDirs)
|
||||
{
|
||||
var ffmpegPath = Path.Combine(dir, "ffmpeg.exe");
|
||||
if (File.Exists(ffmpegPath))
|
||||
{
|
||||
return ffmpegPath;
|
||||
}
|
||||
}
|
||||
|
||||
// Check common installation locations
|
||||
var commonPaths = new[]
|
||||
{
|
||||
@"C:\.tools\ffmpeg\bin\ffmpeg.exe",
|
||||
@"C:\ffmpeg\bin\ffmpeg.exe",
|
||||
@"C:\Program Files\ffmpeg\bin\ffmpeg.exe",
|
||||
@"C:\Program Files (x86)\ffmpeg\bin\ffmpeg.exe",
|
||||
@$"{Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)}\Microsoft\WinGet\Links\ffmpeg.exe",
|
||||
};
|
||||
|
||||
foreach (var path in commonPaths)
|
||||
{
|
||||
if (File.Exists(path))
|
||||
{
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path to the recorded video file.
|
||||
/// </summary>
|
||||
public string OutputFilePath => outputFilePath;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the directory containing recordings.
|
||||
/// </summary>
|
||||
public string OutputDirectory => outputDirectory;
|
||||
|
||||
/// <summary>
|
||||
/// Cleans up resources.
|
||||
/// </summary>
|
||||
private void Cleanup()
|
||||
{
|
||||
recordingCancellation?.Dispose();
|
||||
recordingCancellation = null;
|
||||
recordingTask = null;
|
||||
|
||||
// Clean up frames directory if it exists
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(framesDirectory))
|
||||
{
|
||||
Directory.Delete(framesDirectory, true);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Failed to cleanup frames directory: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (isRecording)
|
||||
{
|
||||
StopRecordingAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
Cleanup();
|
||||
recordingLock.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -130,9 +130,13 @@ namespace Microsoft.PowerToys.UITest
|
||||
/// </summary>
|
||||
/// <param name="appPath">The path to the application executable.</param>
|
||||
/// <param name="args">Optional command line arguments to pass to the application.</param>
|
||||
public void StartExe(string appPath, string[]? args = null)
|
||||
public void StartExe(string appPath, string[]? args = null, string? enableModules = null)
|
||||
{
|
||||
var opts = new AppiumOptions();
|
||||
if (!string.IsNullOrEmpty(enableModules))
|
||||
{
|
||||
opts.AddAdditionalCapability("enableModules", enableModules);
|
||||
}
|
||||
|
||||
if (scope == PowerToysModule.PowerToysSettings)
|
||||
{
|
||||
@@ -169,27 +173,66 @@ namespace Microsoft.PowerToys.UITest
|
||||
|
||||
private void TryLaunchPowerToysSettings(AppiumOptions opts)
|
||||
{
|
||||
try
|
||||
if (opts.ToCapabilities().HasCapability("enableModules"))
|
||||
{
|
||||
var runnerProcessInfo = new ProcessStartInfo
|
||||
var modulesString = (string)opts.ToCapabilities().GetCapability("enableModules");
|
||||
var modulesArray = modulesString.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
SettingsConfigHelper.ConfigureGlobalModuleSettings(modulesArray);
|
||||
}
|
||||
else
|
||||
{
|
||||
SettingsConfigHelper.ConfigureGlobalModuleSettings();
|
||||
}
|
||||
|
||||
const int maxTries = 3;
|
||||
const int delayMs = 5000;
|
||||
const int maxRetries = 3;
|
||||
|
||||
for (int tryCount = 1; tryCount <= maxTries; tryCount++)
|
||||
{
|
||||
try
|
||||
{
|
||||
FileName = locationPath + runnerPath,
|
||||
Verb = "runas",
|
||||
Arguments = "--open-settings",
|
||||
};
|
||||
var runnerProcessInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = locationPath + runnerPath,
|
||||
Verb = "runas",
|
||||
Arguments = "--open-settings",
|
||||
};
|
||||
|
||||
ExitExe(runnerProcessInfo.FileName);
|
||||
runner = Process.Start(runnerProcessInfo);
|
||||
ExitExe(runnerProcessInfo.FileName);
|
||||
|
||||
WaitForWindowAndSetCapability(opts, "PowerToys Settings", 5000, 5);
|
||||
// Verify process was killed
|
||||
string exeName = Path.GetFileNameWithoutExtension(runnerProcessInfo.FileName);
|
||||
var remainingProcesses = Process.GetProcessesByName(exeName);
|
||||
|
||||
// Exit CmdPal UI before launching new process if use installer for test
|
||||
ExitExeByName("Microsoft.CmdPal.UI");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to launch PowerToys Settings: {ex.Message}", ex);
|
||||
runner = Process.Start(runnerProcessInfo);
|
||||
|
||||
if (WaitForWindowAndSetCapability(opts, "PowerToys Settings", delayMs, maxRetries))
|
||||
{
|
||||
// Exit CmdPal UI before launching new process if use installer for test
|
||||
ExitExeByName("Microsoft.CmdPal.UI");
|
||||
return;
|
||||
}
|
||||
|
||||
// Window not found, kill all PowerToys processes and retry
|
||||
if (tryCount < maxTries)
|
||||
{
|
||||
KillPowerToysProcesses();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (tryCount == maxTries)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to launch PowerToys Settings after {maxTries} attempts: {ex.Message}", ex);
|
||||
}
|
||||
|
||||
// Kill processes and retry
|
||||
KillPowerToysProcesses();
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Failed to launch PowerToys Settings: Window not found after {maxTries} attempts.");
|
||||
}
|
||||
|
||||
private void TryLaunchCommandPalette(AppiumOptions opts)
|
||||
@@ -211,7 +254,10 @@ namespace Microsoft.PowerToys.UITest
|
||||
var process = Process.Start(processStartInfo);
|
||||
process?.WaitForExit();
|
||||
|
||||
WaitForWindowAndSetCapability(opts, "Command Palette", 5000, 10);
|
||||
if (!WaitForWindowAndSetCapability(opts, "Command Palette", 5000, 10))
|
||||
{
|
||||
throw new TimeoutException("Failed to find Command Palette window after multiple attempts.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -219,7 +265,7 @@ namespace Microsoft.PowerToys.UITest
|
||||
}
|
||||
}
|
||||
|
||||
private void WaitForWindowAndSetCapability(AppiumOptions opts, string windowName, int delayMs, int maxRetries)
|
||||
private bool WaitForWindowAndSetCapability(AppiumOptions opts, string windowName, int delayMs, int maxRetries)
|
||||
{
|
||||
for (int attempt = 1; attempt <= maxRetries; attempt++)
|
||||
{
|
||||
@@ -230,18 +276,16 @@ namespace Microsoft.PowerToys.UITest
|
||||
{
|
||||
var hexHwnd = window[0].HWnd.ToString("x");
|
||||
opts.AddAdditionalCapability("appTopLevelWindow", hexHwnd);
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (attempt < maxRetries)
|
||||
{
|
||||
Thread.Sleep(delayMs);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new TimeoutException($"Failed to find {windowName} window after multiple attempts.");
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -292,17 +336,17 @@ namespace Microsoft.PowerToys.UITest
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Handle exceptions if needed
|
||||
Debug.WriteLine($"Exception during Cleanup: {ex.Message}");
|
||||
Console.WriteLine($"Exception during Cleanup: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restarts now exe and takes control of it.
|
||||
/// </summary>
|
||||
public void RestartScopeExe()
|
||||
public void RestartScopeExe(string? enableModules = null)
|
||||
{
|
||||
ExitScopeExe();
|
||||
StartExe(locationPath + sessionPath, this.commandLineArgs);
|
||||
StartExe(locationPath + sessionPath, commandLineArgs, enableModules);
|
||||
}
|
||||
|
||||
public WindowsDriver<WindowsElement> GetRoot()
|
||||
@@ -327,5 +371,31 @@ namespace Microsoft.PowerToys.UITest
|
||||
this.ExitExe(winAppDriverProcessInfo.FileName);
|
||||
SessionHelper.appDriver = Process.Start(winAppDriverProcessInfo);
|
||||
}
|
||||
|
||||
private void KillPowerToysProcesses()
|
||||
{
|
||||
var powerToysProcessNames = new[] { "PowerToys", "Microsoft.CmdPal.UI" };
|
||||
|
||||
foreach (var processName in powerToysProcessNames)
|
||||
{
|
||||
try
|
||||
{
|
||||
var processes = Process.GetProcessesByName(processName);
|
||||
|
||||
foreach (var process in processes)
|
||||
{
|
||||
process.Kill();
|
||||
process.WaitForExit();
|
||||
}
|
||||
|
||||
// Verify processes are actually gone
|
||||
var remainingProcesses = Process.GetProcessesByName(processName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[KillPowerToysProcesses] Failed to kill process {processName}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,14 +26,13 @@ namespace Microsoft.PowerToys.UITest
|
||||
/// <summary>
|
||||
/// Configures global PowerToys settings to enable only specified modules and disable all others.
|
||||
/// </summary>
|
||||
/// <param name="modulesToEnable">Array of module names to enable (e.g., "Peek", "FancyZones"). All other modules will be disabled.</param>
|
||||
/// <exception cref="ArgumentNullException">Thrown when modulesToEnable is null.</exception>
|
||||
/// <param name="modulesToEnable">Array of module names to enable (e.g., "Peek", "FancyZones"). All other modules will be disabled. If null or empty, all modules will be disabled.</param>
|
||||
/// <exception cref="InvalidOperationException">Thrown when settings file operations fail.</exception>
|
||||
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "This is test code and will not be trimmed")]
|
||||
[UnconditionalSuppressMessage("AOT", "IL3050", Justification = "This is test code and will not be AOT compiled")]
|
||||
public static void ConfigureGlobalModuleSettings(params string[] modulesToEnable)
|
||||
public static void ConfigureGlobalModuleSettings(params string[]? modulesToEnable)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(modulesToEnable);
|
||||
modulesToEnable ??= Array.Empty<string>();
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
@@ -29,6 +29,8 @@ namespace Microsoft.PowerToys.UITest
|
||||
|
||||
public string? ScreenshotDirectory { get; set; }
|
||||
|
||||
public string? RecordingDirectory { get; set; }
|
||||
|
||||
public static MonitorInfoData.ParamsWrapper MonitorInfoData { get; set; } = new MonitorInfoData.ParamsWrapper() { Monitors = new List<MonitorInfoData.MonitorInfoDataWrapper>() };
|
||||
|
||||
private readonly PowerToysModule scope;
|
||||
@@ -36,6 +38,7 @@ namespace Microsoft.PowerToys.UITest
|
||||
private readonly string[]? commandLineArgs;
|
||||
private SessionHelper? sessionHelper;
|
||||
private System.Threading.Timer? screenshotTimer;
|
||||
private ScreenRecording? screenRecording;
|
||||
|
||||
public UITestBase(PowerToysModule scope = PowerToysModule.PowerToysSettings, WindowSize size = WindowSize.UnSpecified, string[]? commandLineArgs = null)
|
||||
{
|
||||
@@ -65,12 +68,35 @@ namespace Microsoft.PowerToys.UITest
|
||||
CloseOtherApplications();
|
||||
if (IsInPipeline)
|
||||
{
|
||||
ScreenshotDirectory = Path.Combine(this.TestContext.TestResultsDirectory ?? string.Empty, "UITestScreenshots_" + Guid.NewGuid().ToString());
|
||||
string baseDirectory = this.TestContext.TestResultsDirectory ?? string.Empty;
|
||||
ScreenshotDirectory = Path.Combine(baseDirectory, "UITestScreenshots_" + Guid.NewGuid().ToString());
|
||||
Directory.CreateDirectory(ScreenshotDirectory);
|
||||
|
||||
RecordingDirectory = Path.Combine(baseDirectory, "UITestRecordings_" + Guid.NewGuid().ToString());
|
||||
Directory.CreateDirectory(RecordingDirectory);
|
||||
|
||||
// Take screenshot every 1 second
|
||||
screenshotTimer = new System.Threading.Timer(ScreenCapture.TimerCallback, ScreenshotDirectory, TimeSpan.Zero, TimeSpan.FromMilliseconds(1000));
|
||||
|
||||
// Start screen recording (requires FFmpeg)
|
||||
try
|
||||
{
|
||||
screenRecording = new ScreenRecording(RecordingDirectory);
|
||||
if (screenRecording.IsAvailable)
|
||||
{
|
||||
_ = screenRecording.StartRecordingAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
screenRecording = null;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Failed to start screen recording: {ex.Message}");
|
||||
screenRecording = null;
|
||||
}
|
||||
|
||||
// Escape Popups before starting
|
||||
System.Windows.Forms.SendKeys.SendWait("{ESC}");
|
||||
}
|
||||
@@ -88,15 +114,36 @@ namespace Microsoft.PowerToys.UITest
|
||||
if (IsInPipeline)
|
||||
{
|
||||
screenshotTimer?.Change(Timeout.Infinite, Timeout.Infinite);
|
||||
Dispose();
|
||||
|
||||
// Stop screen recording
|
||||
if (screenRecording != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
screenRecording.StopRecordingAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Failed to stop screen recording: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
if (TestContext.CurrentTestOutcome is UnitTestOutcome.Failed
|
||||
or UnitTestOutcome.Error
|
||||
or UnitTestOutcome.Unknown)
|
||||
{
|
||||
Task.Delay(1000).Wait();
|
||||
AddScreenShotsToTestResultsDirectory();
|
||||
AddRecordingsToTestResultsDirectory();
|
||||
AddLogFilesToTestResultsDirectory();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Clean up recording if test passed
|
||||
CleanupRecordingDirectory();
|
||||
}
|
||||
|
||||
Dispose();
|
||||
}
|
||||
|
||||
this.Session.Cleanup();
|
||||
@@ -106,6 +153,7 @@ namespace Microsoft.PowerToys.UITest
|
||||
public void Dispose()
|
||||
{
|
||||
screenshotTimer?.Dispose();
|
||||
screenRecording?.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
@@ -600,6 +648,47 @@ namespace Microsoft.PowerToys.UITest
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds screen recordings to test results directory when test fails.
|
||||
/// </summary>
|
||||
protected void AddRecordingsToTestResultsDirectory()
|
||||
{
|
||||
if (RecordingDirectory != null && Directory.Exists(RecordingDirectory))
|
||||
{
|
||||
// Add video files (MP4)
|
||||
var videoFiles = Directory.GetFiles(RecordingDirectory, "*.mp4");
|
||||
foreach (string file in videoFiles)
|
||||
{
|
||||
this.TestContext.AddResultFile(file);
|
||||
var fileInfo = new FileInfo(file);
|
||||
Console.WriteLine($"Added video recording: {Path.GetFileName(file)} ({fileInfo.Length / 1024 / 1024:F1} MB)");
|
||||
}
|
||||
|
||||
if (videoFiles.Length == 0)
|
||||
{
|
||||
Console.WriteLine("No video recording available (FFmpeg not found). Screenshots are still captured.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cleans up recording directory when test passes.
|
||||
/// </summary>
|
||||
private void CleanupRecordingDirectory()
|
||||
{
|
||||
if (RecordingDirectory != null && Directory.Exists(RecordingDirectory))
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.Delete(RecordingDirectory, true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Failed to cleanup recording directory: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies PowerToys log files to test results directory when test fails.
|
||||
/// Renames files to include the directory structure after \PowerToys.
|
||||
@@ -689,11 +778,11 @@ namespace Microsoft.PowerToys.UITest
|
||||
/// <summary>
|
||||
/// Restart scope exe.
|
||||
/// </summary>
|
||||
public void RestartScopeExe()
|
||||
public Session RestartScopeExe(string? enableModules = null)
|
||||
{
|
||||
this.sessionHelper!.RestartScopeExe();
|
||||
this.sessionHelper!.RestartScopeExe(enableModules);
|
||||
this.Session = new Session(this.sessionHelper.GetRoot(), this.sessionHelper.GetDriver(), this.scope, this.size);
|
||||
return;
|
||||
return Session;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -144,7 +144,7 @@ public sealed class AIServiceBatchIntegrationTests
|
||||
switch (format)
|
||||
{
|
||||
case PasteFormats.CustomTextTransformation:
|
||||
var transformResult = await services.CustomActionTransformService.TransformTextAsync(batchTestInput.Prompt, batchTestInput.Clipboard, CancellationToken.None, progress);
|
||||
var transformResult = await services.CustomActionTransformService.TransformAsync(batchTestInput.Prompt, batchTestInput.Clipboard, null, CancellationToken.None, progress);
|
||||
return DataPackageHelpers.CreateFromText(transformResult.Content ?? string.Empty);
|
||||
|
||||
case PasteFormats.KernelQuery:
|
||||
|
||||
@@ -198,20 +198,14 @@ namespace AdvancedPaste.Pages
|
||||
}
|
||||
}
|
||||
|
||||
private async void ClipboardHistory_ItemInvoked(ItemsView sender, ItemsViewItemInvokedEventArgs args)
|
||||
private void ClipboardHistory_ItemInvoked(ItemsView sender, ItemsViewItemInvokedEventArgs args)
|
||||
{
|
||||
if (args.InvokedItem is ClipboardItem item)
|
||||
if (args.InvokedItem is ClipboardItem item && item.Item is not null)
|
||||
{
|
||||
PowerToysTelemetry.Log.WriteEvent(new Telemetry.AdvancedPasteClipboardItemClicked());
|
||||
if (!string.IsNullOrEmpty(item.Content))
|
||||
{
|
||||
ClipboardHelper.SetTextContent(item.Content);
|
||||
}
|
||||
else if (item.Image is not null)
|
||||
{
|
||||
RandomAccessStreamReference image = await item.Item.Content.GetBitmapAsync();
|
||||
ClipboardHelper.SetImageContent(image);
|
||||
}
|
||||
|
||||
// Use SetHistoryItemAsContent to set the clipboard content without creating a new history entry
|
||||
Clipboard.SetHistoryItemAsContent(item.Item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,6 +225,24 @@ internal static class DataPackageHelpers
|
||||
internal static async Task<string> GetHtmlContentAsync(this DataPackageView dataPackageView) =>
|
||||
dataPackageView.Contains(StandardDataFormats.Html) ? await dataPackageView.GetHtmlFormatAsync() : string.Empty;
|
||||
|
||||
internal static async Task<byte[]> GetImageAsPngBytesAsync(this DataPackageView dataPackageView)
|
||||
{
|
||||
var bitmap = await dataPackageView.GetImageContentAsync();
|
||||
if (bitmap == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
using var pngStream = new InMemoryRandomAccessStream();
|
||||
var encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, pngStream);
|
||||
encoder.SetSoftwareBitmap(bitmap);
|
||||
await encoder.FlushAsync();
|
||||
|
||||
using var memoryStream = new MemoryStream();
|
||||
await pngStream.AsStreamForRead().CopyToAsync(memoryStream);
|
||||
return memoryStream.ToArray();
|
||||
}
|
||||
|
||||
internal static async Task<SoftwareBitmap> GetImageContentAsync(this DataPackageView dataPackageView)
|
||||
{
|
||||
using var stream = await dataPackageView.GetImageStreamAsync();
|
||||
|
||||
@@ -166,5 +166,8 @@ namespace AdvancedPaste.Helpers
|
||||
|
||||
[DllImport("Shlwapi.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
internal static extern HResult AssocQueryString(AssocF flags, AssocStr str, string pszAssoc, string pszExtra, [Out] StringBuilder pszOut, [In][Out] ref uint pcchOut);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
internal static extern uint GetClipboardSequenceNumber();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ public enum PasteFormats
|
||||
CanPreview = true,
|
||||
SupportedClipboardFormats = ClipboardFormat.Image,
|
||||
IPCKey = AdvancedPasteAdditionalActions.PropertyNames.ImageToText,
|
||||
KernelFunctionDescription = "Takes an image in the clipboard and extracts all text from it using OCR.")]
|
||||
KernelFunctionDescription = "Takes an image from the clipboard and extracts text using OCR. This function is intended only for explicit text extraction or OCR requests.")]
|
||||
ImageToText,
|
||||
|
||||
[PasteFormatMetadata(
|
||||
@@ -118,8 +118,8 @@ public enum PasteFormats
|
||||
IconGlyph = "\uE945",
|
||||
RequiresAIService = true,
|
||||
CanPreview = true,
|
||||
SupportedClipboardFormats = ClipboardFormat.Text,
|
||||
KernelFunctionDescription = "Takes input instructions and transforms clipboard text (not TXT files) with these input instructions, putting the result back on the clipboard. This uses AI to accomplish the task.",
|
||||
SupportedClipboardFormats = ClipboardFormat.Text | ClipboardFormat.Image,
|
||||
KernelFunctionDescription = "Takes user instructions and applies them to the current clipboard content (text or image). Use this function for image analysis, description, or transformation tasks beyond simple OCR.",
|
||||
RequiresPrompt = true)]
|
||||
CustomTextTransformation,
|
||||
}
|
||||
|
||||
@@ -40,15 +40,15 @@ namespace AdvancedPaste.Services.CustomActions
|
||||
this.userSettings = userSettings;
|
||||
}
|
||||
|
||||
public async Task<CustomActionTransformResult> TransformTextAsync(string prompt, string inputText, CancellationToken cancellationToken, IProgress<double> progress)
|
||||
public async Task<CustomActionTransformResult> TransformAsync(string prompt, string inputText, byte[] imageBytes, CancellationToken cancellationToken, IProgress<double> progress)
|
||||
{
|
||||
var pasteConfig = userSettings?.PasteAIConfiguration;
|
||||
var providerConfig = BuildProviderConfig(pasteConfig);
|
||||
|
||||
return await TransformAsync(prompt, inputText, providerConfig, cancellationToken, progress);
|
||||
return await TransformAsync(prompt, inputText, imageBytes, providerConfig, cancellationToken, progress);
|
||||
}
|
||||
|
||||
private async Task<CustomActionTransformResult> TransformAsync(string prompt, string inputText, PasteAIConfig providerConfig, CancellationToken cancellationToken, IProgress<double> progress)
|
||||
private async Task<CustomActionTransformResult> TransformAsync(string prompt, string inputText, byte[] imageBytes, PasteAIConfig providerConfig, CancellationToken cancellationToken, IProgress<double> progress)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(providerConfig);
|
||||
|
||||
@@ -57,9 +57,9 @@ namespace AdvancedPaste.Services.CustomActions
|
||||
return new CustomActionTransformResult(string.Empty, AIServiceUsage.None);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(inputText))
|
||||
if (string.IsNullOrWhiteSpace(inputText) && imageBytes is null)
|
||||
{
|
||||
Logger.LogWarning("Clipboard has no usable text data");
|
||||
Logger.LogWarning("Clipboard has no usable data");
|
||||
return new CustomActionTransformResult(string.Empty, AIServiceUsage.None);
|
||||
}
|
||||
|
||||
@@ -80,6 +80,8 @@ namespace AdvancedPaste.Services.CustomActions
|
||||
{
|
||||
Prompt = prompt,
|
||||
InputText = inputText,
|
||||
ImageBytes = imageBytes,
|
||||
ImageMimeType = imageBytes != null ? "image/png" : null,
|
||||
SystemPrompt = systemPrompt,
|
||||
};
|
||||
|
||||
|
||||
@@ -12,6 +12,6 @@ namespace AdvancedPaste.Services.CustomActions
|
||||
{
|
||||
public interface ICustomActionTransformService
|
||||
{
|
||||
Task<CustomActionTransformResult> TransformTextAsync(string prompt, string inputText, CancellationToken cancellationToken, IProgress<double> progress);
|
||||
Task<CustomActionTransformResult> TransformAsync(string prompt, string inputText, byte[] imageBytes, CancellationToken cancellationToken, IProgress<double> progress);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,10 @@ namespace AdvancedPaste.Services.CustomActions
|
||||
|
||||
public string InputText { get; init; }
|
||||
|
||||
public byte[] ImageBytes { get; init; }
|
||||
|
||||
public string ImageMimeType { get; init; }
|
||||
|
||||
public string SystemPrompt { get; init; }
|
||||
|
||||
public AIServiceUsage Usage { get; set; } = AIServiceUsage.None;
|
||||
|
||||
@@ -64,21 +64,13 @@ namespace AdvancedPaste.Services.CustomActions
|
||||
|
||||
var prompt = request.Prompt;
|
||||
var inputText = request.InputText;
|
||||
if (string.IsNullOrWhiteSpace(prompt) || string.IsNullOrWhiteSpace(inputText))
|
||||
var imageBytes = request.ImageBytes;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(prompt) || (string.IsNullOrWhiteSpace(inputText) && imageBytes is null))
|
||||
{
|
||||
throw new ArgumentException("Prompt and input text must be provided", nameof(request));
|
||||
throw new ArgumentException("Prompt and input content must be provided", nameof(request));
|
||||
}
|
||||
|
||||
var userMessageContent = $"""
|
||||
User instructions:
|
||||
{prompt}
|
||||
|
||||
Clipboard Content:
|
||||
{inputText}
|
||||
|
||||
Output:
|
||||
""";
|
||||
|
||||
var executionSettings = CreateExecutionSettings();
|
||||
var kernel = CreateKernel();
|
||||
var modelId = _config.Model;
|
||||
@@ -102,7 +94,32 @@ namespace AdvancedPaste.Services.CustomActions
|
||||
|
||||
var chatHistory = new ChatHistory();
|
||||
chatHistory.AddSystemMessage(systemPrompt);
|
||||
chatHistory.AddUserMessage(userMessageContent);
|
||||
|
||||
if (imageBytes != null)
|
||||
{
|
||||
var collection = new ChatMessageContentItemCollection();
|
||||
if (!string.IsNullOrWhiteSpace(inputText))
|
||||
{
|
||||
collection.Add(new TextContent($"Clipboard Content:\n{inputText}"));
|
||||
}
|
||||
|
||||
collection.Add(new ImageContent(imageBytes, request.ImageMimeType ?? "image/png"));
|
||||
collection.Add(new TextContent($"User instructions:\n{prompt}\n\nOutput:"));
|
||||
chatHistory.AddUserMessage(collection);
|
||||
}
|
||||
else
|
||||
{
|
||||
var userMessageContent = $"""
|
||||
User instructions:
|
||||
{prompt}
|
||||
|
||||
Clipboard Content:
|
||||
{inputText}
|
||||
|
||||
Output:
|
||||
""";
|
||||
chatHistory.AddUserMessage(userMessageContent);
|
||||
}
|
||||
|
||||
var response = await chatService.GetChatMessageContentAsync(chatHistory, executionSettings, kernel, cancellationToken);
|
||||
chatHistory.Add(response);
|
||||
|
||||
@@ -67,12 +67,36 @@ public abstract class KernelServiceBase(
|
||||
|
||||
LogResult(cacheUsed, isSavedQuery, kernel.GetOrAddActionChain(), usage);
|
||||
|
||||
var outputPackage = kernel.GetDataPackage();
|
||||
var hasUsableData = await outputPackage.GetView().HasUsableDataAsync();
|
||||
|
||||
if (kernel.GetLastError() is Exception ex)
|
||||
{
|
||||
throw ex;
|
||||
// If we have an error, but the AI provided a final text response, we can ignore the error (likely a tool failure that the AI handled).
|
||||
// However, if we have usable data (e.g. from a successful tool call before the error?), we might want to keep it?
|
||||
// In the case of ImageToText failure, outputPackage is empty (new DataPackage), hasUsableData is false.
|
||||
// So we check if there is a valid response in the chat history.
|
||||
var lastMessage = chatHistory.LastOrDefault();
|
||||
bool hasAssistantResponse = lastMessage != null && lastMessage.Role == AuthorRole.Assistant && !string.IsNullOrEmpty(lastMessage.Content);
|
||||
|
||||
if (!hasAssistantResponse && !hasUsableData)
|
||||
{
|
||||
throw ex;
|
||||
}
|
||||
|
||||
// If we have a response or data, we log the error but proceed.
|
||||
Logger.LogWarning($"Kernel operation encountered an error but proceeded with available response/data: {ex.Message}");
|
||||
}
|
||||
|
||||
var outputPackage = kernel.GetDataPackage();
|
||||
if (!hasUsableData)
|
||||
{
|
||||
var lastMessage = chatHistory.LastOrDefault();
|
||||
if (lastMessage != null && lastMessage.Role == AuthorRole.Assistant && !string.IsNullOrEmpty(lastMessage.Content))
|
||||
{
|
||||
outputPackage = DataPackageHelpers.CreateFromText(lastMessage.Content);
|
||||
kernel.SetDataPackage(outputPackage);
|
||||
}
|
||||
}
|
||||
|
||||
if (!(await outputPackage.GetView().HasUsableDataAsync()))
|
||||
{
|
||||
@@ -148,7 +172,21 @@ public abstract class KernelServiceBase(
|
||||
var systemPrompt = string.IsNullOrWhiteSpace(runtimeConfig.SystemPrompt) ? DefaultSystemPrompt : runtimeConfig.SystemPrompt;
|
||||
chatHistory.AddSystemMessage(systemPrompt);
|
||||
chatHistory.AddSystemMessage($"Available clipboard formats: {await kernel.GetDataFormatsAsync()}");
|
||||
chatHistory.AddUserMessage(prompt);
|
||||
|
||||
var imageBytes = await kernel.GetDataPackageView().GetImageAsPngBytesAsync();
|
||||
if (imageBytes != null)
|
||||
{
|
||||
var collection = new ChatMessageContentItemCollection
|
||||
{
|
||||
new TextContent(prompt),
|
||||
new ImageContent(imageBytes, "image/png"),
|
||||
};
|
||||
chatHistory.AddUserMessage(collection);
|
||||
}
|
||||
else
|
||||
{
|
||||
chatHistory.AddUserMessage(prompt);
|
||||
}
|
||||
|
||||
if (ShouldModerateAdvancedAI())
|
||||
{
|
||||
@@ -302,8 +340,16 @@ public abstract class KernelServiceBase(
|
||||
new ActionChainItem(PasteFormats.CustomTextTransformation, Arguments: new() { { PromptParameterName, fixedPrompt } }),
|
||||
async dataPackageView =>
|
||||
{
|
||||
var input = await dataPackageView.GetClipboardTextOrThrowAsync(kernel.GetCancellationToken());
|
||||
var result = await _customActionTransformService.TransformTextAsync(fixedPrompt, input, kernel.GetCancellationToken(), kernel.GetProgress());
|
||||
var imageBytes = await dataPackageView.GetImageAsPngBytesAsync();
|
||||
var input = await dataPackageView.GetTextOrHtmlTextAsync();
|
||||
|
||||
if (string.IsNullOrEmpty(input) && imageBytes == null)
|
||||
{
|
||||
// If we have no text and no image, try to get text via OCR or throw if nothing exists
|
||||
input = await dataPackageView.GetClipboardTextOrThrowAsync(kernel.GetCancellationToken());
|
||||
}
|
||||
|
||||
var result = await _customActionTransformService.TransformAsync(fixedPrompt, input, imageBytes, kernel.GetCancellationToken(), kernel.GetProgress());
|
||||
return DataPackageHelpers.CreateFromText(result?.Content ?? string.Empty);
|
||||
});
|
||||
|
||||
@@ -313,15 +359,22 @@ public abstract class KernelServiceBase(
|
||||
new ActionChainItem(format, Arguments: new() { { PromptParameterName, prompt } }),
|
||||
async dataPackageView =>
|
||||
{
|
||||
var input = await dataPackageView.GetClipboardTextOrThrowAsync(kernel.GetCancellationToken());
|
||||
string output = await GetPromptBasedOutput(format, prompt, input, kernel.GetCancellationToken(), kernel.GetProgress());
|
||||
var imageBytes = await dataPackageView.GetImageAsPngBytesAsync();
|
||||
var input = await dataPackageView.GetTextOrHtmlTextAsync();
|
||||
|
||||
if (string.IsNullOrEmpty(input) && imageBytes == null)
|
||||
{
|
||||
input = await dataPackageView.GetClipboardTextOrThrowAsync(kernel.GetCancellationToken());
|
||||
}
|
||||
|
||||
string output = await GetPromptBasedOutput(format, prompt, input, imageBytes, kernel.GetCancellationToken(), kernel.GetProgress());
|
||||
return DataPackageHelpers.CreateFromText(output);
|
||||
});
|
||||
|
||||
private async Task<string> GetPromptBasedOutput(PasteFormats format, string prompt, string input, CancellationToken cancellationToken, IProgress<double> progress) =>
|
||||
private async Task<string> GetPromptBasedOutput(PasteFormats format, string prompt, string input, byte[] imageBytes, CancellationToken cancellationToken, IProgress<double> progress) =>
|
||||
format switch
|
||||
{
|
||||
PasteFormats.CustomTextTransformation => (await _customActionTransformService.TransformTextAsync(prompt, input, cancellationToken, progress))?.Content ?? string.Empty,
|
||||
PasteFormats.CustomTextTransformation => (await _customActionTransformService.TransformAsync(prompt, input, imageBytes, cancellationToken, progress))?.Content ?? string.Empty,
|
||||
_ => throw new ArgumentException($"Unsupported format {format} for prompt transform", nameof(format)),
|
||||
};
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ public sealed class PasteFormatExecutor(IKernelService kernelService, ICustomAct
|
||||
pasteFormat.Format switch
|
||||
{
|
||||
PasteFormats.KernelQuery => await _kernelService.TransformClipboardAsync(pasteFormat.Prompt, clipboardData, pasteFormat.IsSavedQuery, cancellationToken, progress),
|
||||
PasteFormats.CustomTextTransformation => DataPackageHelpers.CreateFromText((await _customActionTransformService.TransformTextAsync(pasteFormat.Prompt, await clipboardData.GetClipboardTextOrThrowAsync(cancellationToken), cancellationToken, progress))?.Content ?? string.Empty),
|
||||
PasteFormats.CustomTextTransformation => DataPackageHelpers.CreateFromText((await _customActionTransformService.TransformAsync(pasteFormat.Prompt, await clipboardData.GetTextOrHtmlTextAsync(), await clipboardData.GetImageAsPngBytesAsync(), cancellationToken, progress))?.Content ?? string.Empty),
|
||||
_ => await TransformHelpers.TransformAsync(format, clipboardData, cancellationToken, progress),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ namespace AdvancedPaste.ViewModels
|
||||
private CancellationTokenSource _pasteActionCancellationTokenSource;
|
||||
|
||||
private string _currentClipboardHistoryId;
|
||||
private uint _lastClipboardSequenceNumber;
|
||||
private DateTimeOffset? _currentClipboardTimestamp;
|
||||
private ClipboardFormat _lastClipboardFormats = ClipboardFormat.None;
|
||||
private bool _clipboardHistoryUnavailableLogged;
|
||||
@@ -455,6 +456,7 @@ namespace AdvancedPaste.ViewModels
|
||||
{
|
||||
ResetClipboardPreview();
|
||||
_currentClipboardHistoryId = null;
|
||||
_lastClipboardSequenceNumber = 0;
|
||||
_currentClipboardTimestamp = null;
|
||||
_lastClipboardFormats = ClipboardFormat.None;
|
||||
return;
|
||||
@@ -477,6 +479,13 @@ namespace AdvancedPaste.ViewModels
|
||||
{
|
||||
bool clipboardChanged = formatsChanged;
|
||||
|
||||
var currentSequenceNumber = NativeMethods.GetClipboardSequenceNumber();
|
||||
if (_lastClipboardSequenceNumber != currentSequenceNumber)
|
||||
{
|
||||
clipboardChanged = true;
|
||||
_lastClipboardSequenceNumber = currentSequenceNumber;
|
||||
}
|
||||
|
||||
if (Clipboard.IsHistoryEnabled())
|
||||
{
|
||||
try
|
||||
|
||||
@@ -312,13 +312,39 @@ private:
|
||||
return false;
|
||||
}
|
||||
|
||||
void read_settings(PowerToysSettings::PowerToyValues& settings)
|
||||
void read_settings(PowerToysSettings::PowerToyValues& settings)
|
||||
{
|
||||
const auto settingsObject = settings.get_raw_json();
|
||||
|
||||
// Migrate Paste As Plain text shortcut
|
||||
Hotkey old_paste_as_plain_hotkey;
|
||||
bool old_data_migrated = migrate_data_and_remove_data_file(old_paste_as_plain_hotkey);
|
||||
|
||||
if (settingsObject.GetView().Size())
|
||||
{
|
||||
const auto propertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES);
|
||||
|
||||
m_is_advanced_ai_enabled = has_advanced_ai_provider(propertiesObject);
|
||||
|
||||
if (propertiesObject.HasKey(JSON_KEY_IS_AI_ENABLED))
|
||||
{
|
||||
m_is_ai_enabled = propertiesObject.GetNamedObject(JSON_KEY_IS_AI_ENABLED).GetNamedBoolean(JSON_KEY_VALUE, false);
|
||||
}
|
||||
else if (propertiesObject.HasKey(JSON_KEY_IS_OPEN_AI_ENABLED))
|
||||
{
|
||||
m_is_ai_enabled = propertiesObject.GetNamedObject(JSON_KEY_IS_OPEN_AI_ENABLED).GetNamedBoolean(JSON_KEY_VALUE, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_is_ai_enabled = false;
|
||||
}
|
||||
|
||||
if (propertiesObject.HasKey(JSON_KEY_SHOW_CUSTOM_PREVIEW))
|
||||
{
|
||||
m_preview_custom_format_output = propertiesObject.GetNamedObject(JSON_KEY_SHOW_CUSTOM_PREVIEW).GetNamedBoolean(JSON_KEY_VALUE);
|
||||
}
|
||||
}
|
||||
|
||||
if (old_data_migrated)
|
||||
{
|
||||
m_paste_as_plain_hotkey = old_paste_as_plain_hotkey;
|
||||
@@ -405,31 +431,6 @@ private:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (settingsObject.GetView().Size())
|
||||
{
|
||||
const auto propertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES);
|
||||
|
||||
m_is_advanced_ai_enabled = has_advanced_ai_provider(propertiesObject);
|
||||
|
||||
if (propertiesObject.HasKey(JSON_KEY_IS_AI_ENABLED))
|
||||
{
|
||||
m_is_ai_enabled = propertiesObject.GetNamedObject(JSON_KEY_IS_AI_ENABLED).GetNamedBoolean(JSON_KEY_VALUE, false);
|
||||
}
|
||||
else if (propertiesObject.HasKey(JSON_KEY_IS_OPEN_AI_ENABLED))
|
||||
{
|
||||
m_is_ai_enabled = propertiesObject.GetNamedObject(JSON_KEY_IS_OPEN_AI_ENABLED).GetNamedBoolean(JSON_KEY_VALUE, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_is_ai_enabled = false;
|
||||
}
|
||||
|
||||
if (propertiesObject.HasKey(JSON_KEY_SHOW_CUSTOM_PREVIEW))
|
||||
{
|
||||
m_preview_custom_format_output = propertiesObject.GetNamedObject(JSON_KEY_SHOW_CUSTOM_PREVIEW).GetNamedBoolean(JSON_KEY_VALUE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load the settings file.
|
||||
|
||||
@@ -50,6 +50,7 @@ enum class ScheduleMode
|
||||
Off,
|
||||
FixedHours,
|
||||
SunsetToSunrise,
|
||||
FollowNightLight,
|
||||
// add more later
|
||||
};
|
||||
|
||||
@@ -61,6 +62,8 @@ inline std::wstring ToString(ScheduleMode mode)
|
||||
return L"SunsetToSunrise";
|
||||
case ScheduleMode::FixedHours:
|
||||
return L"FixedHours";
|
||||
case ScheduleMode::FollowNightLight:
|
||||
return L"FollowNightLight";
|
||||
default:
|
||||
return L"Off";
|
||||
}
|
||||
@@ -72,6 +75,8 @@ inline ScheduleMode FromString(const std::wstring& str)
|
||||
return ScheduleMode::SunsetToSunrise;
|
||||
if (str == L"FixedHours")
|
||||
return ScheduleMode::FixedHours;
|
||||
if (str == L"FollowNightLight")
|
||||
return ScheduleMode::FollowNightLight;
|
||||
return ScheduleMode::Off;
|
||||
}
|
||||
|
||||
@@ -167,7 +172,9 @@ public:
|
||||
ToString(g_settings.m_scheduleMode),
|
||||
{ { L"Off", L"Disable the schedule" },
|
||||
{ L"FixedHours", L"Set hours manually" },
|
||||
{ L"SunsetToSunrise", L"Use sunrise/sunset times" } });
|
||||
{ L"SunsetToSunrise", L"Use sunrise/sunset times" },
|
||||
{ L"FollowNightLight", L"Follow Windows Night Light state" }
|
||||
});
|
||||
|
||||
// Integer spinners
|
||||
settings.add_int_spinner(
|
||||
|
||||
@@ -13,10 +13,12 @@
|
||||
#include <utils/logger_helper.h>
|
||||
#include "LightSwitchStateManager.h"
|
||||
#include <LightSwitchUtils.h>
|
||||
#include <NightLightRegistryObserver.h>
|
||||
|
||||
SERVICE_STATUS g_ServiceStatus = {};
|
||||
SERVICE_STATUS_HANDLE g_StatusHandle = nullptr;
|
||||
HANDLE g_ServiceStopEvent = nullptr;
|
||||
static LightSwitchStateManager* g_stateManagerPtr = nullptr;
|
||||
|
||||
VOID WINAPI ServiceMain(DWORD argc, LPTSTR* argv);
|
||||
VOID WINAPI ServiceCtrlHandler(DWORD dwCtrl);
|
||||
@@ -168,7 +170,15 @@ static void DetectAndHandleExternalThemeChange(LightSwitchStateManager& stateMan
|
||||
}
|
||||
|
||||
// Use shared helper (handles wraparound logic)
|
||||
bool shouldBeLight = ShouldBeLight(nowMinutes, effectiveLight, effectiveDark);
|
||||
bool shouldBeLight = false;
|
||||
if (s.scheduleMode == ScheduleMode::FollowNightLight)
|
||||
{
|
||||
shouldBeLight = !IsNightLightEnabled();
|
||||
}
|
||||
else
|
||||
{
|
||||
shouldBeLight = ShouldBeLight(nowMinutes, effectiveLight, effectiveDark);
|
||||
}
|
||||
|
||||
// Compare current system/apps theme
|
||||
bool currentSystemLight = GetCurrentSystemTheme();
|
||||
@@ -199,15 +209,40 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
|
||||
// Initialization
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
static LightSwitchStateManager stateManager;
|
||||
g_stateManagerPtr = &stateManager;
|
||||
|
||||
LightSwitchSettings::instance().InitFileWatcher();
|
||||
|
||||
HANDLE hManualOverride = OpenEventW(SYNCHRONIZE | EVENT_MODIFY_STATE, FALSE, L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE");
|
||||
HANDLE hSettingsChanged = LightSwitchSettings::instance().GetSettingsChangedEvent();
|
||||
|
||||
static std::unique_ptr<NightLightRegistryObserver> g_nightLightWatcher;
|
||||
|
||||
LightSwitchSettings::instance().LoadSettings();
|
||||
const auto& settings = LightSwitchSettings::instance().settings();
|
||||
|
||||
// after loading settings:
|
||||
bool nightLightNeeded = (settings.scheduleMode == ScheduleMode::FollowNightLight);
|
||||
|
||||
if (nightLightNeeded && !g_nightLightWatcher)
|
||||
{
|
||||
Logger::info(L"[LightSwitchService] Starting Night Light registry watcher...");
|
||||
|
||||
g_nightLightWatcher = std::make_unique<NightLightRegistryObserver>(
|
||||
HKEY_CURRENT_USER,
|
||||
NIGHT_LIGHT_REGISTRY_PATH,
|
||||
[]() {
|
||||
if (g_stateManagerPtr)
|
||||
g_stateManagerPtr->OnNightLightChange();
|
||||
});
|
||||
}
|
||||
else if (!nightLightNeeded && g_nightLightWatcher)
|
||||
{
|
||||
Logger::info(L"[LightSwitchService] Stopping Night Light registry watcher...");
|
||||
g_nightLightWatcher->Stop();
|
||||
g_nightLightWatcher.reset();
|
||||
}
|
||||
|
||||
SYSTEMTIME st;
|
||||
GetLocalTime(&st);
|
||||
int nowMinutes = st.wHour * 60 + st.wMinute;
|
||||
@@ -274,6 +309,31 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
|
||||
ResetEvent(hSettingsChanged);
|
||||
LightSwitchSettings::instance().LoadSettings();
|
||||
stateManager.OnSettingsChanged();
|
||||
|
||||
const auto& settings = LightSwitchSettings::instance().settings();
|
||||
bool nightLightNeeded = (settings.scheduleMode == ScheduleMode::FollowNightLight);
|
||||
|
||||
if (nightLightNeeded && !g_nightLightWatcher)
|
||||
{
|
||||
Logger::info(L"[LightSwitchService] Starting Night Light registry watcher...");
|
||||
|
||||
g_nightLightWatcher = std::make_unique<NightLightRegistryObserver>(
|
||||
HKEY_CURRENT_USER,
|
||||
NIGHT_LIGHT_REGISTRY_PATH,
|
||||
[]() {
|
||||
if (g_stateManagerPtr)
|
||||
g_stateManagerPtr->OnNightLightChange();
|
||||
});
|
||||
|
||||
stateManager.OnNightLightChange();
|
||||
}
|
||||
else if (!nightLightNeeded && g_nightLightWatcher)
|
||||
{
|
||||
Logger::info(L"[LightSwitchService] Stopping Night Light registry watcher...");
|
||||
g_nightLightWatcher->Stop();
|
||||
g_nightLightWatcher.reset();
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -285,6 +345,11 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
|
||||
CloseHandle(hManualOverride);
|
||||
if (hParent)
|
||||
CloseHandle(hParent);
|
||||
if (g_nightLightWatcher)
|
||||
{
|
||||
g_nightLightWatcher->Stop();
|
||||
g_nightLightWatcher.reset();
|
||||
}
|
||||
|
||||
Logger::info(L"[LightSwitchService] Worker thread exiting cleanly.");
|
||||
return 0;
|
||||
|
||||
@@ -76,6 +76,7 @@
|
||||
<ClCompile Include="LightSwitchService.cpp" />
|
||||
<ClCompile Include="LightSwitchSettings.cpp" />
|
||||
<ClCompile Include="LightSwitchStateManager.cpp" />
|
||||
<ClCompile Include="NightLightRegistryObserver.cpp" />
|
||||
<ClCompile Include="SettingsConstants.cpp" />
|
||||
<ClCompile Include="ThemeHelper.cpp" />
|
||||
<ClCompile Include="ThemeScheduler.cpp" />
|
||||
@@ -88,6 +89,7 @@
|
||||
<ClInclude Include="LightSwitchSettings.h" />
|
||||
<ClInclude Include="LightSwitchStateManager.h" />
|
||||
<ClInclude Include="LightSwitchUtils.h" />
|
||||
<ClInclude Include="NightLightRegistryObserver.h" />
|
||||
<ClInclude Include="SettingsConstants.h" />
|
||||
<ClInclude Include="SettingsObserver.h" />
|
||||
<ClInclude Include="ThemeHelper.h" />
|
||||
|
||||
@@ -36,6 +36,9 @@
|
||||
<ClCompile Include="LightSwitchStateManager.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="NightLightRegistryObserver.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="ThemeScheduler.h">
|
||||
@@ -62,6 +65,9 @@
|
||||
<ClInclude Include="LightSwitchUtils.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="NightLightRegistryObserver.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Natvis Include="$(MSBuildThisFileDirectory)..\..\natvis\wil.natvis" />
|
||||
|
||||
@@ -19,7 +19,8 @@ enum class ScheduleMode
|
||||
{
|
||||
Off,
|
||||
FixedHours,
|
||||
SunsetToSunrise
|
||||
SunsetToSunrise,
|
||||
FollowNightLight,
|
||||
// Add more in the future
|
||||
};
|
||||
|
||||
@@ -31,6 +32,8 @@ inline std::wstring ToString(ScheduleMode mode)
|
||||
return L"FixedHours";
|
||||
case ScheduleMode::SunsetToSunrise:
|
||||
return L"SunsetToSunrise";
|
||||
case ScheduleMode::FollowNightLight:
|
||||
return L"FollowNightLight";
|
||||
default:
|
||||
return L"Off";
|
||||
}
|
||||
@@ -42,6 +45,8 @@ inline ScheduleMode FromString(const std::wstring& str)
|
||||
return ScheduleMode::SunsetToSunrise;
|
||||
if (str == L"FixedHours")
|
||||
return ScheduleMode::FixedHours;
|
||||
if (str == L"FollowNightLight")
|
||||
return ScheduleMode::FollowNightLight;
|
||||
else
|
||||
return ScheduleMode::Off;
|
||||
}
|
||||
|
||||
@@ -31,7 +31,10 @@ void LightSwitchStateManager::OnSettingsChanged()
|
||||
void LightSwitchStateManager::OnTick(int currentMinutes)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_stateMutex);
|
||||
EvaluateAndApplyIfNeeded();
|
||||
if (_state.lastAppliedMode != ScheduleMode::FollowNightLight)
|
||||
{
|
||||
EvaluateAndApplyIfNeeded();
|
||||
}
|
||||
}
|
||||
|
||||
// Called when manual override is triggered
|
||||
@@ -49,8 +52,38 @@ void LightSwitchStateManager::OnManualOverride()
|
||||
_state.isAppsLightActive = GetCurrentAppsTheme();
|
||||
|
||||
Logger::debug(L"[LightSwitchStateManager] Synced internal theme state to current system theme ({}) and apps theme ({}).",
|
||||
(_state.isSystemLightActive ? L"light" : L"dark"),
|
||||
(_state.isAppsLightActive ? L"light" : L"dark"));
|
||||
(_state.isSystemLightActive ? L"light" : L"dark"),
|
||||
(_state.isAppsLightActive ? L"light" : L"dark"));
|
||||
}
|
||||
|
||||
EvaluateAndApplyIfNeeded();
|
||||
}
|
||||
|
||||
// Runs with the registry observer detects a change in Night Light settings.
|
||||
void LightSwitchStateManager::OnNightLightChange()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_stateMutex);
|
||||
|
||||
bool newNightLightState = IsNightLightEnabled();
|
||||
|
||||
// In Follow Night Light mode, treat a Night Light toggle as a boundary
|
||||
if (_state.lastAppliedMode == ScheduleMode::FollowNightLight && _state.isManualOverride)
|
||||
{
|
||||
Logger::info(L"[LightSwitchStateManager] Night Light changed while manual override active; "
|
||||
L"treating as a boundary and clearing manual override.");
|
||||
_state.isManualOverride = false;
|
||||
}
|
||||
|
||||
if (newNightLightState != _state.isNightLightActive)
|
||||
{
|
||||
Logger::info(L"[LightSwitchStateManager] Night Light toggled to {}",
|
||||
newNightLightState ? L"ON" : L"OFF");
|
||||
|
||||
_state.isNightLightActive = newNightLightState;
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::debug(L"[LightSwitchStateManager] Night Light change event fired, but no actual change.");
|
||||
}
|
||||
|
||||
EvaluateAndApplyIfNeeded();
|
||||
@@ -77,9 +110,9 @@ void LightSwitchStateManager::SyncInitialThemeState()
|
||||
_state.isSystemLightActive = GetCurrentSystemTheme();
|
||||
_state.isAppsLightActive = GetCurrentAppsTheme();
|
||||
Logger::debug(L"[LightSwitchStateManager] Synced initial state to current system theme ({})",
|
||||
_state.isSystemLightActive ? L"light" : L"dark");
|
||||
_state.isSystemLightActive ? L"light" : L"dark");
|
||||
Logger::debug(L"[LightSwitchStateManager] Synced initial state to current apps theme ({})",
|
||||
_state.isAppsLightActive ? L"light" : L"dark");
|
||||
_state.isAppsLightActive ? L"light" : L"dark");
|
||||
}
|
||||
|
||||
static std::pair<int, int> update_sun_times(auto& settings)
|
||||
@@ -194,7 +227,15 @@ void LightSwitchStateManager::EvaluateAndApplyIfNeeded()
|
||||
|
||||
_state.lastAppliedMode = _currentSettings.scheduleMode;
|
||||
|
||||
bool shouldBeLight = ShouldBeLight(now, _state.effectiveLightMinutes, _state.effectiveDarkMinutes);
|
||||
bool shouldBeLight = false;
|
||||
if (_currentSettings.scheduleMode == ScheduleMode::FollowNightLight)
|
||||
{
|
||||
shouldBeLight = !_state.isNightLightActive;
|
||||
}
|
||||
else
|
||||
{
|
||||
shouldBeLight = ShouldBeLight(now, _state.effectiveLightMinutes, _state.effectiveDarkMinutes);
|
||||
}
|
||||
|
||||
bool appsNeedsToChange = _currentSettings.changeApps && (_state.isAppsLightActive != shouldBeLight);
|
||||
bool systemNeedsToChange = _currentSettings.changeSystem && (_state.isSystemLightActive != shouldBeLight);
|
||||
@@ -227,6 +268,3 @@ void LightSwitchStateManager::EvaluateAndApplyIfNeeded()
|
||||
|
||||
_state.lastTickMinutes = now;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ struct LightSwitchState
|
||||
bool isManualOverride = false;
|
||||
bool isSystemLightActive = false;
|
||||
bool isAppsLightActive = false;
|
||||
bool isNightLightActive = false;
|
||||
int lastEvaluatedDay = -1;
|
||||
int lastTickMinutes = -1;
|
||||
|
||||
@@ -32,6 +33,9 @@ public:
|
||||
// Called when manual override is toggled (via shortcut or system change).
|
||||
void OnManualOverride();
|
||||
|
||||
// Called when night light changes in windows settings
|
||||
void OnNightLightChange();
|
||||
|
||||
// Initial sync at startup to align internal state with system theme
|
||||
void SyncInitialThemeState();
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
#include "NightLightRegistryObserver.h"
|
||||
@@ -0,0 +1,134 @@
|
||||
#pragma once
|
||||
#include <wtypes.h>
|
||||
#include <string>
|
||||
#include <functional>
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
|
||||
class NightLightRegistryObserver
|
||||
{
|
||||
public:
|
||||
NightLightRegistryObserver(HKEY root, const std::wstring& subkey, std::function<void()> callback) :
|
||||
_root(root), _subkey(subkey), _callback(std::move(callback)), _stop(false)
|
||||
{
|
||||
_thread = std::thread([this]() { this->Run(); });
|
||||
}
|
||||
|
||||
~NightLightRegistryObserver()
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
|
||||
void Stop()
|
||||
{
|
||||
_stop = true;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
if (_event)
|
||||
SetEvent(_event);
|
||||
}
|
||||
|
||||
if (_thread.joinable())
|
||||
_thread.join();
|
||||
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
if (_hKey)
|
||||
{
|
||||
RegCloseKey(_hKey);
|
||||
_hKey = nullptr;
|
||||
}
|
||||
|
||||
if (_event)
|
||||
{
|
||||
CloseHandle(_event);
|
||||
_event = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private:
|
||||
void Run()
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
if (RegOpenKeyExW(_root, _subkey.c_str(), 0, KEY_NOTIFY, &_hKey) != ERROR_SUCCESS)
|
||||
return;
|
||||
|
||||
_event = CreateEventW(nullptr, TRUE, FALSE, nullptr);
|
||||
if (!_event)
|
||||
{
|
||||
RegCloseKey(_hKey);
|
||||
_hKey = nullptr;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
while (!_stop)
|
||||
{
|
||||
HKEY hKeyLocal = nullptr;
|
||||
HANDLE eventLocal = nullptr;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
if (_stop)
|
||||
break;
|
||||
|
||||
hKeyLocal = _hKey;
|
||||
eventLocal = _event;
|
||||
}
|
||||
|
||||
if (!hKeyLocal || !eventLocal)
|
||||
break;
|
||||
|
||||
if (_stop)
|
||||
break;
|
||||
|
||||
if (RegNotifyChangeKeyValue(hKeyLocal, FALSE, REG_NOTIFY_CHANGE_LAST_SET, eventLocal, TRUE) != ERROR_SUCCESS)
|
||||
break;
|
||||
|
||||
DWORD wait = WaitForSingleObject(eventLocal, INFINITE);
|
||||
if (_stop || wait == WAIT_FAILED)
|
||||
break;
|
||||
|
||||
ResetEvent(eventLocal);
|
||||
|
||||
if (!_stop && _callback)
|
||||
{
|
||||
try
|
||||
{
|
||||
_callback();
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
if (_hKey)
|
||||
{
|
||||
RegCloseKey(_hKey);
|
||||
_hKey = nullptr;
|
||||
}
|
||||
|
||||
if (_event)
|
||||
{
|
||||
CloseHandle(_event);
|
||||
_event = nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
HKEY _root;
|
||||
std::wstring _subkey;
|
||||
std::function<void()> _callback;
|
||||
HANDLE _event = nullptr;
|
||||
HKEY _hKey = nullptr;
|
||||
std::thread _thread;
|
||||
std::atomic<bool> _stop;
|
||||
std::mutex _mutex;
|
||||
};
|
||||
@@ -11,4 +11,7 @@ enum class SettingId
|
||||
Sunset_Offset,
|
||||
ChangeSystem,
|
||||
ChangeApps
|
||||
};
|
||||
};
|
||||
|
||||
constexpr wchar_t PERSONALIZATION_REGISTRY_PATH[] = L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize";
|
||||
constexpr wchar_t NIGHT_LIGHT_REGISTRY_PATH[] = L"Software\\Microsoft\\Windows\\CurrentVersion\\CloudStore\\Store\\DefaultAccount\\Current\\default$windows.data.bluelightreduction.bluelightreductionstate\\windows.data.bluelightreduction.bluelightreductionstate";
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <logger/logger.h>
|
||||
#include <utils/logger_helper.h>
|
||||
#include "ThemeHelper.h"
|
||||
#include <SettingsConstants.h>
|
||||
|
||||
// Controls changing the themes.
|
||||
|
||||
@@ -10,7 +11,7 @@ static void ResetColorPrevalence()
|
||||
{
|
||||
HKEY hKey;
|
||||
if (RegOpenKeyEx(HKEY_CURRENT_USER,
|
||||
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
|
||||
PERSONALIZATION_REGISTRY_PATH,
|
||||
0,
|
||||
KEY_SET_VALUE,
|
||||
&hKey) == ERROR_SUCCESS)
|
||||
@@ -31,7 +32,7 @@ void SetAppsTheme(bool mode)
|
||||
{
|
||||
HKEY hKey;
|
||||
if (RegOpenKeyEx(HKEY_CURRENT_USER,
|
||||
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
|
||||
PERSONALIZATION_REGISTRY_PATH,
|
||||
0,
|
||||
KEY_SET_VALUE,
|
||||
&hKey) == ERROR_SUCCESS)
|
||||
@@ -50,7 +51,7 @@ void SetSystemTheme(bool mode)
|
||||
{
|
||||
HKEY hKey;
|
||||
if (RegOpenKeyEx(HKEY_CURRENT_USER,
|
||||
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
|
||||
PERSONALIZATION_REGISTRY_PATH,
|
||||
0,
|
||||
KEY_SET_VALUE,
|
||||
&hKey) == ERROR_SUCCESS)
|
||||
@@ -79,7 +80,7 @@ bool GetCurrentSystemTheme()
|
||||
DWORD size = sizeof(value);
|
||||
|
||||
if (RegOpenKeyEx(HKEY_CURRENT_USER,
|
||||
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
|
||||
PERSONALIZATION_REGISTRY_PATH,
|
||||
0,
|
||||
KEY_READ,
|
||||
&hKey) == ERROR_SUCCESS)
|
||||
@@ -98,7 +99,7 @@ bool GetCurrentAppsTheme()
|
||||
DWORD size = sizeof(value);
|
||||
|
||||
if (RegOpenKeyEx(HKEY_CURRENT_USER,
|
||||
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
|
||||
PERSONALIZATION_REGISTRY_PATH,
|
||||
0,
|
||||
KEY_READ,
|
||||
&hKey) == ERROR_SUCCESS)
|
||||
@@ -109,3 +110,30 @@ bool GetCurrentAppsTheme()
|
||||
|
||||
return value == 1; // true = light, false = dark
|
||||
}
|
||||
|
||||
bool IsNightLightEnabled()
|
||||
{
|
||||
HKEY hKey;
|
||||
const wchar_t* path = NIGHT_LIGHT_REGISTRY_PATH;
|
||||
|
||||
if (RegOpenKeyExW(HKEY_CURRENT_USER, path, 0, KEY_READ, &hKey) != ERROR_SUCCESS)
|
||||
return false;
|
||||
|
||||
// RegGetValueW will set size to the size of the data and we expect that to be at least 25 bytes (we need to access bytes 23 and 24)
|
||||
DWORD size = 0;
|
||||
if (RegGetValueW(hKey, nullptr, L"Data", RRF_RT_REG_BINARY, nullptr, nullptr, &size) != ERROR_SUCCESS || size < 25)
|
||||
{
|
||||
RegCloseKey(hKey);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<BYTE> data(size);
|
||||
if (RegGetValueW(hKey, nullptr, L"Data", RRF_RT_REG_BINARY, nullptr, data.data(), &size) != ERROR_SUCCESS)
|
||||
{
|
||||
RegCloseKey(hKey);
|
||||
return false;
|
||||
}
|
||||
|
||||
RegCloseKey(hKey);
|
||||
return data[23] == 0x10 && data[24] == 0x00;
|
||||
}
|
||||
@@ -3,3 +3,4 @@ void SetSystemTheme(bool dark);
|
||||
void SetAppsTheme(bool dark);
|
||||
bool GetCurrentSystemTheme();
|
||||
bool GetCurrentAppsTheme();
|
||||
bool IsNightLightEnabled();
|
||||
@@ -617,6 +617,8 @@ namespace MouseUtils.UITests
|
||||
|
||||
private void LaunchFromSetting(bool reload = false, bool launchAsAdmin = false)
|
||||
{
|
||||
Session = RestartScopeExe("FindMyMouse,MouseHighlighter,MouseJump,MousePointerCrosshairs,CursorWrap");
|
||||
|
||||
// this.Session.Attach(PowerToysModule.PowerToysSettings);
|
||||
this.Session.SetMainWindowSize(WindowSize.Large);
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ using Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Models;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Windows.ApplicationModel.DataTransfer;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.ViewModels;
|
||||
|
||||
@@ -16,6 +17,8 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
{
|
||||
public ExtensionObject<ICommandItem> Model => _commandItemModel;
|
||||
|
||||
private ExtensionObject<IExtendedAttributesProvider>? ExtendedAttributesProvider { get; set; }
|
||||
|
||||
private readonly ExtensionObject<ICommandItem> _commandItemModel = new(null);
|
||||
private CommandContextItemViewModel? _defaultCommandContextItemViewModel;
|
||||
|
||||
@@ -65,6 +68,8 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
|
||||
public bool ShouldBeVisible => !string.IsNullOrEmpty(Name);
|
||||
|
||||
public DataPackageView? DataPackage { get; private set; }
|
||||
|
||||
public List<IContextItemViewModel> AllCommands
|
||||
{
|
||||
get
|
||||
@@ -157,6 +162,13 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
// will never be able to load Hotkeys & aliases
|
||||
UpdateProperty(nameof(IsInitialized));
|
||||
|
||||
if (model is IExtendedAttributesProvider extendedAttributesProvider)
|
||||
{
|
||||
ExtendedAttributesProvider = new ExtensionObject<IExtendedAttributesProvider>(extendedAttributesProvider);
|
||||
var properties = extendedAttributesProvider.GetProperties();
|
||||
UpdateDataPackage(properties);
|
||||
}
|
||||
|
||||
Initialized |= InitializedState.Initialized;
|
||||
}
|
||||
|
||||
@@ -379,6 +391,9 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
UpdateProperty(nameof(SecondaryCommandName));
|
||||
UpdateProperty(nameof(HasMoreCommands));
|
||||
|
||||
break;
|
||||
case nameof(DataPackage):
|
||||
UpdateDataPackage(ExtendedAttributesProvider?.Unsafe?.GetProperties());
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -431,6 +446,16 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
UpdateProperty(nameof(Icon));
|
||||
}
|
||||
|
||||
private void UpdateDataPackage(IDictionary<string, object?>? properties)
|
||||
{
|
||||
DataPackage =
|
||||
properties?.TryGetValue(WellKnownExtensionAttributes.DataPackage, out var dataPackageView) == true &&
|
||||
dataPackageView is DataPackageView view
|
||||
? view
|
||||
: null;
|
||||
UpdateProperty(nameof(DataPackage));
|
||||
}
|
||||
|
||||
protected override void UnsafeCleanup()
|
||||
{
|
||||
base.UnsafeCleanup();
|
||||
|
||||
@@ -19,6 +19,8 @@ public partial class DetailsViewModel(IDetails _details, WeakReference<IPageCont
|
||||
|
||||
public string Body { get; private set; } = string.Empty;
|
||||
|
||||
public ContentSize? Size { get; private set; } = ContentSize.Small;
|
||||
|
||||
// Metadata is an array of IDetailsElement,
|
||||
// where IDetailsElement = {IDetailsTags, IDetailsLink, IDetailsSeparator}
|
||||
public List<DetailsElementViewModel> Metadata { get; private set; } = [];
|
||||
@@ -40,6 +42,21 @@ public partial class DetailsViewModel(IDetails _details, WeakReference<IPageCont
|
||||
UpdateProperty(nameof(Body));
|
||||
UpdateProperty(nameof(HeroImage));
|
||||
|
||||
if (model is IExtendedAttributesProvider provider)
|
||||
{
|
||||
if (provider.GetProperties()?.TryGetValue("Size", out var rawValue) == true)
|
||||
{
|
||||
if (rawValue is int sizeAsInt)
|
||||
{
|
||||
Size = (ContentSize)sizeAsInt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Size ??= ContentSize.Small;
|
||||
|
||||
UpdateProperty(nameof(Size));
|
||||
|
||||
var meta = model.Metadata;
|
||||
if (meta is not null)
|
||||
{
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Models;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Windows.Storage.Streams;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.ViewModels;
|
||||
@@ -57,7 +58,7 @@ public partial class IconDataViewModel : ObservableObject, IIconData
|
||||
// because each call to GetProperties() is a cross process hop, and if you
|
||||
// marshal-by-value the property set, then you don't want to throw it away and
|
||||
// re-marshal it for every property. MAKE SURE YOU CACHE IT.
|
||||
if (props?.TryGetValue("FontFamily", out var family) ?? false)
|
||||
if (props?.TryGetValue(WellKnownExtensionAttributes.FontFamily, out var family) ?? false)
|
||||
{
|
||||
FontFamily = family as string;
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ public partial class ListItemViewModel : CommandItemViewModel
|
||||
|
||||
public string Section { get; private set; } = string.Empty;
|
||||
|
||||
public bool IsSectionOrSeparator { get; private set; }
|
||||
|
||||
public DetailsViewModel? Details { get; private set; }
|
||||
|
||||
[MemberNotNullWhen(true, nameof(Details))]
|
||||
@@ -82,14 +84,18 @@ public partial class ListItemViewModel : CommandItemViewModel
|
||||
}
|
||||
|
||||
UpdateTags(li.Tags);
|
||||
|
||||
Section = li.Section ?? string.Empty;
|
||||
|
||||
UpdateProperty(nameof(Section));
|
||||
IsSectionOrSeparator = IsSeparator(li);
|
||||
UpdateProperty(nameof(Section), nameof(IsSectionOrSeparator));
|
||||
|
||||
UpdateAccessibleName();
|
||||
}
|
||||
|
||||
private bool IsSeparator(IListItem item)
|
||||
{
|
||||
return item.Command is null;
|
||||
}
|
||||
|
||||
public override void SlowInitializeProperties()
|
||||
{
|
||||
base.SlowInitializeProperties();
|
||||
@@ -104,8 +110,7 @@ public partial class ListItemViewModel : CommandItemViewModel
|
||||
{
|
||||
Details = new(extensionDetails, PageContext);
|
||||
Details.InitializeProperties();
|
||||
UpdateProperty(nameof(Details));
|
||||
UpdateProperty(nameof(HasDetails));
|
||||
UpdateProperty(nameof(Details), nameof(HasDetails));
|
||||
}
|
||||
|
||||
AddShowDetailsCommands();
|
||||
@@ -135,14 +140,18 @@ public partial class ListItemViewModel : CommandItemViewModel
|
||||
break;
|
||||
case nameof(model.Section):
|
||||
Section = model.Section ?? string.Empty;
|
||||
UpdateProperty(nameof(Section));
|
||||
IsSectionOrSeparator = IsSeparator(model);
|
||||
UpdateProperty(nameof(Section), nameof(IsSectionOrSeparator));
|
||||
break;
|
||||
case nameof(model.Details):
|
||||
case nameof(model.Command):
|
||||
IsSectionOrSeparator = IsSeparator(model);
|
||||
UpdateProperty(nameof(IsSectionOrSeparator));
|
||||
break;
|
||||
case nameof(Details):
|
||||
var extensionDetails = model.Details;
|
||||
Details = extensionDetails is not null ? new(extensionDetails, PageContext) : null;
|
||||
Details?.InitializeProperties();
|
||||
UpdateProperty(nameof(Details));
|
||||
UpdateProperty(nameof(HasDetails));
|
||||
UpdateProperty(nameof(Details), nameof(HasDetails));
|
||||
UpdateShowDetailsCommand();
|
||||
break;
|
||||
case nameof(model.MoreCommands):
|
||||
@@ -194,8 +203,7 @@ public partial class ListItemViewModel : CommandItemViewModel
|
||||
MoreCommands.Add(showDetailsContextItemViewModel);
|
||||
}
|
||||
|
||||
UpdateProperty(nameof(MoreCommands));
|
||||
UpdateProperty(nameof(AllCommands));
|
||||
UpdateProperty(nameof(MoreCommands), nameof(AllCommands));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,8 +235,7 @@ public partial class ListItemViewModel : CommandItemViewModel
|
||||
showDetailsContextItemViewModel.SlowInitializeProperties();
|
||||
MoreCommands.Add(showDetailsContextItemViewModel);
|
||||
|
||||
UpdateProperty(nameof(MoreCommands));
|
||||
UpdateProperty(nameof(AllCommands));
|
||||
UpdateProperty(nameof(MoreCommands), nameof(AllCommands));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
// 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.Core.ViewModels.Messages;
|
||||
|
||||
/// <summary>
|
||||
/// Message sent when an error occurs during command execution.
|
||||
/// Used to track session error count for telemetry.
|
||||
/// </summary>
|
||||
public record ErrorOccurredMessage();
|
||||
@@ -0,0 +1,11 @@
|
||||
// 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.Core.ViewModels.Messages;
|
||||
|
||||
/// <summary>
|
||||
/// Message sent when an extension command or page is invoked.
|
||||
/// Captures extension usage metrics for telemetry tracking.
|
||||
/// </summary>
|
||||
public record ExtensionInvokedMessage(string ExtensionId, string CommandId, string CommandName, bool Success, ulong ExecutionTimeMs);
|
||||
@@ -0,0 +1,11 @@
|
||||
// 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.Core.ViewModels.Messages;
|
||||
|
||||
/// <summary>
|
||||
/// Message containing the current navigation depth (BackStack count) when navigating to a page.
|
||||
/// Used to track maximum navigation depth reached during a session for telemetry.
|
||||
/// </summary>
|
||||
public record NavigationDepthMessage(int Depth);
|
||||
@@ -0,0 +1,11 @@
|
||||
// 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.Core.ViewModels.Messages;
|
||||
|
||||
/// <summary>
|
||||
/// Message sent when a search query is executed in the Command Palette.
|
||||
/// Used to track session search activity for telemetry.
|
||||
/// </summary>
|
||||
public record SearchQueryMessage();
|
||||
@@ -0,0 +1,11 @@
|
||||
// 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.Core.ViewModels.Messages;
|
||||
|
||||
/// <summary>
|
||||
/// Message containing session telemetry data from Command Palette launch to dismissal.
|
||||
/// Used to aggregate metrics like duration, commands executed, pages visited, and search activity.
|
||||
/// </summary>
|
||||
public record SessionDurationMessage(ulong DurationMs, int CommandsExecuted, int PagesVisited, string DismissalReason, int SearchQueriesCount, int MaxNavigationDepth, int ErrorCount);
|
||||
@@ -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.Core.ViewModels.Messages;
|
||||
|
||||
/// <summary>
|
||||
/// Telemetry message sent when command invocation begins.
|
||||
/// </summary>
|
||||
public record TelemetryBeginInvokeMessage;
|
||||
@@ -0,0 +1,11 @@
|
||||
// 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.Core.ViewModels.Messages;
|
||||
|
||||
/// <summary>
|
||||
/// Telemetry message sent when an extension command or page is invoked.
|
||||
/// Captures extension usage metrics for telemetry tracking.
|
||||
/// </summary>
|
||||
public record TelemetryExtensionInvokedMessage(string ExtensionId, string CommandId, string CommandName, bool Success, ulong ExecutionTimeMs);
|
||||
@@ -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.Core.ViewModels.Messages;
|
||||
|
||||
/// <summary>
|
||||
/// Telemetry message sent when command invocation completes with a result.
|
||||
/// </summary>
|
||||
public record TelemetryInvokeResultMessage(Microsoft.CommandPalette.Extensions.CommandResultKind Kind);
|
||||
@@ -3,13 +3,14 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.ViewModels;
|
||||
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
|
||||
public partial class SeparatorViewModel() :
|
||||
CommandItem,
|
||||
IContextItemViewModel,
|
||||
IFilterItemViewModel,
|
||||
ISeparatorContextItem,
|
||||
|
||||
@@ -14,6 +14,7 @@ using Microsoft.CommandPalette.Extensions;
|
||||
namespace Microsoft.CmdPal.Core.ViewModels;
|
||||
|
||||
public partial class ShellViewModel : ObservableObject,
|
||||
IDisposable,
|
||||
IRecipient<PerformCommandMessage>,
|
||||
IRecipient<HandleCommandResultMessage>
|
||||
{
|
||||
@@ -269,8 +270,18 @@ public partial class ShellViewModel : ObservableObject,
|
||||
var isMainPage = command == _rootPage;
|
||||
_isNested = !isMainPage;
|
||||
|
||||
// Telemetry: Track extension page navigation for session metrics
|
||||
if (host is not null)
|
||||
{
|
||||
string extensionId = host.GetExtensionDisplayName() ?? "builtin";
|
||||
string commandId = command?.Id ?? "unknown";
|
||||
string commandName = command?.Name ?? "unknown";
|
||||
WeakReferenceMessenger.Default.Send<TelemetryExtensionInvokedMessage>(
|
||||
new(extensionId, commandId, commandName, true, 0));
|
||||
}
|
||||
|
||||
// Construct our ViewModel of the appropriate type and pass it the UI Thread context.
|
||||
var pageViewModel = _pageViewModelFactory.TryCreatePageViewModel(page, _isNested, host);
|
||||
var pageViewModel = _pageViewModelFactory.TryCreatePageViewModel(page, _isNested, host!);
|
||||
if (pageViewModel is null)
|
||||
{
|
||||
CoreLogger.LogError($"Failed to create ViewModel for page {page.GetType().Name}");
|
||||
@@ -305,7 +316,7 @@ public partial class ShellViewModel : ObservableObject,
|
||||
{
|
||||
CoreLogger.LogDebug($"Invoking command");
|
||||
|
||||
WeakReferenceMessenger.Default.Send<BeginInvokeMessage>();
|
||||
WeakReferenceMessenger.Default.Send<TelemetryBeginInvokeMessage>();
|
||||
StartInvoke(message, invokable, host);
|
||||
}
|
||||
}
|
||||
@@ -338,6 +349,14 @@ public partial class ShellViewModel : ObservableObject,
|
||||
|
||||
private void SafeHandleInvokeCommandSynchronous(PerformCommandMessage message, IInvokableCommand invokable, AppExtensionHost? host)
|
||||
{
|
||||
// Telemetry: Track command execution time and success
|
||||
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
|
||||
var command = message.Command.Unsafe;
|
||||
string extensionId = host?.GetExtensionDisplayName() ?? "builtin";
|
||||
string commandId = command?.Id ?? "unknown";
|
||||
string commandName = command?.Name ?? "unknown";
|
||||
bool success = false;
|
||||
|
||||
try
|
||||
{
|
||||
// Call out to extension process.
|
||||
@@ -348,16 +367,28 @@ public partial class ShellViewModel : ObservableObject,
|
||||
// But if it did succeed, we need to handle the result.
|
||||
UnsafeHandleCommandResult(result);
|
||||
|
||||
success = true;
|
||||
_handleInvokeTask = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
success = false;
|
||||
_handleInvokeTask = null;
|
||||
|
||||
// Telemetry: Track errors for session metrics
|
||||
WeakReferenceMessenger.Default.Send<ErrorOccurredMessage>(new());
|
||||
|
||||
// TODO: It would be better to do this as a page exception, rather
|
||||
// than a silent log message.
|
||||
host?.Log(ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Telemetry: Send extension invocation metrics (always sent, even on failure)
|
||||
stopwatch.Stop();
|
||||
WeakReferenceMessenger.Default.Send<TelemetryExtensionInvokedMessage>(
|
||||
new(extensionId, commandId, commandName, success, (ulong)stopwatch.ElapsedMilliseconds));
|
||||
}
|
||||
}
|
||||
|
||||
private void UnsafeHandleCommandResult(ICommandResult? result)
|
||||
@@ -371,7 +402,7 @@ public partial class ShellViewModel : ObservableObject,
|
||||
var kind = result.Kind;
|
||||
CoreLogger.LogDebug($"handling {kind.ToString()}");
|
||||
|
||||
WeakReferenceMessenger.Default.Send<CmdPalInvokeResultMessage>(new(kind));
|
||||
WeakReferenceMessenger.Default.Send<TelemetryInvokeResultMessage>(new(kind));
|
||||
switch (kind)
|
||||
{
|
||||
case CommandResultKind.Dismiss:
|
||||
@@ -460,4 +491,12 @@ public partial class ShellViewModel : ObservableObject,
|
||||
{
|
||||
_navigationCts?.Cancel();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_handleInvokeTask?.Dispose();
|
||||
_navigationCts?.Dispose();
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,390 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.ObjectModel;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.WinUI;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
using Microsoft.UI;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Windows.UI;
|
||||
using Windows.UI.ViewManagement;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDisposable
|
||||
{
|
||||
private static readonly ObservableCollection<Color> WindowsColorSwatches = [
|
||||
|
||||
// row 0
|
||||
Color.FromArgb(255, 255, 185, 0), // #ffb900
|
||||
Color.FromArgb(255, 255, 140, 0), // #ff8c00
|
||||
Color.FromArgb(255, 247, 99, 12), // #f7630c
|
||||
Color.FromArgb(255, 202, 80, 16), // #ca5010
|
||||
Color.FromArgb(255, 218, 59, 1), // #da3b01
|
||||
Color.FromArgb(255, 239, 105, 80), // #ef6950
|
||||
|
||||
// row 1
|
||||
Color.FromArgb(255, 209, 52, 56), // #d13438
|
||||
Color.FromArgb(255, 255, 67, 67), // #ff4343
|
||||
Color.FromArgb(255, 231, 72, 86), // #e74856
|
||||
Color.FromArgb(255, 232, 17, 35), // #e81123
|
||||
Color.FromArgb(255, 234, 0, 94), // #ea005e
|
||||
Color.FromArgb(255, 195, 0, 82), // #c30052
|
||||
|
||||
// row 2
|
||||
Color.FromArgb(255, 227, 0, 140), // #e3008c
|
||||
Color.FromArgb(255, 191, 0, 119), // #bf0077
|
||||
Color.FromArgb(255, 194, 57, 179), // #c239b3
|
||||
Color.FromArgb(255, 154, 0, 137), // #9a0089
|
||||
Color.FromArgb(255, 0, 120, 212), // #0078d4
|
||||
Color.FromArgb(255, 0, 99, 177), // #0063b1
|
||||
|
||||
// row 3
|
||||
Color.FromArgb(255, 142, 140, 216), // #8e8cd8
|
||||
Color.FromArgb(255, 107, 105, 214), // #6b69d6
|
||||
Color.FromArgb(255, 135, 100, 184), // #8764b8
|
||||
Color.FromArgb(255, 116, 77, 169), // #744da9
|
||||
Color.FromArgb(255, 177, 70, 194), // #b146c2
|
||||
Color.FromArgb(255, 136, 23, 152), // #881798
|
||||
|
||||
// row 4
|
||||
Color.FromArgb(255, 0, 153, 188), // #0099bc
|
||||
Color.FromArgb(255, 45, 125, 154), // #2d7d9a
|
||||
Color.FromArgb(255, 0, 183, 195), // #00b7c3
|
||||
Color.FromArgb(255, 3, 131, 135), // #038387
|
||||
Color.FromArgb(255, 0, 178, 148), // #00b294
|
||||
Color.FromArgb(255, 1, 133, 116), // #018574
|
||||
|
||||
// row 5
|
||||
Color.FromArgb(255, 0, 204, 106), // #00cc6a
|
||||
Color.FromArgb(255, 16, 137, 62), // #10893e
|
||||
Color.FromArgb(255, 122, 117, 116), // #7a7574
|
||||
Color.FromArgb(255, 93, 90, 88), // #5d5a58
|
||||
Color.FromArgb(255, 104, 118, 138), // #68768a
|
||||
Color.FromArgb(255, 81, 92, 107), // #515c6b
|
||||
|
||||
// row 6
|
||||
Color.FromArgb(255, 86, 124, 115), // #567c73
|
||||
Color.FromArgb(255, 72, 104, 96), // #486860
|
||||
Color.FromArgb(255, 73, 130, 5), // #498205
|
||||
Color.FromArgb(255, 16, 124, 16), // #107c10
|
||||
Color.FromArgb(255, 118, 118, 118), // #767676
|
||||
Color.FromArgb(255, 76, 74, 72), // #4c4a48
|
||||
|
||||
// row 7
|
||||
Color.FromArgb(255, 105, 121, 126), // #69797e
|
||||
Color.FromArgb(255, 74, 84, 89), // #4a5459
|
||||
Color.FromArgb(255, 100, 124, 100), // #647c64
|
||||
Color.FromArgb(255, 82, 94, 84), // #525e54
|
||||
Color.FromArgb(255, 132, 117, 69), // #847545
|
||||
Color.FromArgb(255, 126, 115, 95), // #7e735f
|
||||
];
|
||||
|
||||
private readonly SettingsModel _settings;
|
||||
private readonly UISettings _uiSettings;
|
||||
private readonly IThemeService _themeService;
|
||||
private readonly DispatcherQueueTimer _saveTimer = DispatcherQueue.GetForCurrentThread().CreateTimer();
|
||||
private readonly DispatcherQueue _uiDispatcher = DispatcherQueue.GetForCurrentThread();
|
||||
|
||||
private ElementTheme? _elementThemeOverride;
|
||||
private Color _currentSystemAccentColor;
|
||||
|
||||
public ObservableCollection<Color> Swatches => WindowsColorSwatches;
|
||||
|
||||
public int ThemeIndex
|
||||
{
|
||||
get => (int)_settings.Theme;
|
||||
set => Theme = (UserTheme)value;
|
||||
}
|
||||
|
||||
public UserTheme Theme
|
||||
{
|
||||
get => _settings.Theme;
|
||||
set
|
||||
{
|
||||
if (_settings.Theme != value)
|
||||
{
|
||||
_settings.Theme = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(ThemeIndex));
|
||||
Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ColorizationMode ColorizationMode
|
||||
{
|
||||
get => _settings.ColorizationMode;
|
||||
set
|
||||
{
|
||||
if (_settings.ColorizationMode != value)
|
||||
{
|
||||
_settings.ColorizationMode = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(ColorizationModeIndex));
|
||||
OnPropertyChanged(nameof(IsCustomTintVisible));
|
||||
OnPropertyChanged(nameof(IsCustomTintIntensityVisible));
|
||||
OnPropertyChanged(nameof(IsBackgroundControlsVisible));
|
||||
OnPropertyChanged(nameof(IsNoBackgroundVisible));
|
||||
OnPropertyChanged(nameof(IsAccentColorControlsVisible));
|
||||
|
||||
if (value == ColorizationMode.WindowsAccentColor)
|
||||
{
|
||||
ThemeColor = _currentSystemAccentColor;
|
||||
}
|
||||
|
||||
IsColorizationDetailsExpanded = value != ColorizationMode.None;
|
||||
|
||||
Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int ColorizationModeIndex
|
||||
{
|
||||
get => (int)_settings.ColorizationMode;
|
||||
set => ColorizationMode = (ColorizationMode)value;
|
||||
}
|
||||
|
||||
public Color ThemeColor
|
||||
{
|
||||
get => _settings.CustomThemeColor;
|
||||
set
|
||||
{
|
||||
if (_settings.CustomThemeColor != value)
|
||||
{
|
||||
_settings.CustomThemeColor = value;
|
||||
|
||||
OnPropertyChanged();
|
||||
|
||||
if (ColorIntensity == 0)
|
||||
{
|
||||
ColorIntensity = 100;
|
||||
}
|
||||
|
||||
Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int ColorIntensity
|
||||
{
|
||||
get => _settings.CustomThemeColorIntensity;
|
||||
set
|
||||
{
|
||||
_settings.CustomThemeColorIntensity = value;
|
||||
OnPropertyChanged();
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
public string BackgroundImagePath
|
||||
{
|
||||
get => _settings.BackgroundImagePath ?? string.Empty;
|
||||
set
|
||||
{
|
||||
if (_settings.BackgroundImagePath != value)
|
||||
{
|
||||
_settings.BackgroundImagePath = value;
|
||||
OnPropertyChanged();
|
||||
|
||||
if (BackgroundImageOpacity == 0)
|
||||
{
|
||||
BackgroundImageOpacity = 100;
|
||||
}
|
||||
|
||||
Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int BackgroundImageOpacity
|
||||
{
|
||||
get => _settings.BackgroundImageOpacity;
|
||||
set
|
||||
{
|
||||
if (_settings.BackgroundImageOpacity != value)
|
||||
{
|
||||
_settings.BackgroundImageOpacity = value;
|
||||
OnPropertyChanged();
|
||||
Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int BackgroundImageBrightness
|
||||
{
|
||||
get => _settings.BackgroundImageBrightness;
|
||||
set
|
||||
{
|
||||
if (_settings.BackgroundImageBrightness != value)
|
||||
{
|
||||
_settings.BackgroundImageBrightness = value;
|
||||
OnPropertyChanged();
|
||||
Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int BackgroundImageBlurAmount
|
||||
{
|
||||
get => _settings.BackgroundImageBlurAmount;
|
||||
set
|
||||
{
|
||||
if (_settings.BackgroundImageBlurAmount != value)
|
||||
{
|
||||
_settings.BackgroundImageBlurAmount = value;
|
||||
OnPropertyChanged();
|
||||
Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public BackgroundImageFit BackgroundImageFit
|
||||
{
|
||||
get => _settings.BackgroundImageFit;
|
||||
set
|
||||
{
|
||||
if (_settings.BackgroundImageFit != value)
|
||||
{
|
||||
_settings.BackgroundImageFit = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(BackgroundImageFitIndex));
|
||||
Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int BackgroundImageFitIndex
|
||||
{
|
||||
// Naming between UI facing string and enum is a bit confusing, but the enum fields
|
||||
// are based on XAML Stretch enum values. So I'm choosing to keep the confusion here, close
|
||||
// to the UI.
|
||||
// - BackgroundImageFit.Fill corresponds to "Stretch"
|
||||
// - BackgroundImageFit.UniformToFill corresponds to "Fill"
|
||||
get => BackgroundImageFit switch
|
||||
{
|
||||
BackgroundImageFit.Fill => 1,
|
||||
_ => 0,
|
||||
};
|
||||
set => BackgroundImageFit = value switch
|
||||
{
|
||||
1 => BackgroundImageFit.Fill,
|
||||
_ => BackgroundImageFit.UniformToFill,
|
||||
};
|
||||
}
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsColorizationDetailsExpanded { get; set; }
|
||||
|
||||
public bool IsCustomTintVisible => _settings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.Image;
|
||||
|
||||
public bool IsCustomTintIntensityVisible => _settings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.WindowsAccentColor or ColorizationMode.Image;
|
||||
|
||||
public bool IsBackgroundControlsVisible => _settings.ColorizationMode is ColorizationMode.Image;
|
||||
|
||||
public bool IsNoBackgroundVisible => _settings.ColorizationMode is ColorizationMode.None;
|
||||
|
||||
public bool IsAccentColorControlsVisible => _settings.ColorizationMode is ColorizationMode.WindowsAccentColor;
|
||||
|
||||
public AcrylicBackdropParameters EffectiveBackdrop { get; private set; } = new(Colors.Black, Colors.Black, 0.5f, 0.5f);
|
||||
|
||||
public ElementTheme EffectiveTheme => _elementThemeOverride ?? _themeService.Current.Theme;
|
||||
|
||||
public Color EffectiveThemeColor => ColorizationMode switch
|
||||
{
|
||||
ColorizationMode.WindowsAccentColor => _currentSystemAccentColor,
|
||||
ColorizationMode.CustomColor or ColorizationMode.Image => ThemeColor,
|
||||
_ => Colors.Transparent,
|
||||
};
|
||||
|
||||
// Since the blur amount is absolute, we need to scale it down for the preview (which is smaller than full screen).
|
||||
public int EffectiveBackgroundImageBlurAmount => (int)Math.Round(BackgroundImageBlurAmount / 4f);
|
||||
|
||||
public double EffectiveBackgroundImageBrightness => BackgroundImageBrightness / 100.0;
|
||||
|
||||
public ImageSource? EffectiveBackgroundImageSource =>
|
||||
ColorizationMode is ColorizationMode.Image
|
||||
&& !string.IsNullOrWhiteSpace(BackgroundImagePath)
|
||||
&& Uri.TryCreate(BackgroundImagePath, UriKind.RelativeOrAbsolute, out var uri)
|
||||
? new Microsoft.UI.Xaml.Media.Imaging.BitmapImage(uri)
|
||||
: null;
|
||||
|
||||
public AppearanceSettingsViewModel(IThemeService themeService, SettingsModel settings)
|
||||
{
|
||||
_themeService = themeService;
|
||||
_themeService.ThemeChanged += ThemeServiceOnThemeChanged;
|
||||
_settings = settings;
|
||||
|
||||
_uiSettings = new UISettings();
|
||||
_uiSettings.ColorValuesChanged += UiSettingsOnColorValuesChanged;
|
||||
UpdateAccentColor(_uiSettings);
|
||||
|
||||
Reapply();
|
||||
|
||||
IsColorizationDetailsExpanded = _settings.ColorizationMode != ColorizationMode.None;
|
||||
}
|
||||
|
||||
private void UiSettingsOnColorValuesChanged(UISettings sender, object args) => _uiDispatcher.TryEnqueue(() => UpdateAccentColor(sender));
|
||||
|
||||
private void UpdateAccentColor(UISettings sender)
|
||||
{
|
||||
_currentSystemAccentColor = sender.GetColorValue(UIColorType.Accent);
|
||||
if (ColorizationMode == ColorizationMode.WindowsAccentColor)
|
||||
{
|
||||
ThemeColor = _currentSystemAccentColor;
|
||||
}
|
||||
}
|
||||
|
||||
private void ThemeServiceOnThemeChanged(object? sender, ThemeChangedEventArgs e)
|
||||
{
|
||||
_saveTimer.Debounce(Reapply, TimeSpan.FromMilliseconds(200));
|
||||
}
|
||||
|
||||
private void Save()
|
||||
{
|
||||
SettingsModel.SaveSettings(_settings);
|
||||
_saveTimer.Debounce(Reapply, TimeSpan.FromMilliseconds(200));
|
||||
}
|
||||
|
||||
private void Reapply()
|
||||
{
|
||||
// Theme services recalculates effective color and opacity based on current settings.
|
||||
EffectiveBackdrop = _themeService.Current.BackdropParameters;
|
||||
OnPropertyChanged(nameof(EffectiveBackdrop));
|
||||
OnPropertyChanged(nameof(EffectiveBackgroundImageBrightness));
|
||||
OnPropertyChanged(nameof(EffectiveBackgroundImageSource));
|
||||
OnPropertyChanged(nameof(EffectiveThemeColor));
|
||||
OnPropertyChanged(nameof(EffectiveBackgroundImageBlurAmount));
|
||||
|
||||
// LOAD BEARING:
|
||||
// We need to cycle through the EffectiveTheme property to force reload of resources.
|
||||
_elementThemeOverride = ElementTheme.Light;
|
||||
OnPropertyChanged(nameof(EffectiveTheme));
|
||||
_elementThemeOverride = ElementTheme.Dark;
|
||||
OnPropertyChanged(nameof(EffectiveTheme));
|
||||
_elementThemeOverride = null;
|
||||
OnPropertyChanged(nameof(EffectiveTheme));
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void ResetBackgroundImageProperties()
|
||||
{
|
||||
BackgroundImageBrightness = 0;
|
||||
BackgroundImageBlurAmount = 0;
|
||||
BackgroundImageFit = BackgroundImageFit.UniformToFill;
|
||||
BackgroundImageOpacity = 100;
|
||||
ColorIntensity = 0;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_uiSettings.ColorValuesChanged -= UiSettingsOnColorValuesChanged;
|
||||
_themeService.ThemeChanged -= ThemeServiceOnThemeChanged;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// 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;
|
||||
|
||||
public enum BackgroundImageFit
|
||||
{
|
||||
Fill,
|
||||
UniformToFill,
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public enum ColorizationMode
|
||||
{
|
||||
None,
|
||||
WindowsAccentColor,
|
||||
CustomColor,
|
||||
Image,
|
||||
}
|
||||
@@ -34,6 +34,6 @@ public sealed partial class CommandPaletteHost : AppExtensionHost, IExtensionHos
|
||||
|
||||
public override string? GetExtensionDisplayName()
|
||||
{
|
||||
return Extension?.ExtensionDisplayName;
|
||||
return Extension?.ExtensionDisplayName ?? _builtInProvider?.DisplayName ?? _builtInProvider?.Id;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public partial class MainWindowViewModel : ObservableObject, IDisposable
|
||||
{
|
||||
private readonly IThemeService _themeService;
|
||||
private readonly DispatcherQueue _uiDispatcherQueue = DispatcherQueue.GetForCurrentThread()!;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ImageSource? BackgroundImageSource { get; private set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial Stretch BackgroundImageStretch { get; private set; } = Stretch.Fill;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial double BackgroundImageOpacity { get; private set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial Color BackgroundImageTint { get; private set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial double BackgroundImageTintIntensity { get; private set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial int BackgroundImageBlurAmount { get; private set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial double BackgroundImageBrightness { get; private set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool ShowBackgroundImage { get; private set; }
|
||||
|
||||
public MainWindowViewModel(IThemeService themeService)
|
||||
{
|
||||
_themeService = themeService;
|
||||
_themeService.ThemeChanged += ThemeService_ThemeChanged;
|
||||
}
|
||||
|
||||
private void ThemeService_ThemeChanged(object? sender, ThemeChangedEventArgs e)
|
||||
{
|
||||
_uiDispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
BackgroundImageSource = _themeService.Current.BackgroundImageSource;
|
||||
BackgroundImageStretch = _themeService.Current.BackgroundImageStretch;
|
||||
BackgroundImageOpacity = _themeService.Current.BackgroundImageOpacity;
|
||||
|
||||
BackgroundImageBrightness = _themeService.Current.BackgroundBrightness;
|
||||
BackgroundImageTint = _themeService.Current.Tint;
|
||||
BackgroundImageTintIntensity = _themeService.Current.TintIntensity;
|
||||
BackgroundImageBlurAmount = _themeService.Current.BlurAmount;
|
||||
|
||||
ShowBackgroundImage = BackgroundImageSource != null;
|
||||
});
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_themeService.ThemeChanged -= ThemeService_ThemeChanged;
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
@@ -23,11 +23,12 @@
|
||||
<PackageReference Include="CommunityToolkit.Common" />
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" />
|
||||
<PackageReference Include="AdaptiveCards.Templating" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Extensions" />
|
||||
<PackageReference Include="Microsoft.Bot.AdaptiveExpressions.Core" />
|
||||
<PackageReference Include="AdaptiveCards.ObjectModel.WinUI3" GeneratePathProperty="true">
|
||||
<ExcludeAssets>compile</ExcludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="AdaptiveCards.Rendering.WinUI3" GeneratePathProperty="True" >
|
||||
<PackageReference Include="AdaptiveCards.Rendering.WinUI3" GeneratePathProperty="True">
|
||||
<ExcludeAssets>compile</ExcludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace Microsoft.CmdPal.UI.ViewModels.Properties {
|
||||
// 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.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
public class Resources {
|
||||
@@ -411,6 +411,15 @@ namespace Microsoft.CmdPal.UI.ViewModels.Properties {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Pick background image.
|
||||
/// </summary>
|
||||
public static string builtin_settings_appearance_pick_background_image_title {
|
||||
get {
|
||||
return ResourceManager.GetString("builtin_settings_appearance_pick_background_image_title", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to {0} extensions found.
|
||||
/// </summary>
|
||||
|
||||
@@ -239,4 +239,7 @@
|
||||
<data name="builtin_settings_extension_n_extensions_installed" xml:space="preserve">
|
||||
<value>{0} extensions installed</value>
|
||||
</data>
|
||||
<data name="builtin_settings_appearance_pick_background_image_title" xml:space="preserve">
|
||||
<value>Pick background image</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -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.
|
||||
|
||||
using Windows.UI;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
|
||||
public sealed record AcrylicBackdropParameters(Color TintColor, Color FallbackColor, float TintOpacity, float LuminosityOpacity);
|
||||
@@ -0,0 +1,39 @@
|
||||
// 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.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Provides theme-related values for the Command Palette and notifies listeners about
|
||||
/// changes that affect visual appearance (theme, tint, background image, and backdrop).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Implementations are expected to monitor system/app theme changes and raise
|
||||
/// <see cref="ThemeChanged"/> accordingly. Consumers should call <see cref="Initialize"/>
|
||||
/// once to hook required sources and then query properties/methods for the current visuals.
|
||||
/// </remarks>
|
||||
public interface IThemeService
|
||||
{
|
||||
/// <summary>
|
||||
/// Occurs when the effective theme or any visual-affecting setting changes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Triggered for changes such as app theme (light/dark/default), background image,
|
||||
/// tint/accent, or backdrop parameters that would require UI to refresh styling.
|
||||
/// </remarks>
|
||||
event EventHandler<ThemeChangedEventArgs>? ThemeChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the theme service and starts listening for theme-related changes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Safe to call once during application startup before consuming the service.
|
||||
/// </remarks>
|
||||
void Initialize();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current theme settings.
|
||||
/// </summary>
|
||||
ThemeSnapshot Current { get; }
|
||||
}
|
||||
@@ -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.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Event arguments for theme-related changes. </summary>
|
||||
public class ThemeChangedEventArgs : EventArgs;
|
||||
@@ -0,0 +1,62 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a snapshot of theme-related visual settings, including accent color, theme preference, and background
|
||||
/// image configuration, for use in rendering the Command Palette UI.
|
||||
/// </summary>
|
||||
public sealed class ThemeSnapshot
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the accent tint color used by the Command Palette visuals.
|
||||
/// </summary>
|
||||
public required Color Tint { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the accent tint color used by the Command Palette visuals.
|
||||
/// </summary>
|
||||
public required float TintIntensity { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the configured application theme preference.
|
||||
/// </summary>
|
||||
public required ElementTheme Theme { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the image source to render as the background, if any.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Returns <see langword="null"/> when no background image is configured.
|
||||
/// </remarks>
|
||||
public required ImageSource? BackgroundImageSource { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the stretch mode used to lay out the background image.
|
||||
/// </summary>
|
||||
public required Stretch BackgroundImageStretch { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the opacity applied to the background image.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// A value in the range [0, 1], where 0 is fully transparent and 1 is fully opaque.
|
||||
/// </value>
|
||||
public required double BackgroundImageOpacity { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the effective acrylic backdrop parameters based on current settings and theme.
|
||||
/// </summary>
|
||||
/// <returns>The resolved <c>AcrylicBackdropParameters</c> to apply.</returns>
|
||||
public required AcrylicBackdropParameters BackdropParameters { get; init; }
|
||||
|
||||
public required int BlurAmount { get; init; }
|
||||
|
||||
public required float BackgroundBrightness { get; init; }
|
||||
}
|
||||
@@ -11,7 +11,9 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Microsoft.UI;
|
||||
using Windows.Foundation;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
@@ -62,6 +64,24 @@ public partial class SettingsModel : ObservableObject
|
||||
|
||||
public EscapeKeyBehavior EscapeKeyBehaviorSetting { get; set; } = EscapeKeyBehavior.ClearSearchFirstThenGoBack;
|
||||
|
||||
public UserTheme Theme { get; set; } = UserTheme.Default;
|
||||
|
||||
public ColorizationMode ColorizationMode { get; set; }
|
||||
|
||||
public Color CustomThemeColor { get; set; } = Colors.Transparent;
|
||||
|
||||
public int CustomThemeColorIntensity { get; set; } = 100;
|
||||
|
||||
public int BackgroundImageOpacity { get; set; } = 20;
|
||||
|
||||
public int BackgroundImageBlurAmount { get; set; }
|
||||
|
||||
public int BackgroundImageBrightness { get; set; }
|
||||
|
||||
public BackgroundImageFit BackgroundImageFit { get; set; }
|
||||
|
||||
public string? BackgroundImagePath { get; set; }
|
||||
|
||||
// END SETTINGS
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using Microsoft.CmdPal.Core.Common.Services;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
@@ -29,6 +31,8 @@ public partial class SettingsViewModel : INotifyPropertyChanged
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
public AppearanceSettingsViewModel Appearance { get; }
|
||||
|
||||
public HotkeySettings? Hotkey
|
||||
{
|
||||
get => _settings.Hotkey;
|
||||
@@ -179,6 +183,9 @@ public partial class SettingsViewModel : INotifyPropertyChanged
|
||||
_settings = settings;
|
||||
_serviceProvider = serviceProvider;
|
||||
|
||||
var themeService = serviceProvider.GetRequiredService<IThemeService>();
|
||||
Appearance = new AppearanceSettingsViewModel(themeService, _settings);
|
||||
|
||||
var activeProviders = GetCommandProviders();
|
||||
var allProviderSettings = _settings.ProviderSettings;
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ using WyHash;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public sealed partial class TopLevelViewModel : ObservableObject, IListItem
|
||||
public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IExtendedAttributesProvider
|
||||
{
|
||||
private readonly SettingsModel _settings;
|
||||
private readonly ProviderSettings _providerSettings;
|
||||
@@ -232,6 +232,13 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem
|
||||
{
|
||||
UpdateInitialIcon();
|
||||
}
|
||||
else if (e.PropertyName == nameof(CommandItem.DataPackage))
|
||||
{
|
||||
DoOnUiThread(() =>
|
||||
{
|
||||
OnPropertyChanged(nameof(CommandItem.DataPackage));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -394,4 +401,12 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem
|
||||
{
|
||||
return $"{nameof(TopLevelViewModel)}: {Id} ({Title}) - display: {DisplayTitle} - fallback: {IsFallback} - enabled: {IsEnabled}";
|
||||
}
|
||||
|
||||
public IDictionary<string, object?> GetProperties()
|
||||
{
|
||||
return new Dictionary<string, object?>
|
||||
{
|
||||
[WellKnownExtensionAttributes.DataPackage] = _commandItemViewModel?.DataPackage,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public enum UserTheme
|
||||
{
|
||||
Default,
|
||||
Light,
|
||||
Dark,
|
||||
}
|
||||
@@ -4,19 +4,23 @@
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:controls="using:Microsoft.CmdPal.UI.Controls"
|
||||
xmlns:local="using:Microsoft.CmdPal.UI">
|
||||
xmlns:local="using:Microsoft.CmdPal.UI"
|
||||
xmlns:services="using:Microsoft.CmdPal.UI.Services">
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
|
||||
<!-- Other merged dictionaries here -->
|
||||
<ResourceDictionary Source="Styles/Colors.xaml" />
|
||||
<ResourceDictionary Source="Styles/TextBlock.xaml" />
|
||||
<ResourceDictionary Source="Styles/TextBox.xaml" />
|
||||
<ResourceDictionary Source="Styles/Settings.xaml" />
|
||||
<ResourceDictionary Source="Controls/Tag.xaml" />
|
||||
<ResourceDictionary Source="Controls/KeyVisual/KeyVisual.xaml" />
|
||||
<ResourceDictionary Source="Controls/IsEnabledTextBlock.xaml" />
|
||||
<ResourceDictionary Source="ms-appx:///Styles/Colors.xaml" />
|
||||
<ResourceDictionary Source="ms-appx:///Styles/TextBlock.xaml" />
|
||||
<ResourceDictionary Source="ms-appx:///Styles/TextBox.xaml" />
|
||||
<ResourceDictionary Source="ms-appx:///Styles/Settings.xaml" />
|
||||
<ResourceDictionary Source="ms-appx:///Controls/Tag.xaml" />
|
||||
<ResourceDictionary Source="ms-appx:///Controls/KeyVisual/KeyVisual.xaml" />
|
||||
<ResourceDictionary Source="ms-appx:///Controls/IsEnabledTextBlock.xaml" />
|
||||
<!-- Default theme dictionary -->
|
||||
<ResourceDictionary Source="ms-appx:///Styles/Theme.Normal.xaml" />
|
||||
<services:MutableOverridesDictionary />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
<!-- Other app resources here -->
|
||||
|
||||
|
||||
@@ -24,9 +24,11 @@ using Microsoft.CmdPal.Ext.WindowsTerminal;
|
||||
using Microsoft.CmdPal.Ext.WindowWalker;
|
||||
using Microsoft.CmdPal.Ext.WinGet;
|
||||
using Microsoft.CmdPal.UI.Helpers;
|
||||
using Microsoft.CmdPal.UI.Services;
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels.BuiltinCommands;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Models;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.PowerToys.Telemetry;
|
||||
@@ -112,6 +114,17 @@ public partial class App : Application
|
||||
// Root services
|
||||
services.AddSingleton(TaskScheduler.FromCurrentSynchronizationContext());
|
||||
|
||||
AddBuiltInCommands(services);
|
||||
|
||||
AddCoreServices(services);
|
||||
|
||||
AddUIServices(services);
|
||||
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
|
||||
private static void AddBuiltInCommands(ServiceCollection services)
|
||||
{
|
||||
// Built-in Commands. Order matters - this is the order they'll be presented by default.
|
||||
var allApps = new AllAppsCommandProvider();
|
||||
var files = new IndexerCommandsProvider();
|
||||
@@ -154,17 +167,32 @@ public partial class App : Application
|
||||
services.AddSingleton<ICommandProvider, TimeDateCommandsProvider>();
|
||||
services.AddSingleton<ICommandProvider, SystemCommandExtensionProvider>();
|
||||
services.AddSingleton<ICommandProvider, RemoteDesktopCommandProvider>();
|
||||
}
|
||||
|
||||
private static void AddUIServices(ServiceCollection services)
|
||||
{
|
||||
// Models
|
||||
services.AddSingleton<TopLevelCommandManager>();
|
||||
services.AddSingleton<AliasManager>();
|
||||
services.AddSingleton<HotkeyManager>();
|
||||
var sm = SettingsModel.LoadSettings();
|
||||
services.AddSingleton(sm);
|
||||
var state = AppStateModel.LoadState();
|
||||
services.AddSingleton(state);
|
||||
services.AddSingleton<IExtensionService, ExtensionService>();
|
||||
|
||||
// Services
|
||||
services.AddSingleton<TopLevelCommandManager>();
|
||||
services.AddSingleton<AliasManager>();
|
||||
services.AddSingleton<HotkeyManager>();
|
||||
|
||||
services.AddSingleton<MainWindowViewModel>();
|
||||
services.AddSingleton<TrayIconService>();
|
||||
|
||||
services.AddSingleton<IThemeService, ThemeService>();
|
||||
services.AddSingleton<ResourceSwapper>();
|
||||
}
|
||||
|
||||
private static void AddCoreServices(ServiceCollection services)
|
||||
{
|
||||
// Core services
|
||||
services.AddSingleton<IExtensionService, ExtensionService>();
|
||||
services.AddSingleton<IRunHistoryService, RunHistoryService>();
|
||||
|
||||
services.AddSingleton<IRootPageService, PowerToysRootPageService>();
|
||||
@@ -174,7 +202,5 @@ public partial class App : Application
|
||||
// ViewModels
|
||||
services.AddSingleton<ShellViewModel>();
|
||||
services.AddSingleton<IPageViewModelFactoryService, CommandPalettePageViewModelFactory>();
|
||||
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,449 @@
|
||||
// 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.Numerics;
|
||||
using ManagedCommon;
|
||||
using Microsoft.Graphics.Canvas.Effects;
|
||||
using Microsoft.UI;
|
||||
using Microsoft.UI.Composition;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Hosting;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Controls;
|
||||
|
||||
internal sealed partial class BlurImageControl : Control
|
||||
{
|
||||
private const string ImageSourceParameterName = "ImageSource";
|
||||
|
||||
private const string BrightnessEffectName = "Brightness";
|
||||
private const string BrightnessOverlayEffectName = "BrightnessOverlay";
|
||||
private const string BlurEffectName = "Blur";
|
||||
private const string TintBlendEffectName = "TintBlend";
|
||||
private const string TintEffectName = "Tint";
|
||||
|
||||
#pragma warning disable CA1507 // Use nameof to express symbol names ... some of these refer to effect properties that are separate from the class properties
|
||||
private static readonly string BrightnessSource1AmountEffectProperty = GetPropertyName(BrightnessEffectName, "Source1Amount");
|
||||
private static readonly string BrightnessSource2AmountEffectProperty = GetPropertyName(BrightnessEffectName, "Source2Amount");
|
||||
private static readonly string BrightnessOverlayColorEffectProperty = GetPropertyName(BrightnessOverlayEffectName, "Color");
|
||||
private static readonly string BlurBlurAmountEffectProperty = GetPropertyName(BlurEffectName, "BlurAmount");
|
||||
private static readonly string TintColorEffectProperty = GetPropertyName(TintEffectName, "Color");
|
||||
#pragma warning restore CA1507
|
||||
|
||||
private static readonly string[] AnimatableProperties = [
|
||||
BrightnessSource1AmountEffectProperty,
|
||||
BrightnessSource2AmountEffectProperty,
|
||||
BrightnessOverlayColorEffectProperty,
|
||||
BlurBlurAmountEffectProperty,
|
||||
TintColorEffectProperty
|
||||
];
|
||||
|
||||
public static readonly DependencyProperty ImageSourceProperty =
|
||||
DependencyProperty.Register(
|
||||
nameof(ImageSource),
|
||||
typeof(ImageSource),
|
||||
typeof(BlurImageControl),
|
||||
new PropertyMetadata(null, OnImageChanged));
|
||||
|
||||
public static readonly DependencyProperty ImageStretchProperty =
|
||||
DependencyProperty.Register(
|
||||
nameof(ImageStretch),
|
||||
typeof(Stretch),
|
||||
typeof(BlurImageControl),
|
||||
new PropertyMetadata(Stretch.UniformToFill, OnImageStretchChanged));
|
||||
|
||||
public static readonly DependencyProperty ImageOpacityProperty =
|
||||
DependencyProperty.Register(
|
||||
nameof(ImageOpacity),
|
||||
typeof(double),
|
||||
typeof(BlurImageControl),
|
||||
new PropertyMetadata(1.0, OnOpacityChanged));
|
||||
|
||||
public static readonly DependencyProperty ImageBrightnessProperty =
|
||||
DependencyProperty.Register(
|
||||
nameof(ImageBrightness),
|
||||
typeof(double),
|
||||
typeof(BlurImageControl),
|
||||
new PropertyMetadata(1.0, OnBrightnessChanged));
|
||||
|
||||
public static readonly DependencyProperty BlurAmountProperty =
|
||||
DependencyProperty.Register(
|
||||
nameof(BlurAmount),
|
||||
typeof(double),
|
||||
typeof(BlurImageControl),
|
||||
new PropertyMetadata(0.0, OnBlurAmountChanged));
|
||||
|
||||
public static readonly DependencyProperty TintColorProperty =
|
||||
DependencyProperty.Register(
|
||||
nameof(TintColor),
|
||||
typeof(Color),
|
||||
typeof(BlurImageControl),
|
||||
new PropertyMetadata(Colors.Transparent, OnVisualPropertyChanged));
|
||||
|
||||
public static readonly DependencyProperty TintIntensityProperty =
|
||||
DependencyProperty.Register(
|
||||
nameof(TintIntensity),
|
||||
typeof(double),
|
||||
typeof(BlurImageControl),
|
||||
new PropertyMetadata(0.0, OnVisualPropertyChanged));
|
||||
|
||||
private Compositor? _compositor;
|
||||
private SpriteVisual? _effectVisual;
|
||||
private CompositionEffectBrush? _effectBrush;
|
||||
private CompositionSurfaceBrush? _imageBrush;
|
||||
|
||||
public BlurImageControl()
|
||||
{
|
||||
this.DefaultStyleKey = typeof(BlurImageControl);
|
||||
this.Loaded += OnLoaded;
|
||||
this.SizeChanged += OnSizeChanged;
|
||||
}
|
||||
|
||||
public ImageSource ImageSource
|
||||
{
|
||||
get => (ImageSource)GetValue(ImageSourceProperty);
|
||||
set => SetValue(ImageSourceProperty, value);
|
||||
}
|
||||
|
||||
public Stretch ImageStretch
|
||||
{
|
||||
get => (Stretch)GetValue(ImageStretchProperty);
|
||||
set => SetValue(ImageStretchProperty, value);
|
||||
}
|
||||
|
||||
public double ImageOpacity
|
||||
{
|
||||
get => (double)GetValue(ImageOpacityProperty);
|
||||
set => SetValue(ImageOpacityProperty, value);
|
||||
}
|
||||
|
||||
public double ImageBrightness
|
||||
{
|
||||
get => (double)GetValue(ImageBrightnessProperty);
|
||||
set => SetValue(ImageBrightnessProperty, Math.Clamp(value, -1, 1));
|
||||
}
|
||||
|
||||
public double BlurAmount
|
||||
{
|
||||
get => (double)GetValue(BlurAmountProperty);
|
||||
set => SetValue(BlurAmountProperty, value);
|
||||
}
|
||||
|
||||
public Color TintColor
|
||||
{
|
||||
get => (Color)GetValue(TintColorProperty);
|
||||
set => SetValue(TintColorProperty, value);
|
||||
}
|
||||
|
||||
public double TintIntensity
|
||||
{
|
||||
get => (double)GetValue(TintIntensityProperty);
|
||||
set => SetValue(TintIntensityProperty, value);
|
||||
}
|
||||
|
||||
private static void OnImageStretchChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is BlurImageControl control && control._imageBrush != null)
|
||||
{
|
||||
control._imageBrush.Stretch = ConvertStretch((Stretch)e.NewValue);
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnVisualPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is BlurImageControl control && control._compositor != null)
|
||||
{
|
||||
control.UpdateEffect();
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnOpacityChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is BlurImageControl control && control._effectVisual != null)
|
||||
{
|
||||
control._effectVisual.Opacity = (float)(double)e.NewValue;
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnBlurAmountChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is BlurImageControl control && control._effectBrush != null)
|
||||
{
|
||||
control.UpdateEffect();
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnBrightnessChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is BlurImageControl control && control._effectBrush != null)
|
||||
{
|
||||
control.UpdateEffect();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
InitializeComposition();
|
||||
}
|
||||
|
||||
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
|
||||
{
|
||||
if (_effectVisual != null)
|
||||
{
|
||||
_effectVisual.Size = new Vector2(
|
||||
(float)Math.Max(1, e.NewSize.Width),
|
||||
(float)Math.Max(1, e.NewSize.Height));
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnImageChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is not BlurImageControl control)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
control.EnsureEffect(force: true);
|
||||
control.UpdateEffect();
|
||||
}
|
||||
|
||||
private void InitializeComposition()
|
||||
{
|
||||
var visual = ElementCompositionPreview.GetElementVisual(this);
|
||||
_compositor = visual.Compositor;
|
||||
|
||||
_effectVisual = _compositor.CreateSpriteVisual();
|
||||
_effectVisual.Size = new Vector2(
|
||||
(float)Math.Max(1, ActualWidth),
|
||||
(float)Math.Max(1, ActualHeight));
|
||||
_effectVisual.Opacity = (float)ImageOpacity;
|
||||
|
||||
ElementCompositionPreview.SetElementChildVisual(this, _effectVisual);
|
||||
|
||||
UpdateEffect();
|
||||
}
|
||||
|
||||
private void EnsureEffect(bool force = false)
|
||||
{
|
||||
if (_compositor is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_effectBrush is not null && !force)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var imageSource = new CompositionEffectSourceParameter(ImageSourceParameterName);
|
||||
|
||||
// 1) Brightness via ArithmeticCompositeEffect
|
||||
// We blend between the original image and either black or white,
|
||||
// depending on whether we want to darken or brighten. BrightnessEffect isn't supported
|
||||
// in the composition graph.
|
||||
var brightnessEffect = new ArithmeticCompositeEffect
|
||||
{
|
||||
Name = BrightnessEffectName,
|
||||
Source1 = imageSource, // original image
|
||||
Source2 = new ColorSourceEffect
|
||||
{
|
||||
Name = BrightnessOverlayEffectName,
|
||||
Color = Colors.Black, // we'll swap black/white via properties
|
||||
},
|
||||
|
||||
MultiplyAmount = 0.0f,
|
||||
Source1Amount = 1.0f, // original
|
||||
Source2Amount = 0.0f, // overlay
|
||||
Offset = 0.0f,
|
||||
};
|
||||
|
||||
// 2) Blur
|
||||
var blurEffect = new GaussianBlurEffect
|
||||
{
|
||||
Name = BlurEffectName,
|
||||
BlurAmount = 0.0f,
|
||||
BorderMode = EffectBorderMode.Hard,
|
||||
Optimization = EffectOptimization.Balanced,
|
||||
Source = brightnessEffect,
|
||||
};
|
||||
|
||||
// 3) Tint (always in the chain; intensity via alpha)
|
||||
var tintEffect = new BlendEffect
|
||||
{
|
||||
Name = TintBlendEffectName,
|
||||
Background = blurEffect,
|
||||
Foreground = new ColorSourceEffect
|
||||
{
|
||||
Name = TintEffectName,
|
||||
Color = Colors.Transparent,
|
||||
},
|
||||
Mode = BlendEffectMode.Multiply,
|
||||
};
|
||||
|
||||
var effectFactory = _compositor.CreateEffectFactory(tintEffect, AnimatableProperties);
|
||||
|
||||
_effectBrush?.Dispose();
|
||||
_effectBrush = effectFactory.CreateBrush();
|
||||
|
||||
// Set initial source
|
||||
if (ImageSource is not null)
|
||||
{
|
||||
_imageBrush ??= _compositor.CreateSurfaceBrush();
|
||||
LoadImageAsync(ImageSource);
|
||||
_effectBrush.SetSourceParameter(ImageSourceParameterName, _imageBrush);
|
||||
}
|
||||
else
|
||||
{
|
||||
_effectBrush.SetSourceParameter(ImageSourceParameterName, _compositor.CreateBackdropBrush());
|
||||
}
|
||||
|
||||
if (_effectVisual is not null)
|
||||
{
|
||||
_effectVisual.Brush = _effectBrush;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateEffect()
|
||||
{
|
||||
if (_compositor is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
EnsureEffect();
|
||||
if (_effectBrush is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var props = _effectBrush.Properties;
|
||||
|
||||
// Brightness
|
||||
var b = (float)Math.Clamp(ImageBrightness, -1.0, 1.0);
|
||||
|
||||
float source1Amount;
|
||||
float source2Amount;
|
||||
Color overlayColor;
|
||||
|
||||
if (b >= 0)
|
||||
{
|
||||
// Brighten: blend towards white
|
||||
overlayColor = Colors.White;
|
||||
source1Amount = 1.0f - b; // original image contribution
|
||||
source2Amount = b; // white overlay contribution
|
||||
}
|
||||
else
|
||||
{
|
||||
// Darken: blend towards black
|
||||
overlayColor = Colors.Black;
|
||||
var t = -b; // 0..1
|
||||
source1Amount = 1.0f - t; // original image
|
||||
source2Amount = t; // black overlay
|
||||
}
|
||||
|
||||
props.InsertScalar(BrightnessSource1AmountEffectProperty, source1Amount);
|
||||
props.InsertScalar(BrightnessSource2AmountEffectProperty, source2Amount);
|
||||
props.InsertColor(BrightnessOverlayColorEffectProperty, overlayColor);
|
||||
|
||||
// Blur
|
||||
props.InsertScalar(BlurBlurAmountEffectProperty, (float)BlurAmount);
|
||||
|
||||
// Tint
|
||||
var tintColor = TintColor;
|
||||
var clampedIntensity = (float)Math.Clamp(TintIntensity, 0.0, 1.0);
|
||||
|
||||
var adjustedColor = Color.FromArgb(
|
||||
(byte)(clampedIntensity * 255),
|
||||
tintColor.R,
|
||||
tintColor.G,
|
||||
tintColor.B);
|
||||
|
||||
props.InsertColor(TintColorEffectProperty, adjustedColor);
|
||||
}
|
||||
|
||||
private void LoadImageAsync(ImageSource imageSource)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (imageSource is not Microsoft.UI.Xaml.Media.Imaging.BitmapImage bitmapImage)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_imageBrush ??= _compositor?.CreateSurfaceBrush();
|
||||
if (_imageBrush is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.LogDebug($"Starting load of BlurImageControl from '{bitmapImage.UriSource}'");
|
||||
var loadedSurface = LoadedImageSurface.StartLoadFromUri(bitmapImage.UriSource);
|
||||
loadedSurface.LoadCompleted += OnLoadedSurfaceOnLoadCompleted;
|
||||
SetLoadedSurfaceToBrush(loadedSurface);
|
||||
_effectBrush?.SetSourceParameter(ImageSourceParameterName, _imageBrush);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to load image for BlurImageControl: {0}", ex);
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
void OnLoadedSurfaceOnLoadCompleted(LoadedImageSurface loadedSurface, LoadedImageSourceLoadCompletedEventArgs e)
|
||||
{
|
||||
switch (e.Status)
|
||||
{
|
||||
case LoadedImageSourceLoadStatus.Success:
|
||||
Logger.LogDebug($"BlurImageControl loaded successfully: has _imageBrush? {_imageBrush != null}");
|
||||
|
||||
try
|
||||
{
|
||||
SetLoadedSurfaceToBrush(loadedSurface);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to set surface in BlurImageControl", ex);
|
||||
throw;
|
||||
}
|
||||
|
||||
break;
|
||||
case LoadedImageSourceLoadStatus.NetworkError:
|
||||
case LoadedImageSourceLoadStatus.InvalidFormat:
|
||||
case LoadedImageSourceLoadStatus.Other:
|
||||
default:
|
||||
Logger.LogError($"Failed to load image for BlurImageControl: Load status {e.Status}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SetLoadedSurfaceToBrush(LoadedImageSurface loadedSurface)
|
||||
{
|
||||
var surfaceBrush = _imageBrush;
|
||||
if (surfaceBrush is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
surfaceBrush.Surface = loadedSurface;
|
||||
surfaceBrush.Stretch = ConvertStretch(ImageStretch);
|
||||
surfaceBrush.BitmapInterpolationMode = CompositionBitmapInterpolationMode.Linear;
|
||||
}
|
||||
|
||||
private static CompositionStretch ConvertStretch(Stretch stretch)
|
||||
{
|
||||
return stretch switch
|
||||
{
|
||||
Stretch.None => CompositionStretch.None,
|
||||
Stretch.Fill => CompositionStretch.Fill,
|
||||
Stretch.Uniform => CompositionStretch.Uniform,
|
||||
Stretch.UniformToFill => CompositionStretch.UniformToFill,
|
||||
_ => CompositionStretch.UniformToFill,
|
||||
};
|
||||
}
|
||||
|
||||
private static string GetPropertyName(string effectName, string propertyName) => $"{effectName}.{propertyName}";
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<UserControl
|
||||
x:Class="Microsoft.CmdPal.UI.Controls.ColorPalette"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="using:Microsoft.CmdPal.UI.Controls"
|
||||
xmlns:localConverters="using:Microsoft.CmdPal.UI.Converters"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:toolkitConverters="using:CommunityToolkit.WinUI.Converters"
|
||||
xmlns:ui="using:CommunityToolkit.WinUI"
|
||||
mc:Ignorable="d">
|
||||
<UserControl.Resources>
|
||||
|
||||
<toolkitConverters:ColorToDisplayNameConverter x:Key="ColorToDisplayNameConverter" />
|
||||
<localConverters:ContrastBrushConverter x:Key="ContrastBrushConverter" />
|
||||
|
||||
<Style x:Key="PaletteGridViewItemStyle" TargetType="GridViewItem">
|
||||
<Setter Property="FontFamily" Value="{ThemeResource ContentControlThemeFontFamily}" />
|
||||
<Setter Property="FontSize" Value="{ThemeResource ControlContentThemeFontSize}" />
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="Foreground" Value="{ThemeResource SystemControlForegroundBaseHighBrush}" />
|
||||
<Setter Property="TabNavigation" Value="Local" />
|
||||
<Setter Property="IsHoldingEnabled" Value="True" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="Margin" Value="0" />
|
||||
<Setter Property="Padding" Value="0" />
|
||||
<Setter Property="MinWidth" Value="32" />
|
||||
<Setter Property="MinHeight" Value="32" />
|
||||
<Setter Property="AllowDrop" Value="False" />
|
||||
<Setter Property="UseSystemFocusVisuals" Value="{StaticResource UseSystemFocusVisuals}" />
|
||||
<Setter Property="FocusVisualMargin" Value="-2" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="GridViewItem">
|
||||
<Grid
|
||||
x:Name="ContentBorder"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
Control.IsTemplateFocusTarget="True"
|
||||
CornerRadius="{TemplateBinding CornerRadius}"
|
||||
FocusVisualMargin="{TemplateBinding FocusVisualMargin}"
|
||||
RenderTransformOrigin="0.5,0.5">
|
||||
<Grid.RenderTransform>
|
||||
<ScaleTransform x:Name="ContentBorderScale" />
|
||||
</Grid.RenderTransform>
|
||||
<ContentPresenter
|
||||
x:Name="ContentPresenter"
|
||||
Margin="{TemplateBinding Padding}"
|
||||
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||
ContentTransitions="{TemplateBinding ContentTransitions}" />
|
||||
<!--
|
||||
The 'Xg' text simulates the amount of space one line of text will occupy.
|
||||
In the DataPlaceholder state, the Content is not loaded yet so we
|
||||
approximate the size of the item using placeholder text.
|
||||
-->
|
||||
<TextBlock
|
||||
x:Name="PlaceholderTextBlock"
|
||||
Margin="{TemplateBinding Padding}"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
Foreground="{x:Null}"
|
||||
IsHitTestVisible="False"
|
||||
Text="Xg"
|
||||
Visibility="Collapsed" />
|
||||
<Rectangle
|
||||
x:Name="PlaceholderRect"
|
||||
Fill="{ThemeResource ListViewItemPlaceholderBackground}"
|
||||
Visibility="Collapsed" />
|
||||
<Rectangle
|
||||
x:Name="BorderRectangle"
|
||||
IsHitTestVisible="False"
|
||||
Opacity="0"
|
||||
RadiusX="6"
|
||||
RadiusY="6"
|
||||
Stroke="{ThemeResource SystemControlHighlightListAccentLowBrush}"
|
||||
StrokeThickness="2" />
|
||||
<Border
|
||||
x:Name="MultiSelectSquare"
|
||||
Width="20"
|
||||
Height="20"
|
||||
Margin="0,2,2,0"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Top"
|
||||
Background="{ThemeResource SystemControlBackgroundChromeMediumBrush}"
|
||||
CornerRadius="6"
|
||||
Visibility="Collapsed">
|
||||
<FontIcon
|
||||
x:Name="MultiSelectCheck"
|
||||
FontFamily="{ThemeResource SymbolThemeFontFamily}"
|
||||
FontSize="16"
|
||||
Foreground="{ThemeResource SystemControlForegroundBaseMediumHighBrush}"
|
||||
Glyph=""
|
||||
Opacity="0" />
|
||||
</Border>
|
||||
<Border
|
||||
x:Name="MultiArrangeOverlayTextBorder"
|
||||
Height="20"
|
||||
MinWidth="20"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Background="{ThemeResource SystemControlBackgroundAccentBrush}"
|
||||
BorderBrush="{ThemeResource SystemControlBackgroundChromeWhiteBrush}"
|
||||
BorderThickness="2"
|
||||
CornerRadius="6"
|
||||
IsHitTestVisible="False"
|
||||
Opacity="0">
|
||||
<TextBlock
|
||||
x:Name="MultiArrangeOverlayText"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
IsHitTestVisible="False"
|
||||
Opacity="0"
|
||||
Style="{ThemeResource CaptionTextBlockStyle}"
|
||||
Text="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.DragItemsCount}" />
|
||||
</Border>
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup x:Name="FocusStates">
|
||||
<VisualState x:Name="Focused">
|
||||
|
||||
<Storyboard>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="BorderRectangle" Storyboard.TargetProperty="Visibility">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="Collapsed" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
</VisualState>
|
||||
<VisualState x:Name="Unfocused" />
|
||||
|
||||
</VisualStateGroup>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal">
|
||||
|
||||
<Storyboard>
|
||||
<PointerUpThemeAnimation Storyboard.TargetName="ContentPresenter" />
|
||||
</Storyboard>
|
||||
</VisualState>
|
||||
|
||||
<VisualState x:Name="PointerOver">
|
||||
|
||||
<Storyboard>
|
||||
<DoubleAnimation
|
||||
Storyboard.TargetName="BorderRectangle"
|
||||
Storyboard.TargetProperty="Opacity"
|
||||
To="1"
|
||||
Duration="0" />
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="BorderRectangle" Storyboard.TargetProperty="Stroke">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{Binding Converter={StaticResource ContrastBrushConverter}, ConverterParameter={ThemeResource TextControlForeground}}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{Binding Converter={StaticResource ContrastBrushConverter}, ConverterParameter={ThemeResource TextControlForeground}}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentBorder" Storyboard.TargetProperty="FocusVisualSecondaryBrush">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{Binding Converter={StaticResource ContrastBrushConverter}, ConverterParameter={ThemeResource TextControlForeground}}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentBorder" Storyboard.TargetProperty="FocusVisualSecondaryThickness">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="2" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<PointerUpThemeAnimation Storyboard.TargetName="ContentPresenter" />
|
||||
|
||||
<DoubleAnimation
|
||||
Storyboard.TargetName="MultiSelectCheck"
|
||||
Storyboard.TargetProperty="Opacity"
|
||||
To="1"
|
||||
Duration="0" />
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="MultiSelectSquare" Storyboard.TargetProperty="Background">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{Binding Converter={StaticResource ContrastBrushConverter}, ConverterParameter={ThemeResource TextControlForeground}}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
</VisualState>
|
||||
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
</Grid>
|
||||
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</UserControl.Resources>
|
||||
|
||||
<Grid HorizontalAlignment="Stretch">
|
||||
<GridView
|
||||
Margin="0"
|
||||
Padding="0"
|
||||
IsItemClickEnabled="True"
|
||||
ItemClick="ListViewBase_OnItemClick"
|
||||
ItemContainerStyle="{StaticResource PaletteGridViewItemStyle}"
|
||||
ItemsSource="{x:Bind PaletteColors}"
|
||||
SelectionMode="None">
|
||||
<GridView.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<controls:UniformGrid ui:FrameworkElementExtensions.AncestorType="local:ColorPalette" Columns="{Binding (ui:FrameworkElementExtensions.Ancestor).CustomPaletteColumnCount, RelativeSource={RelativeSource Self}}" />
|
||||
</ItemsPanelTemplate>
|
||||
</GridView.ItemsPanel>
|
||||
<GridView.ItemTemplate>
|
||||
<DataTemplate x:DataType="Color">
|
||||
<Border
|
||||
Margin="2"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
AutomationProperties.Name="{Binding Converter={StaticResource ColorToDisplayNameConverter}}"
|
||||
CornerRadius="4"
|
||||
ToolTipService.ToolTip="{Binding Converter={StaticResource ColorToDisplayNameConverter}}">
|
||||
<Border.Background>
|
||||
<SolidColorBrush Color="{Binding}" />
|
||||
</Border.Background>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</GridView.ItemTemplate>
|
||||
</GridView>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,71 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.ObjectModel;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Controls;
|
||||
|
||||
public sealed partial class ColorPalette : UserControl
|
||||
{
|
||||
public static readonly DependencyProperty PaletteColorsProperty = DependencyProperty.Register(nameof(PaletteColors), typeof(ObservableCollection<Color>), typeof(ColorPalette), null!)!;
|
||||
|
||||
public static readonly DependencyProperty CustomPaletteColumnCountProperty = DependencyProperty.Register(nameof(CustomPaletteColumnCount), typeof(int), typeof(ColorPalette), new PropertyMetadata(10))!;
|
||||
|
||||
public static readonly DependencyProperty SelectedColorProperty = DependencyProperty.Register(nameof(SelectedColor), typeof(Color), typeof(ColorPalette), new PropertyMetadata(null!))!;
|
||||
|
||||
public event EventHandler<Color?>? SelectedColorChanged;
|
||||
|
||||
private Color? _selectedColor;
|
||||
|
||||
public Color? SelectedColor
|
||||
{
|
||||
get => _selectedColor;
|
||||
|
||||
set
|
||||
{
|
||||
if (_selectedColor != value)
|
||||
{
|
||||
_selectedColor = value;
|
||||
if (value is not null)
|
||||
{
|
||||
SetValue(SelectedColorProperty, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
ClearValue(SelectedColorProperty);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ObservableCollection<Color> PaletteColors
|
||||
{
|
||||
get => (ObservableCollection<Color>)GetValue(PaletteColorsProperty)!;
|
||||
set => SetValue(PaletteColorsProperty, value);
|
||||
}
|
||||
|
||||
public int CustomPaletteColumnCount
|
||||
{
|
||||
get => (int)GetValue(CustomPaletteColumnCountProperty);
|
||||
set => SetValue(CustomPaletteColumnCountProperty, value);
|
||||
}
|
||||
|
||||
public ColorPalette()
|
||||
{
|
||||
PaletteColors = [];
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void ListViewBase_OnItemClick(object sender, ItemClickEventArgs e)
|
||||
{
|
||||
if (e.ClickedItem is Color color)
|
||||
{
|
||||
SelectedColor = color;
|
||||
SelectedColorChanged?.Invoke(this, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<UserControl
|
||||
x:Class="Microsoft.CmdPal.UI.Controls.ColorPickerButton"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:controls="using:Microsoft.CmdPal.UI.Controls"
|
||||
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
d:DesignHeight="300"
|
||||
d:DesignWidth="400"
|
||||
mc:Ignorable="d">
|
||||
<UserControl.Resources>
|
||||
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
|
||||
<converters:BoolToVisibilityConverter
|
||||
x:Key="BoolToVisibilityInvertedConverter"
|
||||
FalseValue="Visible"
|
||||
TrueValue="Collapsed" />
|
||||
</UserControl.Resources>
|
||||
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<DropDownButton Padding="{x:Bind ToDropDownPadding(HasSelectedColor), Mode=OneWay}">
|
||||
<Grid>
|
||||
<TextBlock x:Uid="OptionalColorPickerButton_UnsetTextBlock" Visibility="{x:Bind HasSelectedColor, Mode=OneWay, Converter={StaticResource BoolToVisibilityInvertedConverter}}" />
|
||||
<Border
|
||||
x:Name="ColorPreviewBorder"
|
||||
Width="48"
|
||||
Height="24"
|
||||
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{ThemeResource ControlCornerRadius}"
|
||||
Visibility="{x:Bind HasSelectedColor, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<Border.Background>
|
||||
<SolidColorBrush Color="{x:Bind SelectedColor, Mode=OneWay}" />
|
||||
</Border.Background>
|
||||
</Border>
|
||||
</Grid>
|
||||
<DropDownButton.Flyout>
|
||||
<Flyout
|
||||
x:Name="ColorPickerFlyout"
|
||||
Opened="FlyoutBase_OnOpened"
|
||||
Placement="Bottom"
|
||||
ShouldConstrainToRootBounds="False">
|
||||
<Flyout.FlyoutPresenterStyle>
|
||||
<Style BasedOn="{StaticResource DefaultFlyoutPresenterStyle}" TargetType="FlyoutPresenter">
|
||||
<Setter Property="MinWidth" Value="660" />
|
||||
</Style>
|
||||
</Flyout.FlyoutPresenterStyle>
|
||||
<StackPanel
|
||||
x:Name="FlyoutRoot"
|
||||
Orientation="Horizontal"
|
||||
SizeChanged="FlyoutRoot_OnSizeChanged"
|
||||
Spacing="20">
|
||||
<!-- Left column: Preset colors and reset button -->
|
||||
<StackPanel Margin="2">
|
||||
<TextBlock
|
||||
x:Uid="OptionalColorPickerButton_WindowsColorsSectionHeading"
|
||||
Margin="0,0,0,12"
|
||||
Style="{StaticResource BodyTextBlockStyle}" />
|
||||
|
||||
<controls:ColorPalette
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Top"
|
||||
CustomPaletteColumnCount="9"
|
||||
PaletteColors="{x:Bind PaletteColors}"
|
||||
SelectedColorChanged="ColorPalette_OnSelectedColorChanged" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Right column: Spectrum -->
|
||||
<StackPanel>
|
||||
<TextBlock
|
||||
x:Uid="OptionalColorPickerButton_CustomColorsSectionHeading"
|
||||
Margin="0,0,0,12"
|
||||
Style="{StaticResource BodyTextBlockStyle}" />
|
||||
|
||||
<ColorPicker
|
||||
IsAlphaEnabled="{x:Bind IsAlphaEnabled, Mode=OneWay}"
|
||||
IsAlphaSliderVisible="{x:Bind IsAlphaEnabled, Mode=OneWay}"
|
||||
IsAlphaTextInputVisible="{x:Bind IsAlphaEnabled, Mode=OneWay}"
|
||||
IsColorChannelTextInputVisible="True"
|
||||
IsColorSliderVisible="True"
|
||||
IsHexInputVisible="True"
|
||||
IsMoreButtonVisible="True"
|
||||
Color="{x:Bind SelectedColor, Mode=TwoWay}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Flyout>
|
||||
</DropDownButton.Flyout>
|
||||
</DropDownButton>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,146 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.ObjectModel;
|
||||
using ManagedCommon;
|
||||
using Microsoft.UI;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Controls;
|
||||
|
||||
public sealed partial class ColorPickerButton : UserControl
|
||||
{
|
||||
public static readonly DependencyProperty PaletteColorsProperty = DependencyProperty.Register(nameof(PaletteColors), typeof(ObservableCollection<Color>), typeof(ColorPickerButton), new PropertyMetadata(new ObservableCollection<Color>()))!;
|
||||
|
||||
public static readonly DependencyProperty SelectedColorProperty = DependencyProperty.Register(nameof(SelectedColor), typeof(Color), typeof(ColorPickerButton), new PropertyMetadata(Colors.Black))!;
|
||||
|
||||
public static readonly DependencyProperty IsAlphaEnabledProperty = DependencyProperty.Register(nameof(IsAlphaEnabled), typeof(bool), typeof(ColorPickerButton), new PropertyMetadata(defaultValue: false))!;
|
||||
|
||||
public static readonly DependencyProperty IsValueEditorEnabledProperty = DependencyProperty.Register(nameof(IsValueEditorEnabled), typeof(bool), typeof(ColorPickerButton), new PropertyMetadata(false))!;
|
||||
|
||||
public static readonly DependencyProperty HasSelectedColorProperty = DependencyProperty.Register(nameof(HasSelectedColor), typeof(bool), typeof(ColorPickerButton), new PropertyMetadata(false))!;
|
||||
|
||||
private Color _selectedColor;
|
||||
|
||||
public Color SelectedColor
|
||||
{
|
||||
get
|
||||
{
|
||||
return _selectedColor;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
if (_selectedColor != value)
|
||||
{
|
||||
_selectedColor = value;
|
||||
SetValue(SelectedColorProperty, value);
|
||||
HasSelectedColor = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasSelectedColor
|
||||
{
|
||||
get { return (bool)GetValue(HasSelectedColorProperty); }
|
||||
set { SetValue(HasSelectedColorProperty, value); }
|
||||
}
|
||||
|
||||
public bool IsAlphaEnabled
|
||||
{
|
||||
get => (bool)GetValue(IsAlphaEnabledProperty);
|
||||
set => SetValue(IsAlphaEnabledProperty, value);
|
||||
}
|
||||
|
||||
public bool IsValueEditorEnabled
|
||||
{
|
||||
get { return (bool)GetValue(IsValueEditorEnabledProperty); }
|
||||
set { SetValue(IsValueEditorEnabledProperty, value); }
|
||||
}
|
||||
|
||||
public ObservableCollection<Color> PaletteColors
|
||||
{
|
||||
get { return (ObservableCollection<Color>)GetValue(PaletteColorsProperty); }
|
||||
set { SetValue(PaletteColorsProperty, value); }
|
||||
}
|
||||
|
||||
public ColorPickerButton()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
|
||||
IsEnabledChanged -= ColorPickerButton_IsEnabledChanged;
|
||||
SetEnabledState();
|
||||
IsEnabledChanged += ColorPickerButton_IsEnabledChanged;
|
||||
}
|
||||
|
||||
private void ColorPickerButton_IsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
SetEnabledState();
|
||||
}
|
||||
|
||||
private void SetEnabledState()
|
||||
{
|
||||
if (this.IsEnabled)
|
||||
{
|
||||
ColorPreviewBorder.Opacity = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
ColorPreviewBorder.Opacity = 0.2;
|
||||
}
|
||||
}
|
||||
|
||||
private void ColorPalette_OnSelectedColorChanged(object? sender, Color? e)
|
||||
{
|
||||
if (e.HasValue)
|
||||
{
|
||||
HasSelectedColor = true;
|
||||
SelectedColor = e.Value;
|
||||
}
|
||||
}
|
||||
|
||||
private void FlyoutBase_OnOpened(object? sender, object e)
|
||||
{
|
||||
if (sender is not Flyout flyout || (flyout.Content as FrameworkElement)?.Parent is not FlyoutPresenter flyoutPresenter)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
FlyoutRoot!.UpdateLayout();
|
||||
flyoutPresenter.UpdateLayout();
|
||||
|
||||
// Logger.LogInfo($"FlyoutBase_OnOpened: {flyoutPresenter}, {FlyoutRoot!.ActualWidth}");
|
||||
flyoutPresenter.MaxWidth = FlyoutRoot!.ActualWidth;
|
||||
flyoutPresenter.MinWidth = 660;
|
||||
flyoutPresenter.Width = FlyoutRoot!.ActualWidth;
|
||||
}
|
||||
|
||||
private void FlyoutRoot_OnSizeChanged(object sender, SizeChangedEventArgs e)
|
||||
{
|
||||
if ((ColorPickerFlyout!.Content as FrameworkElement)?.Parent is not FlyoutPresenter flyoutPresenter)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
FlyoutRoot!.UpdateLayout();
|
||||
flyoutPresenter.UpdateLayout();
|
||||
|
||||
flyoutPresenter.MaxWidth = FlyoutRoot!.ActualWidth;
|
||||
flyoutPresenter.MinWidth = 660;
|
||||
flyoutPresenter.Width = FlyoutRoot!.ActualWidth;
|
||||
}
|
||||
|
||||
private Thickness ToDropDownPadding(bool hasColor)
|
||||
{
|
||||
return hasColor ? new Thickness(3, 3, 8, 3) : new Thickness(8, 4, 8, 4);
|
||||
}
|
||||
|
||||
private void ResetButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
HasSelectedColor = false;
|
||||
ColorPickerFlyout?.Hide();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<UserControl
|
||||
x:Class="Microsoft.CmdPal.UI.Controls.CommandPalettePreview"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:h="using:Microsoft.CmdPal.UI.Helpers"
|
||||
xmlns:local="using:Microsoft.CmdPal.UI.Controls"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d">
|
||||
<Border
|
||||
Width="200"
|
||||
Height="120"
|
||||
BorderBrush="{ThemeResource SurfaceStrokeColorDefaultBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="8"
|
||||
Translation="0,0,8">
|
||||
<Grid>
|
||||
<Border
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
BorderBrush="{ThemeResource SurfaceStrokeColorDefaultBrush}"
|
||||
BorderThickness="1"
|
||||
Visibility="{x:Bind h:BindTransformers.NegateVisibility(ShowBackgroundImage), Mode=OneWay}">
|
||||
<Border.Background>
|
||||
<AcrylicBrush
|
||||
FallbackColor="{x:Bind PreviewBackgroundColor, Mode=OneWay}"
|
||||
TintColor="{x:Bind PreviewBackgroundColor, Mode=OneWay}"
|
||||
TintOpacity="{x:Bind PreviewBackgroundOpacity, Mode=OneWay}" />
|
||||
</Border.Background>
|
||||
</Border>
|
||||
<local:BlurImageControl
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
BlurAmount="{x:Bind PreviewBackgroundImageBlurAmount, Mode=OneWay}"
|
||||
ImageBrightness="{x:Bind PreviewBackgroundImageBrightness, Mode=OneWay}"
|
||||
ImageSource="{x:Bind PreviewBackgroundImageSource, Mode=OneWay}"
|
||||
ImageStretch="{x:Bind ToStretch(PreviewBackgroundImageFit), Mode=OneWay}"
|
||||
IsHitTestVisible="False"
|
||||
TintColor="{x:Bind PreviewBackgroundImageTint, Mode=OneWay}"
|
||||
TintIntensity="{x:Bind ToTintIntensity(PreviewBackgroundImageTintIntensity), Mode=OneWay}"
|
||||
Visibility="{x:Bind ShowBackgroundImage, Mode=OneWay}" />
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="100" />
|
||||
<RowDefinition Height="20" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Border
|
||||
x:Name="ContentPreview"
|
||||
Grid.Row="0"
|
||||
Background="{ThemeResource LayerOnAcrylicPrimaryBackgroundBrush}">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="20" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Border BorderBrush="{ThemeResource CmdPal.TopBarBorderBrush}" BorderThickness="0,0,0,1" />
|
||||
</Grid>
|
||||
|
||||
</Border>
|
||||
|
||||
<Border
|
||||
x:Name="CommandBarPreview"
|
||||
Grid.Row="1"
|
||||
Background="{ThemeResource LayerOnAcrylicSecondaryBackgroundBrush}"
|
||||
BorderBrush="{ThemeResource CmdPal.CommandBarBorderBrush}"
|
||||
BorderThickness="0,1,0,0" />
|
||||
</Grid>
|
||||
|
||||
</Grid>
|
||||
</Border>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,123 @@
|
||||
// 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.UI.ViewModels;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Controls;
|
||||
|
||||
public sealed partial class CommandPalettePreview : UserControl
|
||||
{
|
||||
public static readonly DependencyProperty PreviewBackgroundOpacityProperty = DependencyProperty.Register(nameof(PreviewBackgroundOpacity), typeof(double), typeof(CommandPalettePreview), new PropertyMetadata(0d));
|
||||
|
||||
public static readonly DependencyProperty PreviewBackgroundColorProperty = DependencyProperty.Register(nameof(PreviewBackgroundColor), typeof(Color), typeof(CommandPalettePreview), new PropertyMetadata(default(Color)));
|
||||
|
||||
public static readonly DependencyProperty PreviewBackgroundImageSourceProperty = DependencyProperty.Register(nameof(PreviewBackgroundImageSource), typeof(ImageSource), typeof(CommandPalettePreview), new PropertyMetadata(null, PropertyChangedCallback));
|
||||
|
||||
public static readonly DependencyProperty PreviewBackgroundImageOpacityProperty = DependencyProperty.Register(nameof(PreviewBackgroundImageOpacity), typeof(int), typeof(CommandPalettePreview), new PropertyMetadata(0));
|
||||
|
||||
public static readonly DependencyProperty PreviewBackgroundImageFitProperty = DependencyProperty.Register(nameof(PreviewBackgroundImageFit), typeof(BackgroundImageFit), typeof(CommandPalettePreview), new PropertyMetadata(default(BackgroundImageFit)));
|
||||
|
||||
public static readonly DependencyProperty PreviewBackgroundImageBrightnessProperty = DependencyProperty.Register(nameof(PreviewBackgroundImageBrightness), typeof(double), typeof(CommandPalettePreview), new PropertyMetadata(0d));
|
||||
|
||||
public static readonly DependencyProperty PreviewBackgroundImageBlurAmountProperty = DependencyProperty.Register(nameof(PreviewBackgroundImageBlurAmount), typeof(double), typeof(CommandPalettePreview), new PropertyMetadata(0d));
|
||||
|
||||
public static readonly DependencyProperty PreviewBackgroundImageTintProperty = DependencyProperty.Register(nameof(PreviewBackgroundImageTint), typeof(Color), typeof(CommandPalettePreview), new PropertyMetadata(default(Color)));
|
||||
|
||||
public static readonly DependencyProperty PreviewBackgroundImageTintIntensityProperty = DependencyProperty.Register(nameof(PreviewBackgroundImageTintIntensity), typeof(int), typeof(CommandPalettePreview), new PropertyMetadata(0));
|
||||
|
||||
public static readonly DependencyProperty ShowBackgroundImageProperty = DependencyProperty.Register(nameof(ShowBackgroundImage), typeof(Visibility), typeof(CommandPalettePreview), new PropertyMetadata(Visibility.Collapsed));
|
||||
|
||||
public BackgroundImageFit PreviewBackgroundImageFit
|
||||
{
|
||||
get { return (BackgroundImageFit)GetValue(PreviewBackgroundImageFitProperty); }
|
||||
set { SetValue(PreviewBackgroundImageFitProperty, value); }
|
||||
}
|
||||
|
||||
public double PreviewBackgroundOpacity
|
||||
{
|
||||
get { return (double)GetValue(PreviewBackgroundOpacityProperty); }
|
||||
set { SetValue(PreviewBackgroundOpacityProperty, value); }
|
||||
}
|
||||
|
||||
public Color PreviewBackgroundColor
|
||||
{
|
||||
get { return (Color)GetValue(PreviewBackgroundColorProperty); }
|
||||
set { SetValue(PreviewBackgroundColorProperty, value); }
|
||||
}
|
||||
|
||||
public ImageSource PreviewBackgroundImageSource
|
||||
{
|
||||
get { return (ImageSource)GetValue(PreviewBackgroundImageSourceProperty); }
|
||||
set { SetValue(PreviewBackgroundImageSourceProperty, value); }
|
||||
}
|
||||
|
||||
public int PreviewBackgroundImageOpacity
|
||||
{
|
||||
get { return (int)GetValue(PreviewBackgroundImageOpacityProperty); }
|
||||
set { SetValue(PreviewBackgroundImageOpacityProperty, value); }
|
||||
}
|
||||
|
||||
public double PreviewBackgroundImageBrightness
|
||||
{
|
||||
get => (double)GetValue(PreviewBackgroundImageBrightnessProperty);
|
||||
set => SetValue(PreviewBackgroundImageBrightnessProperty, value);
|
||||
}
|
||||
|
||||
public double PreviewBackgroundImageBlurAmount
|
||||
{
|
||||
get => (double)GetValue(PreviewBackgroundImageBlurAmountProperty);
|
||||
set => SetValue(PreviewBackgroundImageBlurAmountProperty, value);
|
||||
}
|
||||
|
||||
public Color PreviewBackgroundImageTint
|
||||
{
|
||||
get => (Color)GetValue(PreviewBackgroundImageTintProperty);
|
||||
set => SetValue(PreviewBackgroundImageTintProperty, value);
|
||||
}
|
||||
|
||||
public int PreviewBackgroundImageTintIntensity
|
||||
{
|
||||
get => (int)GetValue(PreviewBackgroundImageTintIntensityProperty);
|
||||
set => SetValue(PreviewBackgroundImageTintIntensityProperty, value);
|
||||
}
|
||||
|
||||
public Visibility ShowBackgroundImage
|
||||
{
|
||||
get => (Visibility)GetValue(ShowBackgroundImageProperty);
|
||||
set => SetValue(ShowBackgroundImageProperty, value);
|
||||
}
|
||||
|
||||
public CommandPalettePreview()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private static void PropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is not CommandPalettePreview preview)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
preview.ShowBackgroundImage = e.NewValue is ImageSource ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
private double ToOpacity(int value) => value / 100.0;
|
||||
|
||||
private double ToTintIntensity(int value) => value / 100.0;
|
||||
|
||||
private Stretch ToStretch(BackgroundImageFit fit)
|
||||
{
|
||||
return fit switch
|
||||
{
|
||||
BackgroundImageFit.Fill => Stretch.Fill,
|
||||
BackgroundImageFit.UniformToFill => Stretch.UniformToFill,
|
||||
_ => Stretch.None,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<UserControl
|
||||
x:Class="Microsoft.CmdPal.UI.Controls.ScreenPreview"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Border
|
||||
x:Name="ScreenBorder"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
BorderBrush="Black"
|
||||
BorderThickness="8"
|
||||
CornerRadius="8"
|
||||
UseLayoutRounding="True">
|
||||
<Viewbox Height="120" UseLayoutRounding="True">
|
||||
<Grid>
|
||||
<Image
|
||||
x:Name="WallpaperImage"
|
||||
MaxHeight="200"
|
||||
Stretch="Uniform"
|
||||
UseLayoutRounding="True" />
|
||||
<ContentPresenter
|
||||
x:Name="ContentPresenter"
|
||||
Margin="40"
|
||||
Content="{x:Bind PreviewContent}"
|
||||
UseLayoutRounding="True" />
|
||||
</Grid>
|
||||
</Viewbox>
|
||||
</Border>
|
||||
|
||||
</UserControl>
|
||||
@@ -0,0 +1,33 @@
|
||||
// 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.UI.Helpers;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Markup;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Controls;
|
||||
|
||||
[ContentProperty(Name = nameof(PreviewContent))]
|
||||
public sealed partial class ScreenPreview : UserControl
|
||||
{
|
||||
public static readonly DependencyProperty PreviewContentProperty =
|
||||
DependencyProperty.Register(nameof(PreviewContent), typeof(object), typeof(ScreenPreview), new PropertyMetadata(null!))!;
|
||||
|
||||
public object PreviewContent
|
||||
{
|
||||
get => GetValue(PreviewContentProperty)!;
|
||||
set => SetValue(PreviewContentProperty, value);
|
||||
}
|
||||
|
||||
public ScreenPreview()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
var wallpaperHelper = new WallpaperHelper();
|
||||
WallpaperImage!.Source = wallpaperHelper.GetWallpaperImage()!;
|
||||
ScreenBorder!.Background = new SolidColorBrush(wallpaperHelper.GetWallpaperColor());
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,8 @@
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:cmdpalUi="using:Microsoft.CmdPal.UI"
|
||||
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
|
||||
xmlns:cpcontrols="using:Microsoft.CmdPal.UI.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:h="using:Microsoft.CmdPal.UI.Helpers"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d">
|
||||
|
||||
@@ -22,6 +21,7 @@
|
||||
MinHeight="32"
|
||||
VerticalAlignment="Stretch"
|
||||
VerticalContentAlignment="Stretch"
|
||||
h:TextBoxCaretColor.SyncWithForeground="True"
|
||||
AutomationProperties.AutomationId="MainSearchBox"
|
||||
KeyDown="FilterBox_KeyDown"
|
||||
PlaceholderText="{x:Bind CurrentPageViewModel.PlaceholderText, Converter={StaticResource PlaceholderTextConverter}, Mode=OneWay}"
|
||||
|
||||
@@ -379,6 +379,12 @@ public sealed partial class SearchBar : UserControl,
|
||||
if (CurrentPageViewModel is not null)
|
||||
{
|
||||
CurrentPageViewModel.SearchTextBox = FilterBox.Text;
|
||||
|
||||
// Telemetry: Track search query count for session metrics (only non-empty queries)
|
||||
if (!string.IsNullOrWhiteSpace(FilterBox.Text))
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<SearchQueryMessage>(new());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Controls;
|
||||
|
||||
internal sealed class UVBounds
|
||||
{
|
||||
public double UMin { get; }
|
||||
|
||||
public double UMax { get; }
|
||||
|
||||
public double VMin { get; }
|
||||
|
||||
public double VMax { get; }
|
||||
|
||||
public UVBounds(Orientation orientation, Rect rect)
|
||||
{
|
||||
if (orientation == Orientation.Horizontal)
|
||||
{
|
||||
UMin = rect.Left;
|
||||
UMax = rect.Right;
|
||||
VMin = rect.Top;
|
||||
VMax = rect.Bottom;
|
||||
}
|
||||
else
|
||||
{
|
||||
UMin = rect.Top;
|
||||
UMax = rect.Bottom;
|
||||
VMin = rect.Left;
|
||||
VMax = rect.Right;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Diagnostics;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Controls;
|
||||
|
||||
[DebuggerDisplay("U = {U} V = {V}")]
|
||||
internal struct UvMeasure
|
||||
{
|
||||
internal double U { get; set; }
|
||||
|
||||
internal double V { get; set; }
|
||||
|
||||
internal static UvMeasure Zero => default(UvMeasure);
|
||||
|
||||
public UvMeasure(Orientation orientation, Size size)
|
||||
: this(orientation, size.Width, size.Height)
|
||||
{
|
||||
}
|
||||
|
||||
public UvMeasure(Orientation orientation, double width, double height)
|
||||
{
|
||||
if (orientation == Orientation.Horizontal)
|
||||
{
|
||||
U = width;
|
||||
V = height;
|
||||
}
|
||||
else
|
||||
{
|
||||
U = height;
|
||||
V = width;
|
||||
}
|
||||
}
|
||||
|
||||
public UvMeasure Add(double u, double v)
|
||||
{
|
||||
UvMeasure result = default(UvMeasure);
|
||||
result.U = U + u;
|
||||
result.V = V + v;
|
||||
return result;
|
||||
}
|
||||
|
||||
public UvMeasure Add(UvMeasure measure)
|
||||
{
|
||||
return Add(measure.U, measure.V);
|
||||
}
|
||||
|
||||
public Size ToSize(Orientation orientation)
|
||||
{
|
||||
if (orientation != Orientation.Horizontal)
|
||||
{
|
||||
return new Size(V, U);
|
||||
}
|
||||
|
||||
return new Size(U, V);
|
||||
}
|
||||
|
||||
public Point GetPoint(Orientation orientation)
|
||||
{
|
||||
return orientation is Orientation.Horizontal ? new Point(U, V) : new Point(V, U);
|
||||
}
|
||||
|
||||
public Size GetSize(Orientation orientation)
|
||||
{
|
||||
return orientation is Orientation.Horizontal ? new Size(U, V) : new Size(V, U);
|
||||
}
|
||||
|
||||
public static bool operator ==(UvMeasure measure1, UvMeasure measure2)
|
||||
{
|
||||
return measure1.U == measure2.U && measure1.V == measure2.V;
|
||||
}
|
||||
|
||||
public static bool operator !=(UvMeasure measure1, UvMeasure measure2)
|
||||
{
|
||||
return !(measure1 == measure2);
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is UvMeasure measure && this == measure;
|
||||
}
|
||||
|
||||
public bool Equals(UvMeasure value)
|
||||
{
|
||||
return this == value;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return base.GetHashCode();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,418 @@
|
||||
// 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.WinUI.Controls;
|
||||
using Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Windows.Foundation;
|
||||
|
||||
using ToolkitStretchChild = CommunityToolkit.WinUI.Controls.StretchChild;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// Arranges elements by wrapping them to fit the available space.
|
||||
/// When <see cref="Orientation"/> is set to Orientation.Horizontal, element are arranged in rows until the available width is reached and then to a new row.
|
||||
/// When <see cref="Orientation"/> is set to Orientation.Vertical, element are arranged in columns until the available height is reached.
|
||||
/// </summary>
|
||||
public sealed partial class WrapPanel : Panel
|
||||
{
|
||||
private struct UvRect
|
||||
{
|
||||
public UvMeasure Position { get; set; }
|
||||
|
||||
public UvMeasure Size { get; set; }
|
||||
|
||||
public Rect ToRect(Orientation orientation)
|
||||
{
|
||||
return orientation switch
|
||||
{
|
||||
Orientation.Vertical => new Rect(Position.V, Position.U, Size.V, Size.U),
|
||||
Orientation.Horizontal => new Rect(Position.U, Position.V, Size.U, Size.V),
|
||||
_ => ThrowArgumentException(),
|
||||
};
|
||||
}
|
||||
|
||||
private static Rect ThrowArgumentException()
|
||||
{
|
||||
throw new ArgumentException("The input orientation is not valid.");
|
||||
}
|
||||
}
|
||||
|
||||
private struct Row
|
||||
{
|
||||
public List<UvRect> ChildrenRects { get; }
|
||||
|
||||
public UvMeasure Size { get; set; }
|
||||
|
||||
public UvRect Rect
|
||||
{
|
||||
get
|
||||
{
|
||||
UvRect result;
|
||||
if (ChildrenRects.Count <= 0)
|
||||
{
|
||||
result = default(UvRect);
|
||||
result.Position = UvMeasure.Zero;
|
||||
result.Size = Size;
|
||||
return result;
|
||||
}
|
||||
|
||||
result = default(UvRect);
|
||||
result.Position = ChildrenRects.First().Position;
|
||||
result.Size = Size;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public Row(List<UvRect> childrenRects, UvMeasure size)
|
||||
{
|
||||
ChildrenRects = childrenRects;
|
||||
Size = size;
|
||||
}
|
||||
|
||||
public void Add(UvMeasure position, UvMeasure size)
|
||||
{
|
||||
ChildrenRects.Add(new UvRect
|
||||
{
|
||||
Position = position,
|
||||
Size = size,
|
||||
});
|
||||
|
||||
Size = new UvMeasure
|
||||
{
|
||||
U = position.U + size.U,
|
||||
V = Math.Max(Size.V, size.V),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a uniform Horizontal distance (in pixels) between items when <see cref="Orientation"/> is set to Horizontal,
|
||||
/// or between columns of items when <see cref="Orientation"/> is set to Vertical.
|
||||
/// </summary>
|
||||
public double HorizontalSpacing
|
||||
{
|
||||
get { return (double)GetValue(HorizontalSpacingProperty); }
|
||||
set { SetValue(HorizontalSpacingProperty, value); }
|
||||
}
|
||||
|
||||
private bool IsSectionItem(UIElement element) => element is FrameworkElement fe && fe.DataContext is ListItemViewModel item && item.IsSectionOrSeparator;
|
||||
|
||||
/// <summary>
|
||||
/// Identifies the <see cref="HorizontalSpacing"/> dependency property.
|
||||
/// </summary>
|
||||
public static readonly DependencyProperty HorizontalSpacingProperty =
|
||||
DependencyProperty.Register(
|
||||
nameof(HorizontalSpacing),
|
||||
typeof(double),
|
||||
typeof(WrapPanel),
|
||||
new PropertyMetadata(0d, LayoutPropertyChanged));
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a uniform Vertical distance (in pixels) between items when <see cref="Orientation"/> is set to Vertical,
|
||||
/// or between rows of items when <see cref="Orientation"/> is set to Horizontal.
|
||||
/// </summary>
|
||||
public double VerticalSpacing
|
||||
{
|
||||
get { return (double)GetValue(VerticalSpacingProperty); }
|
||||
set { SetValue(VerticalSpacingProperty, value); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Identifies the <see cref="VerticalSpacing"/> dependency property.
|
||||
/// </summary>
|
||||
public static readonly DependencyProperty VerticalSpacingProperty =
|
||||
DependencyProperty.Register(
|
||||
nameof(VerticalSpacing),
|
||||
typeof(double),
|
||||
typeof(WrapPanel),
|
||||
new PropertyMetadata(0d, LayoutPropertyChanged));
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the orientation of the WrapPanel.
|
||||
/// Horizontal means that child controls will be added horizontally until the width of the panel is reached, then a new row is added to add new child controls.
|
||||
/// Vertical means that children will be added vertically until the height of the panel is reached, then a new column is added.
|
||||
/// </summary>
|
||||
public Orientation Orientation
|
||||
{
|
||||
get { return (Orientation)GetValue(OrientationProperty); }
|
||||
set { SetValue(OrientationProperty, value); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Identifies the <see cref="Orientation"/> dependency property.
|
||||
/// </summary>
|
||||
public static readonly DependencyProperty OrientationProperty =
|
||||
DependencyProperty.Register(
|
||||
nameof(Orientation),
|
||||
typeof(Orientation),
|
||||
typeof(WrapPanel),
|
||||
new PropertyMetadata(Orientation.Horizontal, LayoutPropertyChanged));
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the distance between the border and its child object.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// The dimensions of the space between the border and its child as a Thickness value.
|
||||
/// Thickness is a structure that stores dimension values using pixel measures.
|
||||
/// </returns>
|
||||
public Thickness Padding
|
||||
{
|
||||
get { return (Thickness)GetValue(PaddingProperty); }
|
||||
set { SetValue(PaddingProperty, value); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Identifies the Padding dependency property.
|
||||
/// </summary>
|
||||
/// <returns>The identifier for the <see cref="Padding"/> dependency property.</returns>
|
||||
public static readonly DependencyProperty PaddingProperty =
|
||||
DependencyProperty.Register(
|
||||
nameof(Padding),
|
||||
typeof(Thickness),
|
||||
typeof(WrapPanel),
|
||||
new PropertyMetadata(default(Thickness), LayoutPropertyChanged));
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating how to arrange child items
|
||||
/// </summary>
|
||||
public ToolkitStretchChild StretchChild
|
||||
{
|
||||
get { return (ToolkitStretchChild)GetValue(StretchChildProperty); }
|
||||
set { SetValue(StretchChildProperty, value); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Identifies the <see cref="StretchChild"/> dependency property.
|
||||
/// </summary>
|
||||
/// <returns>The identifier for the <see cref="StretchChild"/> dependency property.</returns>
|
||||
public static readonly DependencyProperty StretchChildProperty =
|
||||
DependencyProperty.Register(
|
||||
nameof(StretchChild),
|
||||
typeof(ToolkitStretchChild),
|
||||
typeof(WrapPanel),
|
||||
new PropertyMetadata(ToolkitStretchChild.None, LayoutPropertyChanged));
|
||||
|
||||
/// <summary>
|
||||
/// Identifies the IsFullLine attached dependency property.
|
||||
/// If true, the child element will occupy the entire width of the panel and force a line break before and after itself.
|
||||
/// </summary>
|
||||
public static readonly DependencyProperty IsFullLineProperty =
|
||||
DependencyProperty.RegisterAttached(
|
||||
"IsFullLine",
|
||||
typeof(bool),
|
||||
typeof(WrapPanel),
|
||||
new PropertyMetadata(false, OnIsFullLineChanged));
|
||||
|
||||
public static bool GetIsFullLine(DependencyObject obj)
|
||||
{
|
||||
return (bool)obj.GetValue(IsFullLineProperty);
|
||||
}
|
||||
|
||||
public static void SetIsFullLine(DependencyObject obj, bool value)
|
||||
{
|
||||
obj.SetValue(IsFullLineProperty, value);
|
||||
}
|
||||
|
||||
private static void OnIsFullLineChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (FindVisualParentWrapPanel(d) is WrapPanel wp)
|
||||
{
|
||||
wp.InvalidateMeasure();
|
||||
}
|
||||
}
|
||||
|
||||
private static WrapPanel? FindVisualParentWrapPanel(DependencyObject child)
|
||||
{
|
||||
var parent = Microsoft.UI.Xaml.Media.VisualTreeHelper.GetParent(child);
|
||||
|
||||
while (parent != null)
|
||||
{
|
||||
if (parent is WrapPanel wrapPanel)
|
||||
{
|
||||
return wrapPanel;
|
||||
}
|
||||
|
||||
parent = Microsoft.UI.Xaml.Media.VisualTreeHelper.GetParent(parent);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void LayoutPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is WrapPanel wp)
|
||||
{
|
||||
wp.InvalidateMeasure();
|
||||
wp.InvalidateArrange();
|
||||
}
|
||||
}
|
||||
|
||||
private readonly List<Row> _rows = new List<Row>();
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Size MeasureOverride(Size availableSize)
|
||||
{
|
||||
var childAvailableSize = new Size(
|
||||
availableSize.Width - Padding.Left - Padding.Right,
|
||||
availableSize.Height - Padding.Top - Padding.Bottom);
|
||||
foreach (var child in Children)
|
||||
{
|
||||
child.Measure(childAvailableSize);
|
||||
}
|
||||
|
||||
var requiredSize = UpdateRows(availableSize);
|
||||
return requiredSize;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Size ArrangeOverride(Size finalSize)
|
||||
{
|
||||
if ((Orientation == Orientation.Horizontal && finalSize.Width < DesiredSize.Width) ||
|
||||
(Orientation == Orientation.Vertical && finalSize.Height < DesiredSize.Height))
|
||||
{
|
||||
// We haven't received our desired size. We need to refresh the rows.
|
||||
UpdateRows(finalSize);
|
||||
}
|
||||
|
||||
if (_rows.Count > 0)
|
||||
{
|
||||
// Now that we have all the data, we do the actual arrange pass
|
||||
var childIndex = 0;
|
||||
foreach (var row in _rows)
|
||||
{
|
||||
foreach (var rect in row.ChildrenRects)
|
||||
{
|
||||
var child = Children[childIndex++];
|
||||
while (child.Visibility == Visibility.Collapsed)
|
||||
{
|
||||
// Collapsed children are not added into the rows,
|
||||
// we skip them.
|
||||
child = Children[childIndex++];
|
||||
}
|
||||
|
||||
var arrangeRect = new UvRect
|
||||
{
|
||||
Position = rect.Position,
|
||||
Size = new UvMeasure { U = rect.Size.U, V = row.Size.V },
|
||||
};
|
||||
|
||||
var finalRect = arrangeRect.ToRect(Orientation);
|
||||
child.Arrange(finalRect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return finalSize;
|
||||
}
|
||||
|
||||
private Size UpdateRows(Size availableSize)
|
||||
{
|
||||
_rows.Clear();
|
||||
|
||||
var paddingStart = new UvMeasure(Orientation, Padding.Left, Padding.Top);
|
||||
var paddingEnd = new UvMeasure(Orientation, Padding.Right, Padding.Bottom);
|
||||
|
||||
if (Children.Count == 0)
|
||||
{
|
||||
return paddingStart.Add(paddingEnd).ToSize(Orientation);
|
||||
}
|
||||
|
||||
var parentMeasure = new UvMeasure(Orientation, availableSize.Width, availableSize.Height);
|
||||
var spacingMeasure = new UvMeasure(Orientation, HorizontalSpacing, VerticalSpacing);
|
||||
var position = new UvMeasure(Orientation, Padding.Left, Padding.Top);
|
||||
|
||||
var currentRow = new Row(new List<UvRect>(), default);
|
||||
var finalMeasure = new UvMeasure(Orientation, width: 0.0, height: 0.0);
|
||||
|
||||
void CommitRow()
|
||||
{
|
||||
// Only adds if the row has a content
|
||||
if (currentRow.ChildrenRects.Count > 0)
|
||||
{
|
||||
_rows.Add(currentRow);
|
||||
|
||||
position.V += currentRow.Size.V + spacingMeasure.V;
|
||||
}
|
||||
|
||||
position.U = paddingStart.U;
|
||||
|
||||
currentRow = new Row(new List<UvRect>(), default);
|
||||
}
|
||||
|
||||
void Arrange(UIElement child, bool isLast = false)
|
||||
{
|
||||
if (child.Visibility == Visibility.Collapsed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var isFullLine = IsSectionItem(child);
|
||||
var desiredMeasure = new UvMeasure(Orientation, child.DesiredSize);
|
||||
|
||||
if (isFullLine)
|
||||
{
|
||||
if (currentRow.ChildrenRects.Count > 0)
|
||||
{
|
||||
CommitRow();
|
||||
}
|
||||
|
||||
// Forces the width to fill all the available space
|
||||
// (Total width - Padding Left - Padding Right)
|
||||
desiredMeasure.U = parentMeasure.U - paddingStart.U - paddingEnd.U;
|
||||
|
||||
// Adds the Section Header to the row
|
||||
currentRow.Add(position, desiredMeasure);
|
||||
|
||||
// Updates the global measures
|
||||
position.U += desiredMeasure.U + spacingMeasure.U;
|
||||
finalMeasure.U = Math.Max(finalMeasure.U, position.U);
|
||||
|
||||
CommitRow();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Checks if the item can fit in the row
|
||||
if ((desiredMeasure.U + position.U + paddingEnd.U) > parentMeasure.U)
|
||||
{
|
||||
CommitRow();
|
||||
}
|
||||
|
||||
if (isLast)
|
||||
{
|
||||
desiredMeasure.U = parentMeasure.U - position.U;
|
||||
}
|
||||
|
||||
currentRow.Add(position, desiredMeasure);
|
||||
|
||||
position.U += desiredMeasure.U + spacingMeasure.U;
|
||||
finalMeasure.U = Math.Max(finalMeasure.U, position.U);
|
||||
}
|
||||
}
|
||||
|
||||
var lastIndex = Children.Count - 1;
|
||||
for (var i = 0; i < lastIndex; i++)
|
||||
{
|
||||
Arrange(Children[i]);
|
||||
}
|
||||
|
||||
Arrange(Children[lastIndex], StretchChild == ToolkitStretchChild.Last);
|
||||
|
||||
if (currentRow.ChildrenRects.Count > 0)
|
||||
{
|
||||
_rows.Add(currentRow);
|
||||
}
|
||||
|
||||
if (_rows.Count == 0)
|
||||
{
|
||||
return paddingStart.Add(paddingEnd).ToSize(Orientation);
|
||||
}
|
||||
|
||||
var lastRowRect = _rows.Last().Rect;
|
||||
finalMeasure.V = lastRowRect.Position.V + lastRowRect.Size.V;
|
||||
return finalMeasure.Add(paddingEnd).ToSize(Orientation);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.UI;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Converters;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a color, either black or white, depending on the brightness of the supplied color.
|
||||
/// </summary>
|
||||
public sealed partial class ContrastBrushConverter : IValueConverter
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the alpha channel threshold below which a default color is used instead of black/white.
|
||||
/// </summary>
|
||||
public byte AlphaThreshold { get; set; } = 128;
|
||||
|
||||
/// <inheritdoc />
|
||||
public object Convert(
|
||||
object value,
|
||||
Type targetType,
|
||||
object parameter,
|
||||
string language)
|
||||
{
|
||||
Color comparisonColor;
|
||||
Color? defaultColor = null;
|
||||
|
||||
// Get the changing color to compare against
|
||||
if (value is Color valueColor)
|
||||
{
|
||||
comparisonColor = valueColor;
|
||||
}
|
||||
else if (value is SolidColorBrush valueBrush)
|
||||
{
|
||||
comparisonColor = valueBrush.Color;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Invalid color value provided
|
||||
return DependencyProperty.UnsetValue;
|
||||
}
|
||||
|
||||
// Get the default color when transparency is high
|
||||
if (parameter is Color parameterColor)
|
||||
{
|
||||
defaultColor = parameterColor;
|
||||
}
|
||||
else if (parameter is SolidColorBrush parameterBrush)
|
||||
{
|
||||
defaultColor = parameterBrush.Color;
|
||||
}
|
||||
|
||||
if (comparisonColor.A < AlphaThreshold &&
|
||||
defaultColor.HasValue)
|
||||
{
|
||||
// If the transparency is less than 50 %, just use the default brush
|
||||
// This can commonly be something like the TextControlForeground brush
|
||||
return new SolidColorBrush(defaultColor.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Chose a white/black brush based on contrast to the base color
|
||||
return UseLightContrastColor(comparisonColor)
|
||||
? new SolidColorBrush(Colors.White)
|
||||
: new SolidColorBrush(Colors.Black);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public object ConvertBack(
|
||||
object value,
|
||||
Type targetType,
|
||||
object parameter,
|
||||
string language)
|
||||
{
|
||||
return DependencyProperty.UnsetValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether a light or dark contrast color should be used with the given displayed color.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This code is using the WinUI algorithm.
|
||||
/// </remarks>
|
||||
private bool UseLightContrastColor(Color displayedColor)
|
||||
{
|
||||
// The selection ellipse should be light if and only if the chosen color
|
||||
// contrasts more with black than it does with white.
|
||||
// To find how much something contrasts with white, we use the equation
|
||||
// for relative luminance, which is given by
|
||||
//
|
||||
// L = 0.2126 * Rg + 0.7152 * Gg + 0.0722 * Bg
|
||||
//
|
||||
// where Xg = { X/3294 if X <= 10, (R/269 + 0.0513)^2.4 otherwise }
|
||||
//
|
||||
// If L is closer to 1, then the color is closer to white; if it is closer to 0,
|
||||
// then the color is closer to black. This is based on the fact that the human
|
||||
// eye perceives green to be much brighter than red, which in turn is perceived to be
|
||||
// brighter than blue.
|
||||
//
|
||||
// If the third dimension is value, then we won't be updating the spectrum's displayed colors,
|
||||
// so in that case we should use a value of 1 when considering the backdrop
|
||||
// for the selection ellipse.
|
||||
var rg = displayedColor.R <= 10
|
||||
? displayedColor.R / 3294.0
|
||||
: Math.Pow((displayedColor.R / 269.0) + 0.0513, 2.4);
|
||||
var gg = displayedColor.G <= 10
|
||||
? displayedColor.G / 3294.0
|
||||
: Math.Pow((displayedColor.G / 269.0) + 0.0513, 2.4);
|
||||
var bg = displayedColor.B <= 10
|
||||
? displayedColor.B / 3294.0
|
||||
: Math.Pow((displayedColor.B / 269.0) + 0.0513, 2.4);
|
||||
|
||||
return (0.2126 * rg) + (0.7152 * gg) + (0.0722 * bg) <= 0.5;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// 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;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
|
||||
namespace Microsoft.CmdPal.UI;
|
||||
|
||||
public partial class DetailsSizeToGridLengthConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
if (value is ContentSize size)
|
||||
{
|
||||
// This converter calculates the Star width for the LIST.
|
||||
//
|
||||
// The input 'size' (ContentSize) represents the TARGET WIDTH desired for the DETAILS PANEL.
|
||||
//
|
||||
// To ensure the Details Panel achieves its target size (e.g. ContentSize.Large),
|
||||
// we must shrink the List and let the Details fill the available space.
|
||||
// (e.g., A larger target size for Details results in a smaller Star value for the List).
|
||||
var starValue = size switch
|
||||
{
|
||||
ContentSize.Small => 3.0,
|
||||
ContentSize.Medium => 2.0,
|
||||
ContentSize.Large => 1.0,
|
||||
_ => 3.0,
|
||||
};
|
||||
|
||||
return new GridLength(starValue, GridUnitType.Star);
|
||||
}
|
||||
|
||||
return new GridLength(3.0, GridUnitType.Star);
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language) => throw new NotImplementedException();
|
||||
}
|
||||
@@ -18,8 +18,23 @@ internal sealed partial class GridItemTemplateSelector : DataTemplateSelector
|
||||
|
||||
public DataTemplate? Gallery { get; set; }
|
||||
|
||||
public DataTemplate? Section { get; set; }
|
||||
|
||||
public DataTemplate? Separator { get; set; }
|
||||
|
||||
protected override DataTemplate? SelectTemplateCore(object item, DependencyObject dependencyObject)
|
||||
{
|
||||
if (item is ListItemViewModel element && element.IsSectionOrSeparator)
|
||||
{
|
||||
if (dependencyObject is UIElement li)
|
||||
{
|
||||
li.IsTabStop = false;
|
||||
li.IsHitTestVisible = false;
|
||||
}
|
||||
|
||||
return string.IsNullOrWhiteSpace(element.Section) ? Separator : Section;
|
||||
}
|
||||
|
||||
return GridProperties switch
|
||||
{
|
||||
SmallGridPropertiesViewModel => Small,
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace Microsoft.CmdPal.UI;
|
||||
|
||||
public sealed partial class ListItemTemplateSelector : DataTemplateSelector
|
||||
{
|
||||
public DataTemplate? ListItem { get; set; }
|
||||
|
||||
public DataTemplate? Separator { get; set; }
|
||||
|
||||
public DataTemplate? Section { get; set; }
|
||||
|
||||
protected override DataTemplate? SelectTemplateCore(object item, DependencyObject container)
|
||||
{
|
||||
DataTemplate? dataTemplate = ListItem;
|
||||
|
||||
if (container is ListViewItem listItem)
|
||||
{
|
||||
if (item is ListItemViewModel element)
|
||||
{
|
||||
if (container is ListViewItem li && element.IsSectionOrSeparator)
|
||||
{
|
||||
li.IsEnabled = false;
|
||||
li.AllowFocusWhenDisabled = false;
|
||||
li.AllowFocusOnInteraction = false;
|
||||
li.IsHitTestVisible = false;
|
||||
dataTemplate = string.IsNullOrWhiteSpace(element.Section) ? Separator : Section;
|
||||
}
|
||||
else
|
||||
{
|
||||
listItem.IsEnabled = true;
|
||||
listItem.AllowFocusWhenDisabled = true;
|
||||
listItem.AllowFocusOnInteraction = true;
|
||||
listItem.IsHitTestVisible = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return dataTemplate;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// 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 System.Diagnostics.Tracing;
|
||||
using Microsoft.PowerToys.Telemetry;
|
||||
using Microsoft.PowerToys.Telemetry.Events;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Tracks extension usage with extension name and invocation details.
|
||||
/// Purpose: Identify popular vs. unused plugins and track extension performance.
|
||||
/// </summary>
|
||||
[EventData]
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
|
||||
public class CmdPalExtensionInvoked : EventBase, IEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the unique identifier of the extension provider.
|
||||
/// </summary>
|
||||
public string ExtensionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the non-localized identifier of the command being invoked.
|
||||
/// </summary>
|
||||
public string CommandId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the localized display name of the command being invoked.
|
||||
/// </summary>
|
||||
public string CommandName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the command executed successfully.
|
||||
/// </summary>
|
||||
public bool Success { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the execution time in milliseconds.
|
||||
/// </summary>
|
||||
public ulong ExecutionTimeMs { get; set; }
|
||||
|
||||
public CmdPalExtensionInvoked(string extensionId, string commandId, string commandName, bool success, ulong executionTimeMs)
|
||||
{
|
||||
EventName = "CmdPal_ExtensionInvoked";
|
||||
ExtensionId = extensionId;
|
||||
CommandId = commandId;
|
||||
CommandName = commandName;
|
||||
Success = success;
|
||||
ExecutionTimeMs = executionTimeMs;
|
||||
}
|
||||
|
||||
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
|
||||
}
|
||||
@@ -18,6 +18,7 @@ public class CmdPalInvokeResult : EventBase, IEvent
|
||||
|
||||
public CmdPalInvokeResult(CommandResultKind resultKind)
|
||||
{
|
||||
EventName = "CmdPal_InvokeResult";
|
||||
ResultKind = resultKind.ToString();
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user