mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-02-17 08:39:43 +01:00
Compare commits
186 Commits
main
...
dev/migrie
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0283d5b983 | ||
|
|
2876d2a496 | ||
|
|
01851e80b3 | ||
|
|
f40614ba8e | ||
|
|
72accf6ac2 | ||
|
|
767a7eba00 | ||
|
|
df4f6f19a2 | ||
|
|
c4fc884d7f | ||
|
|
01164b33ec | ||
|
|
6438793daa | ||
|
|
6c1f3d436b | ||
|
|
965520ffe6 | ||
|
|
84706830e7 | ||
|
|
c203335558 | ||
|
|
673db83022 | ||
|
|
a0a1f275b4 | ||
|
|
340bc7fe4b | ||
|
|
58850be91d | ||
|
|
311d05538b | ||
|
|
f531dd4de0 | ||
|
|
cc87469ada | ||
|
|
7159b8c17b | ||
|
|
ea354b6a8b | ||
|
|
f087c4c239 | ||
|
|
8b07c05887 | ||
|
|
b023a6bf96 | ||
|
|
bcb9a2a205 | ||
|
|
14c10b192a | ||
|
|
5da332fc9b | ||
|
|
a567cd6b19 | ||
|
|
f0179f2455 | ||
|
|
7d9b7582f9 | ||
|
|
637d6d7e96 | ||
|
|
c98689e385 | ||
|
|
a53dd0f040 | ||
|
|
5a328c429b | ||
|
|
41f14c4bf4 | ||
|
|
2913477f07 | ||
|
|
0cc5c08ea4 | ||
|
|
2494bf84b3 | ||
|
|
7f217609f9 | ||
|
|
a0c8b70697 | ||
|
|
7833526186 | ||
|
|
b56e4ea560 | ||
|
|
80e734587b | ||
|
|
daf586b86d | ||
|
|
95247aa6d5 | ||
|
|
947457d20c | ||
|
|
5d4a971bcf | ||
|
|
0cacfa3cc9 | ||
|
|
40f3425a1b | ||
|
|
c32f67bc5a | ||
|
|
ba2ad4b317 | ||
|
|
d46bb66c11 | ||
|
|
ea0af2bb9c | ||
|
|
806d9eebe6 | ||
|
|
e24cf24835 | ||
|
|
30ba1e7aca | ||
|
|
2996d4b9d5 | ||
|
|
efec6cfc03 | ||
|
|
d5017fffe4 | ||
|
|
1bffcfb6fa | ||
|
|
5d55c5c120 | ||
|
|
90df6f15ae | ||
|
|
aa6acf8145 | ||
|
|
3bae9a57e7 | ||
|
|
e0972996ff | ||
|
|
90afe7e9f5 | ||
|
|
884d1ec6d6 | ||
|
|
0b38be2c01 | ||
|
|
5d7211cf85 | ||
|
|
a5e93da8ad | ||
|
|
17aa472af5 | ||
|
|
96581fd24c | ||
|
|
41d12017b6 | ||
|
|
6e7add6feb | ||
|
|
d55bad1457 | ||
|
|
e02d384051 | ||
|
|
2293b76f7b | ||
|
|
b7ea22e017 | ||
|
|
d83dc94841 | ||
|
|
031f29418e | ||
|
|
fe533cd350 | ||
|
|
d4802fec40 | ||
|
|
12af2770e6 | ||
|
|
d64ccf686f | ||
|
|
17b840c583 | ||
|
|
421df9f1e0 | ||
|
|
806d383272 | ||
|
|
bffce430cc | ||
|
|
b310df6e2e | ||
|
|
1e1f0118d9 | ||
|
|
037062e754 | ||
|
|
19d77a4d02 | ||
|
|
72c0922af7 | ||
|
|
329a6434f8 | ||
|
|
b0250fc0f4 | ||
|
|
029c894ee4 | ||
|
|
f624625344 | ||
|
|
2c61ec1a8c | ||
|
|
3e4e3dd6f1 | ||
|
|
edcce595df | ||
|
|
ef0efcbe2f | ||
|
|
4de34eca96 | ||
|
|
f886d52484 | ||
|
|
017966e3db | ||
|
|
e499f90ee5 | ||
|
|
c0fe992e37 | ||
|
|
bd316d4d34 | ||
|
|
f03eb96b9c | ||
|
|
bbd15a3ae8 | ||
|
|
777a301666 | ||
|
|
f9e3ab4852 | ||
|
|
f311a65708 | ||
|
|
b9040d82c3 | ||
|
|
1d8b45f824 | ||
|
|
221cf083bc | ||
|
|
ccac1e1ac9 | ||
|
|
fb428b2d61 | ||
|
|
acb933643a | ||
|
|
f63785d80d | ||
|
|
87c1a73ecc | ||
|
|
44b0b9ac67 | ||
|
|
7629c6fbfa | ||
|
|
b8c024ac07 | ||
|
|
640c1a8388 | ||
|
|
78b2b23764 | ||
|
|
46d26041b9 | ||
|
|
08454f8b18 | ||
|
|
b7a65ab609 | ||
|
|
08d3435a0d | ||
|
|
46b8eea695 | ||
|
|
5b255011c7 | ||
|
|
6782829cdd | ||
|
|
6ed8d73b50 | ||
|
|
38dfee0234 | ||
|
|
d547a6f613 | ||
|
|
58bea1c813 | ||
|
|
5ad2bdf6c2 | ||
|
|
44f739a289 | ||
|
|
f3d9fc2342 | ||
|
|
90d4ca060e | ||
|
|
6554a4aaaa | ||
|
|
cac0048ca7 | ||
|
|
ddb28a8606 | ||
|
|
a7206863bc | ||
|
|
96def3b79a | ||
|
|
5231543ed2 | ||
|
|
2462da68bc | ||
|
|
bbfa6c6ccb | ||
|
|
f0ea908ee6 | ||
|
|
6e11230fed | ||
|
|
6c26e86e9a | ||
|
|
1d19705568 | ||
|
|
e5e20eca9c | ||
|
|
ef0639602f | ||
|
|
fdd4416049 | ||
|
|
0dab46e58f | ||
|
|
86d1061a25 | ||
|
|
e0197dd7a5 | ||
|
|
64ea63b77d | ||
|
|
bc6b2af03c | ||
|
|
c1af5fdc57 | ||
|
|
5be208520e | ||
|
|
5aaf0e010a | ||
|
|
48eee1b0d9 | ||
|
|
1447a825ee | ||
|
|
76f7dd3b09 | ||
|
|
ee174ddd1d | ||
|
|
35c4f8fdaa | ||
|
|
2ec7ae664e | ||
|
|
1b8ddaa849 | ||
|
|
d6bca1d38e | ||
|
|
b1d7626ab7 | ||
|
|
91598c091e | ||
|
|
fd3e73ee7e | ||
|
|
06a664a53a | ||
|
|
87d2509380 | ||
|
|
c1dc487f2c | ||
|
|
e0dd7ad44a | ||
|
|
aaa68fa351 | ||
|
|
d9e4133b5a | ||
|
|
821b99c4e0 | ||
|
|
8b5a2e9537 | ||
|
|
2e49835b4d | ||
|
|
ef106f6811 |
1
.github/actions/spell-check/allow/names.txt
vendored
1
.github/actions/spell-check/allow/names.txt
vendored
@@ -207,7 +207,6 @@ Bilibili
|
||||
BVID
|
||||
capturevideosample
|
||||
cmdow
|
||||
Contoso
|
||||
Controlz
|
||||
cortana
|
||||
devhints
|
||||
|
||||
2
.github/actions/spell-check/excludes.txt
vendored
2
.github/actions/spell-check/excludes.txt
vendored
@@ -143,5 +143,3 @@ ignore$
|
||||
^src/modules/registrypreview/RegistryPreviewUILib/Controls/HexBox/.*$
|
||||
^src/common/CalculatorEngineCommon/exprtk\.hpp$
|
||||
src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleMarkdownImagesPage.cs
|
||||
^src/modules/powerrename/unittests/testdata/avif_test\.avif$
|
||||
^src/modules/powerrename/unittests/testdata/heif_test\.heic$
|
||||
|
||||
943
.github/actions/spell-check/expect.txt
vendored
943
.github/actions/spell-check/expect.txt
vendored
File diff suppressed because it is too large
Load Diff
@@ -18,7 +18,6 @@
|
||||
"StylesReportTool\\PowerToys.StylesReportTool.exe",
|
||||
|
||||
"CalculatorEngineCommon.dll",
|
||||
"PowerToys.Common.UI.Controls.dll",
|
||||
"PowerToys.ManagedTelemetry.dll",
|
||||
"PowerToys.ManagedCommon.dll",
|
||||
"PowerToys.ManagedCsWin32.dll",
|
||||
|
||||
@@ -13,36 +13,9 @@ Param(
|
||||
|
||||
# Root folder Path for processing
|
||||
[Parameter(Mandatory=$False,Position=4)]
|
||||
[string]$sourceLink = "https://microsoft.pkgs.visualstudio.com/ProjectReunion/_packaging/Project.Reunion.nuget.internal/nuget/v3/index.json",
|
||||
|
||||
# Use Azure Pipeline artifact as source for metapackage
|
||||
[Parameter(Mandatory=$False,Position=5)]
|
||||
[boolean]$useArtifactSource = $False,
|
||||
|
||||
# Azure DevOps organization URL
|
||||
[Parameter(Mandatory=$False,Position=6)]
|
||||
[string]$azureDevOpsOrg = "https://dev.azure.com/microsoft",
|
||||
|
||||
# Azure DevOps project name
|
||||
[Parameter(Mandatory=$False,Position=7)]
|
||||
[string]$azureDevOpsProject = "ProjectReunion",
|
||||
|
||||
# Pipeline build ID (or "latest" for latest build)
|
||||
[Parameter(Mandatory=$False,Position=8)]
|
||||
[string]$buildId = "",
|
||||
|
||||
# Artifact name containing the NuGet packages
|
||||
[Parameter(Mandatory=$False,Position=9)]
|
||||
[string]$artifactName = "WindowsAppSDK_Nuget_And_MSIX",
|
||||
|
||||
# Metapackage name to look for in artifact
|
||||
[Parameter(Mandatory=$False,Position=10)]
|
||||
[string]$metaPackageName = "Microsoft.WindowsAppSDK"
|
||||
[string]$sourceLink = "https://microsoft.pkgs.visualstudio.com/ProjectReunion/_packaging/Project.Reunion.nuget.internal/nuget/v3/index.json"
|
||||
)
|
||||
|
||||
# Script-level constants
|
||||
$script:PackageVersionRegex = '^(.+?)\.(\d+\..*)$'
|
||||
|
||||
|
||||
|
||||
function Read-FileWithEncoding {
|
||||
@@ -84,7 +57,7 @@ function Add-NuGetSourceAndMapping {
|
||||
|
||||
# Ensure packageSources exists
|
||||
if (-not $Xml.configuration.packageSources) {
|
||||
$null = $Xml.configuration.AppendChild($Xml.CreateElement("packageSources"))
|
||||
$Xml.configuration.AppendChild($Xml.CreateElement("packageSources")) | Out-Null
|
||||
}
|
||||
$sources = $Xml.configuration.packageSources
|
||||
|
||||
@@ -93,13 +66,13 @@ function Add-NuGetSourceAndMapping {
|
||||
if (-not $sourceNode) {
|
||||
$sourceNode = $Xml.CreateElement("add")
|
||||
$sourceNode.SetAttribute("key", $Key)
|
||||
$null = $sources.AppendChild($sourceNode)
|
||||
$sources.AppendChild($sourceNode) | Out-Null
|
||||
}
|
||||
$sourceNode.SetAttribute("value", $Value)
|
||||
|
||||
# Ensure packageSourceMapping exists
|
||||
if (-not $Xml.configuration.packageSourceMapping) {
|
||||
$null = $Xml.configuration.AppendChild($Xml.CreateElement("packageSourceMapping"))
|
||||
$Xml.configuration.AppendChild($Xml.CreateElement("packageSourceMapping")) | Out-Null
|
||||
}
|
||||
$mapping = $Xml.configuration.packageSourceMapping
|
||||
|
||||
@@ -107,7 +80,7 @@ function Add-NuGetSourceAndMapping {
|
||||
$invalidNodes = $mapping.SelectNodes("packageSource[not(@key) or @key='']")
|
||||
if ($invalidNodes) {
|
||||
foreach ($node in $invalidNodes) {
|
||||
$null = $mapping.RemoveChild($node)
|
||||
$mapping.RemoveChild($node) | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,9 +91,9 @@ function Add-NuGetSourceAndMapping {
|
||||
$mappingSource.SetAttribute("key", $Key)
|
||||
# Insert at top for priority
|
||||
if ($mapping.HasChildNodes) {
|
||||
$null = $mapping.InsertBefore($mappingSource, $mapping.FirstChild)
|
||||
$mapping.InsertBefore($mappingSource, $mapping.FirstChild) | Out-Null
|
||||
} else {
|
||||
$null = $mapping.AppendChild($mappingSource)
|
||||
$mapping.AppendChild($mappingSource) | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,273 +110,14 @@ function Add-NuGetSourceAndMapping {
|
||||
foreach ($pattern in $Patterns) {
|
||||
$pkg = $Xml.CreateElement("package")
|
||||
$pkg.SetAttribute("pattern", $pattern)
|
||||
$null = $mappingSource.AppendChild($pkg)
|
||||
$mappingSource.AppendChild($pkg) | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
function Download-ArtifactFromPipeline {
|
||||
param (
|
||||
[string]$Organization,
|
||||
[string]$Project,
|
||||
[string]$BuildId,
|
||||
[string]$ArtifactName,
|
||||
[string]$OutputDir
|
||||
)
|
||||
|
||||
Write-Host "Downloading artifact '$ArtifactName' from build $BuildId..."
|
||||
$null = New-Item -ItemType Directory -Path $OutputDir -Force
|
||||
|
||||
try {
|
||||
# Authenticate with Azure DevOps using System Access Token (if available)
|
||||
if ($env:SYSTEM_ACCESSTOKEN) {
|
||||
Write-Host "Authenticating with Azure DevOps using System Access Token..."
|
||||
$env:AZURE_DEVOPS_EXT_PAT = $env:SYSTEM_ACCESSTOKEN
|
||||
} else {
|
||||
Write-Host "No SYSTEM_ACCESSTOKEN found, assuming az CLI is already authenticated..."
|
||||
}
|
||||
|
||||
# Use az CLI to download artifact
|
||||
& az pipelines runs artifact download `
|
||||
--organization $Organization `
|
||||
--project $Project `
|
||||
--run-id $BuildId `
|
||||
--artifact-name $ArtifactName `
|
||||
--path $OutputDir
|
||||
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Host "Successfully downloaded artifact to $OutputDir"
|
||||
return $true
|
||||
} else {
|
||||
Write-Warning "Failed to download artifact. Exit code: $LASTEXITCODE"
|
||||
return $false
|
||||
}
|
||||
} catch {
|
||||
Write-Warning "Error downloading artifact: $_"
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Get-NuspecDependencies {
|
||||
param (
|
||||
[string]$NupkgPath,
|
||||
[string]$TargetFramework = ""
|
||||
)
|
||||
|
||||
$tempDir = Join-Path $env:TEMP "nuspec_parse_$(Get-Random)"
|
||||
|
||||
try {
|
||||
# Extract .nupkg (it's a zip file)
|
||||
# Workaround: Expand-Archive may not recognize .nupkg extension, so copy to .zip first
|
||||
$tempZip = Join-Path $env:TEMP "temp_$(Get-Random).zip"
|
||||
Copy-Item $NupkgPath -Destination $tempZip -Force
|
||||
Expand-Archive -Path $tempZip -DestinationPath $tempDir -Force
|
||||
Remove-Item $tempZip -Force -ErrorAction SilentlyContinue
|
||||
|
||||
# Find .nuspec file
|
||||
$nuspecFile = Get-ChildItem -Path $tempDir -Filter "*.nuspec" -Recurse | Select-Object -First 1
|
||||
|
||||
if (-not $nuspecFile) {
|
||||
Write-Warning "No .nuspec file found in $NupkgPath"
|
||||
return @{}
|
||||
}
|
||||
|
||||
[xml]$nuspec = Get-Content $nuspecFile.FullName
|
||||
|
||||
# Extract package info
|
||||
$packageId = $nuspec.package.metadata.id
|
||||
$version = $nuspec.package.metadata.version
|
||||
Write-Host "Parsing $packageId version $version"
|
||||
|
||||
# Parse dependencies
|
||||
$dependencies = @{}
|
||||
$depGroups = $nuspec.package.metadata.dependencies.group
|
||||
|
||||
if ($depGroups) {
|
||||
# Dependencies are grouped by target framework
|
||||
foreach ($group in $depGroups) {
|
||||
$fx = $group.targetFramework
|
||||
Write-Host " Target Framework: $fx"
|
||||
|
||||
foreach ($dep in $group.dependency) {
|
||||
$depId = $dep.id
|
||||
$depVer = $dep.version
|
||||
# Remove version range brackets if present (e.g., "[2.0.0]" -> "2.0.0")
|
||||
$depVer = $depVer -replace '[\[\]]', ''
|
||||
$dependencies[$depId] = $depVer
|
||||
Write-Host " - ${depId} : ${depVer}"
|
||||
}
|
||||
}
|
||||
} else {
|
||||
# No grouping, direct dependencies
|
||||
$deps = $nuspec.package.metadata.dependencies.dependency
|
||||
if ($deps) {
|
||||
foreach ($dep in $deps) {
|
||||
$depId = $dep.id
|
||||
$depVer = $dep.version
|
||||
$depVer = $depVer -replace '[\[\]]', ''
|
||||
$dependencies[$depId] = $depVer
|
||||
Write-Host " - ${depId} : ${depVer}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $dependencies
|
||||
}
|
||||
catch {
|
||||
Write-Warning "Failed to parse nuspec: $_"
|
||||
return @{}
|
||||
}
|
||||
finally {
|
||||
Remove-Item $tempDir -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
function Resolve-ArtifactBasedDependencies {
|
||||
param (
|
||||
[string]$ArtifactDir,
|
||||
[string]$MetaPackageName,
|
||||
[string]$SourceUrl,
|
||||
[string]$OutputDir
|
||||
)
|
||||
|
||||
Write-Host "Resolving dependencies from artifact-based metapackage..."
|
||||
$null = New-Item -ItemType Directory -Path $OutputDir -Force
|
||||
|
||||
# Find the metapackage in artifact
|
||||
$metaNupkg = Get-ChildItem -Path $ArtifactDir -Recurse -Filter "$MetaPackageName.*.nupkg" |
|
||||
Where-Object { $_.Name -notmatch "Runtime" } |
|
||||
Select-Object -First 1
|
||||
|
||||
if (-not $metaNupkg) {
|
||||
Write-Warning "Metapackage $MetaPackageName not found in artifact"
|
||||
return @{}
|
||||
}
|
||||
|
||||
# Extract version from filename
|
||||
if ($metaNupkg.Name -match "$MetaPackageName\.(.+)\.nupkg") {
|
||||
$metaVersion = $Matches[1]
|
||||
Write-Host "Found metapackage: $MetaPackageName version $metaVersion"
|
||||
} else {
|
||||
Write-Warning "Could not extract version from $($metaNupkg.Name)"
|
||||
return @{}
|
||||
}
|
||||
|
||||
# Parse dependencies from metapackage
|
||||
$dependencies = Get-NuspecDependencies -NupkgPath $metaNupkg.FullName
|
||||
|
||||
# Copy metapackage to output directory
|
||||
Copy-Item $metaNupkg.FullName -Destination $OutputDir -Force
|
||||
Write-Host "Copied metapackage to $OutputDir"
|
||||
|
||||
# Prepare package versions hashtable - initialize with metapackage version
|
||||
$packageVersions = @{ $MetaPackageName = $metaVersion }
|
||||
|
||||
# Copy Runtime package from artifact (it's not in feed) and extract its version
|
||||
$runtimeNupkg = Get-ChildItem -Path $ArtifactDir -Recurse -Filter "$MetaPackageName.Runtime.*.nupkg" | Select-Object -First 1
|
||||
if ($runtimeNupkg) {
|
||||
Copy-Item $runtimeNupkg.FullName -Destination $OutputDir -Force
|
||||
Write-Host "Copied Runtime package to $OutputDir"
|
||||
|
||||
# Extract version from Runtime package filename
|
||||
if ($runtimeNupkg.Name -match "$MetaPackageName\.Runtime\.(.+)\.nupkg") {
|
||||
$runtimeVersion = $Matches[1]
|
||||
$packageVersions["$MetaPackageName.Runtime"] = $runtimeVersion
|
||||
Write-Host "Extracted Runtime package version: $runtimeVersion"
|
||||
} else {
|
||||
Write-Warning "Could not extract version from Runtime package: $($runtimeNupkg.Name)"
|
||||
}
|
||||
}
|
||||
|
||||
# Download other dependencies from feed (excluding Runtime as it's already copied)
|
||||
# Create temp nuget.config that includes both local packages and remote feed
|
||||
# This allows NuGet to find packages already copied from artifact
|
||||
$tempConfig = Join-Path $env:TEMP "nuget_artifact_$(Get-Random).config"
|
||||
$tempConfigContent = @"
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<clear />
|
||||
<add key='LocalPackages' value='$OutputDir' />
|
||||
<add key='RemoteFeed' value='$SourceUrl' />
|
||||
</packageSources>
|
||||
</configuration>
|
||||
"@
|
||||
Set-Content -Path $tempConfig -Value $tempConfigContent
|
||||
|
||||
try {
|
||||
foreach ($depId in $dependencies.Keys) {
|
||||
# Skip Runtime as it's already copied from artifact
|
||||
if ($depId -like "*Runtime*") {
|
||||
# Don't overwrite the version we extracted from the Runtime package filename
|
||||
if (-not $packageVersions.ContainsKey($depId)) {
|
||||
$packageVersions[$depId] = $dependencies[$depId]
|
||||
}
|
||||
Write-Host "Skipping $depId (already in artifact)"
|
||||
continue
|
||||
}
|
||||
|
||||
$depVersion = $dependencies[$depId]
|
||||
Write-Host "Downloading dependency: $depId version $depVersion from feed..."
|
||||
|
||||
& nuget install $depId `
|
||||
-Version $depVersion `
|
||||
-ConfigFile $tempConfig `
|
||||
-OutputDirectory $OutputDir `
|
||||
-NonInteractive `
|
||||
-NoCache `
|
||||
| Out-Null
|
||||
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
$packageVersions[$depId] = $depVersion
|
||||
Write-Host " Successfully downloaded $depId"
|
||||
} else {
|
||||
Write-Warning " Failed to download $depId version $depVersion"
|
||||
}
|
||||
}
|
||||
}
|
||||
finally {
|
||||
Remove-Item $tempConfig -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
# Parse all downloaded packages to get actual versions
|
||||
$directories = Get-ChildItem -Path $OutputDir -Directory
|
||||
$allLocalPackages = @()
|
||||
|
||||
# Add metapackage and runtime to the list (they are .nupkg files, not directories)
|
||||
$allLocalPackages += $MetaPackageName
|
||||
if ($packageVersions.ContainsKey("$MetaPackageName.Runtime")) {
|
||||
$allLocalPackages += "$MetaPackageName.Runtime"
|
||||
}
|
||||
|
||||
foreach ($dir in $directories) {
|
||||
if ($dir.Name -match $script:PackageVersionRegex) {
|
||||
$pkgId = $Matches[1]
|
||||
$pkgVer = $Matches[2]
|
||||
$allLocalPackages += $pkgId
|
||||
if (-not $packageVersions.ContainsKey($pkgId)) {
|
||||
$packageVersions[$pkgId] = $pkgVer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Update nuget.config dynamically during pipeline execution
|
||||
# This modification is temporary and won't be committed back to the repo
|
||||
$nugetConfig = Join-Path $rootPath "nuget.config"
|
||||
$configData = Read-FileWithEncoding -Path $nugetConfig
|
||||
[xml]$xml = $configData.Content
|
||||
|
||||
Add-NuGetSourceAndMapping -Xml $xml -Key "localpackages" -Value $OutputDir -Patterns $allLocalPackages
|
||||
|
||||
$xml.Save($nugetConfig)
|
||||
Write-Host "Updated nuget.config with localpackages mapping (temporary, for pipeline execution only)."
|
||||
|
||||
return ,$packageVersions
|
||||
}
|
||||
|
||||
function Resolve-WinAppSdkSplitDependencies {
|
||||
Write-Host "Version $WinAppSDKVersion detected. Resolving split dependencies..."
|
||||
$installDir = Join-Path $rootPath "localpackages\output"
|
||||
$null = New-Item -ItemType Directory -Path $installDir -Force
|
||||
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"
|
||||
@@ -417,24 +131,14 @@ function Resolve-WinAppSdkSplitDependencies {
|
||||
if ($propsContent -match '<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="([^"]+)"') {
|
||||
$buildToolsVersion = $Matches[1]
|
||||
Write-Host "Downloading Microsoft.Windows.SDK.BuildTools version $buildToolsVersion..."
|
||||
& nuget install Microsoft.Windows.SDK.BuildTools `
|
||||
-Version $buildToolsVersion `
|
||||
-ConfigFile $tempConfig `
|
||||
-OutputDirectory $installDir `
|
||||
-NonInteractive `
|
||||
-NoCache `
|
||||
| Out-Null
|
||||
$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
|
||||
& nuget install Microsoft.WindowsAppSDK `
|
||||
-Version $WinAppSDKVersion `
|
||||
-ConfigFile $tempConfig `
|
||||
-OutputDirectory $installDir `
|
||||
-NonInteractive `
|
||||
-NoCache `
|
||||
| Out-Null
|
||||
$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}
|
||||
@@ -468,101 +172,52 @@ function Resolve-WinAppSdkSplitDependencies {
|
||||
}
|
||||
}
|
||||
|
||||
# Main logic: choose between artifact-based or feed-based approach
|
||||
if ($useArtifactSource) {
|
||||
Write-Host "=== Using Artifact-Based Source ===" -ForegroundColor Cyan
|
||||
Write-Host "Organization: $azureDevOpsOrg"
|
||||
Write-Host "Project: $azureDevOpsProject"
|
||||
Write-Host "Build ID: $buildId"
|
||||
Write-Host "Artifact: $artifactName"
|
||||
|
||||
if ([string]::IsNullOrEmpty($buildId) -or $buildId -eq 'N/A') {
|
||||
Write-Error "buildId parameter is required when using artifact source. Please provide a valid Windows App SDK Build ID."
|
||||
Write-Host "Tip: You can find the build ID from the Windows App SDK pipeline run in Azure DevOps."
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Download artifact
|
||||
$artifactDir = Join-Path $rootPath "localpackages\artifact"
|
||||
$downloadSuccess = Download-ArtifactFromPipeline `
|
||||
-Organization $azureDevOpsOrg `
|
||||
-Project $azureDevOpsProject `
|
||||
-BuildId $buildId `
|
||||
-ArtifactName $artifactName `
|
||||
-OutputDir $artifactDir
|
||||
|
||||
if (-not $downloadSuccess) {
|
||||
Write-Host "Failed to download artifact"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Resolve dependencies from artifact
|
||||
$installDir = Join-Path $rootPath "localpackages\output"
|
||||
$packageVersions = Resolve-ArtifactBasedDependencies `
|
||||
-ArtifactDir $artifactDir `
|
||||
-MetaPackageName $metaPackageName `
|
||||
-SourceUrl $sourceLink `
|
||||
-OutputDir $installDir
|
||||
|
||||
if ($packageVersions.Count -eq 0) {
|
||||
Write-Error "Failed to resolve dependencies from artifact"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$WinAppSDKVersion = $packageVersions[$metaPackageName]
|
||||
Write-Host "WinAppSDK Version: $WinAppSDKVersion"
|
||||
Write-Host "##vso[task.setvariable variable=WinAppSDKVersion]$WinAppSDKVersion"
|
||||
|
||||
# Execute nuget list and capture the output
|
||||
if ($useExperimentalVersion) {
|
||||
# The nuget list for experimental versions will cost more time
|
||||
# So, we will not use -AllVersions to wast time
|
||||
# But it can only get the latest experimental version
|
||||
Write-Host "Fetching WindowsAppSDK with experimental versions"
|
||||
$nugetOutput = nuget list Microsoft.WindowsAppSDK `
|
||||
-Source $sourceLink `
|
||||
-Prerelease
|
||||
# Filter versions based on the specified version prefix
|
||||
$escapedVersionNumber = [regex]::Escape($winAppSdkVersionNumber)
|
||||
$filteredVersions = $nugetOutput | Where-Object { $_ -match "Microsoft.WindowsAppSDK $escapedVersionNumber\." }
|
||||
$latestVersions = $filteredVersions
|
||||
} else {
|
||||
Write-Host "=== Using Feed-Based Source ===" -ForegroundColor Cyan
|
||||
|
||||
# Execute nuget list and capture the output
|
||||
if ($useExperimentalVersion) {
|
||||
# The nuget list for experimental versions will cost more time
|
||||
# So, we will not use -AllVersions to wast time
|
||||
# But it can only get the latest experimental version
|
||||
Write-Host "Fetching WindowsAppSDK with experimental versions"
|
||||
$nugetOutput = nuget list Microsoft.WindowsAppSDK `
|
||||
-Source $sourceLink `
|
||||
-Prerelease
|
||||
# Filter versions based on the specified version prefix
|
||||
$escapedVersionNumber = [regex]::Escape($winAppSdkVersionNumber)
|
||||
$filteredVersions = $nugetOutput | Where-Object { $_ -match "Microsoft.WindowsAppSDK $escapedVersionNumber\." }
|
||||
$latestVersions = $filteredVersions
|
||||
} else {
|
||||
Write-Host "Fetching stable WindowsAppSDK versions for $winAppSdkVersionNumber"
|
||||
$nugetOutput = nuget list Microsoft.WindowsAppSDK `
|
||||
-Source $sourceLink `
|
||||
-AllVersions
|
||||
# Filter versions based on the specified version prefix
|
||||
$escapedVersionNumber = [regex]::Escape($winAppSdkVersionNumber)
|
||||
$filteredVersions = $nugetOutput | Where-Object { $_ -match "Microsoft.WindowsAppSDK $escapedVersionNumber\." }
|
||||
$latestVersions = $filteredVersions | Sort-Object { [version]($_ -split ' ')[1] } -Descending | Select-Object -First 1
|
||||
}
|
||||
|
||||
Write-Host "Latest versions found: $latestVersions"
|
||||
# Extract the latest version number from the output
|
||||
$latestVersion = $latestVersions -split "`n" | `
|
||||
Select-String -Pattern 'Microsoft.WindowsAppSDK\s*([0-9]+\.[0-9]+\.[0-9]+-*[a-zA-Z0-9]*)' | `
|
||||
ForEach-Object { $_.Matches[0].Groups[1].Value } | `
|
||||
Sort-Object -Descending | `
|
||||
Select-Object -First 1
|
||||
|
||||
if ($latestVersion) {
|
||||
$WinAppSDKVersion = $latestVersion
|
||||
Write-Host "Extracted version: $WinAppSDKVersion"
|
||||
Write-Host "##vso[task.setvariable variable=WinAppSDKVersion]$WinAppSDKVersion"
|
||||
} else {
|
||||
Write-Host "Failed to extract version number from nuget list output"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Resolve dependencies for 1.8+
|
||||
$packageVersions = @{ "Microsoft.WindowsAppSDK" = $WinAppSDKVersion }
|
||||
|
||||
Resolve-WinAppSdkSplitDependencies
|
||||
Write-Host "Fetching stable WindowsAppSDK versions for $winAppSdkVersionNumber"
|
||||
$nugetOutput = nuget list Microsoft.WindowsAppSDK `
|
||||
-Source $sourceLink `
|
||||
-AllVersions
|
||||
# Filter versions based on the specified version prefix
|
||||
$escapedVersionNumber = [regex]::Escape($winAppSdkVersionNumber)
|
||||
$filteredVersions = $nugetOutput | Where-Object { $_ -match "Microsoft.WindowsAppSDK $escapedVersionNumber\." }
|
||||
$latestVersions = $filteredVersions | Sort-Object { [version]($_ -split ' ')[1] } -Descending | Select-Object -First 1
|
||||
}
|
||||
|
||||
Write-Host "Latest versions found: $latestVersions"
|
||||
# Extract the latest version number from the output
|
||||
$latestVersion = $latestVersions -split "`n" | `
|
||||
Select-String -Pattern 'Microsoft.WindowsAppSDK\s*([0-9]+\.[0-9]+\.[0-9]+-*[a-zA-Z0-9]*)' | `
|
||||
ForEach-Object { $_.Matches[0].Groups[1].Value } | `
|
||||
Sort-Object -Descending | `
|
||||
Select-Object -First 1
|
||||
|
||||
if ($latestVersion) {
|
||||
$WinAppSDKVersion = $latestVersion
|
||||
Write-Host "Extracted version: $WinAppSDKVersion"
|
||||
Write-Host "##vso[task.setvariable variable=WinAppSDKVersion]$WinAppSDKVersion"
|
||||
} else {
|
||||
Write-Host "Failed to extract version number from nuget list output"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# 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
|
||||
@@ -571,16 +226,9 @@ Get-ChildItem -Path $rootPath -Recurse "Directory.Packages.props" | ForEach-Obje
|
||||
|
||||
foreach ($pkgId in $packageVersions.Keys) {
|
||||
$ver = $packageVersions[$pkgId]
|
||||
|
||||
# Skip packages with empty versions to prevent corruption
|
||||
if ([string]::IsNullOrWhiteSpace($ver)) {
|
||||
Write-Warning "Skipping ${pkgId}: version is empty"
|
||||
continue
|
||||
}
|
||||
|
||||
# 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]*"" />"
|
||||
|
||||
|
||||
@@ -1,8 +1,3 @@
|
||||
# NOTE: When using artifact mode (useArtifactSource: true), the pipeline needs
|
||||
# permission to access System.AccessToken. This is automatically handled by the
|
||||
# script if SYSTEM_ACCESSTOKEN environment variable is available.
|
||||
# If you encounter authentication errors, ensure the job has oauth access enabled.
|
||||
|
||||
trigger: none
|
||||
pr: none
|
||||
schedules:
|
||||
@@ -42,23 +37,6 @@ parameters:
|
||||
- name: useExperimentalVersion
|
||||
type: boolean
|
||||
default: false
|
||||
# Artifact mode parameters (optional)
|
||||
- name: useArtifactSource
|
||||
type: boolean
|
||||
displayName: "Use Artifact Source (instead of feed)"
|
||||
default: false
|
||||
- name: buildId
|
||||
type: string
|
||||
displayName: "Windows App SDK Build ID (required only if using artifact source)"
|
||||
default: 'N/A'
|
||||
- name: azureDevOpsProject
|
||||
type: string
|
||||
displayName: "Source Project (for artifact mode, default: ProjectReunion)"
|
||||
default: 'ProjectReunion'
|
||||
- name: artifactName
|
||||
type: string
|
||||
displayName: "Artifact Name (for artifact mode, default: WindowsAppSDK_Nuget_And_MSIX)"
|
||||
default: 'WindowsAppSDK_Nuget_And_MSIX'
|
||||
|
||||
extends:
|
||||
template: templates/pipeline-ci-build.yml
|
||||
@@ -71,7 +49,3 @@ extends:
|
||||
useLatestWinAppSDK: ${{ parameters.useLatestWinAppSDK }}
|
||||
winAppSDKVersionNumber: ${{ parameters.winAppSDKVersionNumber }}
|
||||
useExperimentalVersion: ${{ parameters.useExperimentalVersion }}
|
||||
useArtifactSource: ${{ parameters.useArtifactSource }}
|
||||
buildId: ${{ parameters.buildId }}
|
||||
azureDevOpsProject: ${{ parameters.azureDevOpsProject }}
|
||||
artifactName: ${{ parameters.artifactName }}
|
||||
|
||||
@@ -74,25 +74,6 @@ parameters:
|
||||
- name: useExperimentalVersion
|
||||
type: boolean
|
||||
default: false
|
||||
# Artifact mode parameters
|
||||
- name: useArtifactSource
|
||||
type: boolean
|
||||
default: false
|
||||
- name: azureDevOpsOrg
|
||||
type: string
|
||||
default: 'https://dev.azure.com/microsoft'
|
||||
- name: azureDevOpsProject
|
||||
type: string
|
||||
default: 'ProjectReunion'
|
||||
- name: buildId
|
||||
type: string
|
||||
default: ''
|
||||
- name: artifactName
|
||||
type: string
|
||||
default: 'WindowsAppSDK_Nuget_And_MSIX'
|
||||
- name: metaPackageName
|
||||
type: string
|
||||
default: 'Microsoft.WindowsAppSDK'
|
||||
- name: csProjectsToPublish
|
||||
type: object
|
||||
default:
|
||||
@@ -245,12 +226,6 @@ jobs:
|
||||
parameters:
|
||||
versionNumber: ${{ parameters.winAppSDKVersionNumber }}
|
||||
useExperimentalVersion: ${{ parameters.useExperimentalVersion }}
|
||||
useArtifactSource: ${{ parameters.useArtifactSource }}
|
||||
azureDevOpsOrg: ${{ parameters.azureDevOpsOrg }}
|
||||
azureDevOpsProject: ${{ parameters.azureDevOpsProject }}
|
||||
buildId: ${{ parameters.buildId }}
|
||||
artifactName: ${{ parameters.artifactName }}
|
||||
metaPackageName: ${{ parameters.metaPackageName }}
|
||||
|
||||
- ${{ if eq(parameters.useLatestWinAppSDK, false)}}:
|
||||
- template: .\steps-restore-nuget.yml
|
||||
|
||||
@@ -108,6 +108,9 @@ jobs:
|
||||
sdk: true
|
||||
version: '9.0'
|
||||
|
||||
- task: VisualStudioTestPlatformInstaller@1
|
||||
displayName: Ensure VSTest Platform
|
||||
|
||||
- pwsh: |-
|
||||
& '$(build.sourcesdirectory)\.pipelines\InstallWinAppDriver.ps1'
|
||||
displayName: Download and install WinAppDriver
|
||||
@@ -149,7 +152,46 @@ jobs:
|
||||
inputs:
|
||||
displaySettings: 'optimal'
|
||||
|
||||
- script: |
|
||||
dotnet test $(Build.SourcesDirectory)\src\modules\fancyzones\FancyZones.UITests\FancyZones.UITests.csproj --no-build -c $(BuildConfiguration) -p:Platform=$(BuildPlatform)
|
||||
dotnet test $(Build.SourcesDirectory)\src\modules\fancyzones\FancyZonesEditor.UITests\FancyZonesEditor.UITests.csproj --no-build -c $(BuildConfiguration) -p:Platform=$(BuildPlatform)
|
||||
displayName: "Run UI Tests"
|
||||
- ${{ if eq(length(parameters.uiTestModules), 0) }}:
|
||||
- task: VSTest@3
|
||||
displayName: Run UI Tests
|
||||
inputs:
|
||||
platform: '$(BuildPlatform)'
|
||||
configuration: '$(BuildConfiguration)'
|
||||
testSelector: 'testAssemblies'
|
||||
searchFolder: '$(Pipeline.Workspace)\$(TestArtifactsName)'
|
||||
vsTestVersion: 'toolsInstaller'
|
||||
uiTests: true
|
||||
rerunFailedTests: true
|
||||
testRunTitle: 'UITests_${{ parameters.platform }}_${{ parameters.installMode }}'
|
||||
# Since UITests-FancyZonesEditor.dll is generated in both UITests-FancyZonesEditor and UITests-FancyZones, removed one to avoid duplicate test runs
|
||||
testAssemblyVer2: |
|
||||
**\*UITest*.dll
|
||||
!**\obj\**
|
||||
!**\ref\**
|
||||
!**\UITests-FancyZones\**\UITests-FancyZonesEditor.dll
|
||||
env:
|
||||
platform: '$(TestPlatform)'
|
||||
useInstallerForTest: ${{ ne(parameters.buildSource, 'buildNow') }}
|
||||
|
||||
- ${{ if ne(length(parameters.uiTestModules), 0) }}:
|
||||
- ${{ each module in parameters.uiTestModules }}:
|
||||
- task: VSTest@3
|
||||
displayName: Run UI Test - ${{ module }}
|
||||
inputs:
|
||||
platform: '$(BuildPlatform)'
|
||||
configuration: '$(BuildConfiguration)'
|
||||
testSelector: 'testAssemblies'
|
||||
searchFolder: '$(Pipeline.Workspace)\$(TestArtifactsName)'
|
||||
vsTestVersion: 'toolsInstaller'
|
||||
uiTests: true
|
||||
rerunFailedTests: true
|
||||
testRunTitle: 'UITests_${{ parameters.platform }}_${{ parameters.installMode }}'
|
||||
testAssemblyVer2: |
|
||||
**\*${{ module }}*.dll
|
||||
!**\obj\**
|
||||
!**\ref\**
|
||||
!**\UITests-FancyZones\**\UITests-FancyZonesEditor.dll
|
||||
env:
|
||||
platform: '$(TestPlatform)'
|
||||
useInstallerForTest: ${{ ne(parameters.buildSource, 'buildNow') }}
|
||||
|
||||
@@ -34,25 +34,6 @@ parameters:
|
||||
- name: useExperimentalVersion
|
||||
type: boolean
|
||||
default: false
|
||||
# Artifact mode parameters
|
||||
- name: useArtifactSource
|
||||
type: boolean
|
||||
default: false
|
||||
- name: azureDevOpsOrg
|
||||
type: string
|
||||
default: 'https://dev.azure.com/microsoft'
|
||||
- name: azureDevOpsProject
|
||||
type: string
|
||||
default: 'ProjectReunion'
|
||||
- name: buildId
|
||||
type: string
|
||||
default: ''
|
||||
- name: artifactName
|
||||
type: string
|
||||
default: 'WindowsAppSDK_Nuget_And_MSIX'
|
||||
- name: metaPackageName
|
||||
type: string
|
||||
default: 'Microsoft.WindowsAppSDK'
|
||||
|
||||
stages:
|
||||
- ${{ each platform in parameters.buildPlatforms }}:
|
||||
@@ -84,12 +65,6 @@ stages:
|
||||
${{ if eq(parameters.useLatestWinAppSDK, true) }}:
|
||||
winAppSDKVersionNumber: ${{ parameters.winAppSDKVersionNumber }}
|
||||
useExperimentalVersion: ${{ parameters.useExperimentalVersion }}
|
||||
useArtifactSource: ${{ parameters.useArtifactSource }}
|
||||
azureDevOpsOrg: ${{ parameters.azureDevOpsOrg }}
|
||||
azureDevOpsProject: ${{ parameters.azureDevOpsProject }}
|
||||
buildId: ${{ parameters.buildId }}
|
||||
artifactName: ${{ parameters.artifactName }}
|
||||
metaPackageName: ${{ parameters.metaPackageName }}
|
||||
timeoutInMinutes: 90
|
||||
|
||||
- stage: Build_SDK
|
||||
|
||||
@@ -5,25 +5,6 @@ parameters:
|
||||
- name: useExperimentalVersion
|
||||
type: boolean
|
||||
default: false
|
||||
# Artifact mode parameters
|
||||
- name: useArtifactSource
|
||||
type: boolean
|
||||
default: false
|
||||
- name: azureDevOpsOrg
|
||||
type: string
|
||||
default: 'https://dev.azure.com/microsoft'
|
||||
- name: azureDevOpsProject
|
||||
type: string
|
||||
default: 'ProjectReunion'
|
||||
- name: buildId
|
||||
type: string
|
||||
default: ''
|
||||
- name: artifactName
|
||||
type: string
|
||||
default: 'WindowsAppSDK_Nuget_And_MSIX'
|
||||
- name: metaPackageName
|
||||
type: string
|
||||
default: 'Microsoft.WindowsAppSDK'
|
||||
|
||||
steps:
|
||||
- task: NuGetAuthenticate@1
|
||||
@@ -31,20 +12,12 @@ steps:
|
||||
|
||||
- task: PowerShell@2
|
||||
displayName: Update WinAppSDK Versions
|
||||
env:
|
||||
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
|
||||
inputs:
|
||||
filePath: '$(build.sourcesdirectory)\.pipelines\UpdateVersions.ps1'
|
||||
arguments: >
|
||||
-winAppSdkVersionNumber ${{ parameters.versionNumber }}
|
||||
-useExperimentalVersion $${{ parameters.useExperimentalVersion }}
|
||||
-rootPath "$(build.sourcesdirectory)"
|
||||
-useArtifactSource $${{ parameters.useArtifactSource }}
|
||||
-azureDevOpsOrg "${{ parameters.azureDevOpsOrg }}"
|
||||
-azureDevOpsProject "${{ parameters.azureDevOpsProject }}"
|
||||
-buildId "${{ parameters.buildId }}"
|
||||
-artifactName "${{ parameters.artifactName }}"
|
||||
-metaPackageName "${{ parameters.metaPackageName }}"
|
||||
|
||||
# - task: NuGetCommand@2
|
||||
# displayName: 'Restore NuGet packages (slnx)'
|
||||
@@ -63,4 +36,3 @@ steps:
|
||||
feedsToUse: 'config'
|
||||
nugetConfigPath: '$(build.sourcesdirectory)\nuget.config'
|
||||
workingDirectory: '$(build.sourcesdirectory)'
|
||||
arguments: '/p:NoWarn=NU1602,NU1604'
|
||||
|
||||
@@ -93,8 +93,7 @@ if ($noticeMatch.Success) {
|
||||
# Test-only packages that are allowed to be in NOTICE.md but not in the build
|
||||
# (e.g., when BuildTests=false, these packages won't appear in the NuGet list)
|
||||
$allowedExtraPackages = @(
|
||||
"- Moq",
|
||||
"- MSTest"
|
||||
"- Moq"
|
||||
)
|
||||
|
||||
if (!$noticeFile.Trim().EndsWith($returnList.Trim()))
|
||||
|
||||
@@ -20,23 +20,6 @@
|
||||
<NuGetAuditMode>direct</NuGetAuditMode>
|
||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion> <!-- Don't add source revision hash to the product version of binaries. -->
|
||||
<PlatformTarget>$(Platform)</PlatformTarget>
|
||||
|
||||
<!-- Enable Microsoft.Testing.Platform -->
|
||||
<EnableMSTestRunner>true</EnableMSTestRunner>
|
||||
<TestingPlatformShowTestsFailure>true</TestingPlatformShowTestsFailure>
|
||||
<TestingPlatformDotNetTestSupport>true</TestingPlatformDotNetTestSupport>
|
||||
<TestingPlatformCommandLineArguments>$(TestingPlatformCommandLineArguments) --report-trx</TestingPlatformCommandLineArguments>
|
||||
<!-- No arm64 agents to run the tests. -->
|
||||
<TestingPlatformDisableCustomTestTarget Condition="'$(Platform)' == 'ARM64'">true</TestingPlatformDisableCustomTestTarget>
|
||||
</PropertyGroup>
|
||||
|
||||
<!--
|
||||
UI tests are run in dedicated UI test jobs/pipelines.
|
||||
In CI, the main build uses `/t:Build;Test` across the full solution, so
|
||||
prevent UI test projects from being executed in that pass.
|
||||
-->
|
||||
<PropertyGroup Condition="'$(TF_BUILD)' != '' and $(MSBuildProjectName.Contains('UITest'))">
|
||||
<TestingPlatformDisableCustomTestTarget>true</TestingPlatformDisableCustomTestTarget>
|
||||
</PropertyGroup>
|
||||
|
||||
<!--
|
||||
@@ -99,15 +82,7 @@
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<!-- In CI, we build and test with `/t:Build;Test` -->
|
||||
<!-- So, for non-test projects, we want the target to be there and it's basically doing nothing -->
|
||||
<!-- For C# test projects, Microsoft.Testing.Platform should inject Test target here: -->
|
||||
<!-- https://github.com/microsoft/testfx/blob/5ad21909704db501f58f27d4a7ec241edd761af5/src/Platform/Microsoft.Testing.Platform.MSBuild/buildMultiTargeting/Microsoft.Testing.Platform.MSBuild.targets#L270-L273 -->
|
||||
<!-- For C++ test projects, the RunVSTest SDK will do its job -->
|
||||
<Target Name="Test" />
|
||||
|
||||
<!-- Add ability to run tests via "msbuild /t:Test" using the RunVSTest SDK -->
|
||||
<!-- This is only needed for C++, as we use Microsoft.Testing.Platform for C# -->
|
||||
<!-- Add ability to run tests via "msbuild /t:Test" -->
|
||||
<!--
|
||||
Work around an MSBuild bug where Microsoft.Common.Test.targets is missing from the Arm64 installation.
|
||||
See: https://github.com/dotnet/msbuild/pull/9984
|
||||
@@ -117,11 +92,11 @@
|
||||
Once the change referenced above is fixed, the ImportGroup below can be replaced with:
|
||||
<Sdk Name="Microsoft.Build.RunVSTest" Version="1.0.319" />
|
||||
-->
|
||||
<ImportGroup Condition="'$(PROCESSOR_ARCHITECTURE)' != 'ARM64' AND ('$(Language)' == 'C++' OR '$(MSBuildProjectExtension)' == '.vcxproj')">
|
||||
<ImportGroup Condition="'$(PROCESSOR_ARCHITECTURE)' != 'ARM64'">
|
||||
<Import Project="Sdk.props" Sdk="Microsoft.Build.RunVSTest" Version="1.0.319" />
|
||||
<Import Project="Sdk.targets" Sdk="Microsoft.Build.RunVSTest" Version="1.0.319" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Condition="'$(Language)' == 'C++' OR '$(MSBuildProjectExtension)' == '.vcxproj'">
|
||||
<PropertyGroup>
|
||||
<VSTestLogger>trx</VSTestLogger>
|
||||
<!--
|
||||
RunVSTest by default uses %VSINSTALLDIR%\Common7\IDE\CommonExtensions\Microsoft\TestWindow\vstest.console.exe,
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
<PropertyGroup>
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
|
||||
<MSTestVersion>3.8.3</MSTestVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="AdaptiveCards.ObjectModel.WinUI3" Version="2.0.0-beta" />
|
||||
@@ -87,8 +86,7 @@
|
||||
<PackageVersion Include="ModernWpfUI" Version="0.9.4" />
|
||||
<!-- Moq to stay below v4.20 due to behavior change. need to be sure fixed -->
|
||||
<PackageVersion Include="Moq" Version="4.18.4" />
|
||||
<PackageVersion Include="MSTest" Version="$(MSTestVersion)" />
|
||||
<PackageVersion Include="MSTest.TestFramework" Version="$(MSTestVersion)" />
|
||||
<PackageVersion Include="MSTest" Version="3.8.3" />
|
||||
<PackageVersion Include="NJsonSchema" Version="11.4.0" />
|
||||
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageVersion Include="NLog" Version="5.2.8" />
|
||||
|
||||
@@ -1582,7 +1582,6 @@ SOFTWARE.
|
||||
- ModernWpfUI
|
||||
- Moq
|
||||
- MSTest
|
||||
- MSTest.TestFramework
|
||||
- NJsonSchema
|
||||
- NLog
|
||||
- NLog.Extensions.Logging
|
||||
@@ -1603,4 +1602,4 @@ SOFTWARE.
|
||||
- WinUIEx
|
||||
- WmiLight
|
||||
- WPF-UI
|
||||
- WyHash
|
||||
- WyHash
|
||||
@@ -13,10 +13,6 @@
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
<Project Path="src/common/Common.UI.Controls/Common.UI.Controls.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
<Project Path="src/common/COMUtils/COMUtils.vcxproj" Id="7319089e-46d6-4400-bc65-e39bdf1416ee" />
|
||||
<Project Path="src/common/Display/Display.vcxproj" Id="caba8dfb-823b-4bf2-93ac-3f31984150d9" />
|
||||
<Project Path="src/common/FilePreviewCommon/FilePreviewCommon.csproj">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<clear />
|
||||
|
||||
@@ -4,9 +4,8 @@
|
||||
<Import Project=".\Common.Dotnet.PrepareGeneratedFolder.targets" />
|
||||
|
||||
<PropertyGroup>
|
||||
<CoreTargetFramework>net9.0</CoreTargetFramework>
|
||||
<WindowsSdkPackageVersion>10.0.26100.68-preview</WindowsSdkPackageVersion>
|
||||
<TargetFramework>$(CoreTargetFramework)-windows10.0.26100.0</TargetFramework>
|
||||
<TargetFramework>net9.0-windows10.0.26100.0</TargetFramework>
|
||||
<TargetPlatformMinVersion>10.0.19041.0</TargetPlatformMinVersion>
|
||||
<SupportedOSPlatformVersion>10.0.19041.0</SupportedOSPlatformVersion>
|
||||
<RuntimeIdentifiers>win-x64;win-arm64</RuntimeIdentifiers>
|
||||
|
||||
@@ -7,13 +7,4 @@
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0-windows10.0.26100.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<!--
|
||||
In CI, the main build runs `/t:Build;Test` across the full solution.
|
||||
Fuzz test projects are built for OneFuzz ingestion, but should not be
|
||||
executed as regular MSTest tests in this pass.
|
||||
-->
|
||||
<PropertyGroup Condition="'$(TF_BUILD)' != ''">
|
||||
<TestingPlatformDisableCustomTestTarget>true</TestingPlatformDisableCustomTestTarget>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="$(RepoRoot)src\Common.Dotnet.CsWinRT.props" />
|
||||
<Import Project="$(RepoRoot)src\Common.SelfContained.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Library</OutputType>
|
||||
<TargetFramework>net9.0-windows10.0.26100.0</TargetFramework>
|
||||
<RootNamespace>Microsoft.PowerToys.Common.UI.Controls</RootNamespace>
|
||||
<AssemblyName>PowerToys.Common.UI.Controls</AssemblyName>
|
||||
<UseWinUI>true</UseWinUI>
|
||||
<EnablePreviewMsixTooling>true</EnablePreviewMsixTooling>
|
||||
<GenerateLibraryLayout>true</GenerateLibraryLayout>
|
||||
<ProjectPriFileName>PowerToys.Common.UI.Controls.pri</ProjectPriFileName>
|
||||
<Nullable>enable</Nullable>
|
||||
<Platforms>x64;ARM64</Platforms>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Extensions" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Converters" />
|
||||
<PackageReference Include="CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ManagedCommon\ManagedCommon.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,8 +0,0 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="ms-appx:///PowerToys.Common.UI.Controls/Controls/KeyVisual/KeyVisual.xaml" />
|
||||
<ResourceDictionary Source="ms-appx:///PowerToys.Common.UI.Controls/Controls/KeyVisual/KeyCharPresenter.xaml" />
|
||||
<ResourceDictionary Source="ms-appx:///PowerToys.Common.UI.Controls/Controls/IsEnabledTextBlock/IsEnabledTextBlock.xaml" />
|
||||
<ResourceDictionary Source="ms-appx:///PowerToys.Common.UI.Controls/Controls/ShortcutWithTextLabelControl/ShortcutWithTextLabelControl.xaml" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
@@ -8,6 +8,7 @@ using System.Runtime.CompilerServices;
|
||||
using System.Xml.Linq;
|
||||
using ABI.Windows.Foundation;
|
||||
using Microsoft.PowerToys.UITest;
|
||||
using Microsoft.VisualStudio.TestPlatform.CommunicationUtilities;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using OpenQA.Selenium;
|
||||
using OpenQA.Selenium.Appium;
|
||||
|
||||
@@ -15,8 +15,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Appium.WebDriver" />
|
||||
<!-- Test libraries/utilities should not use the metapackage. -->
|
||||
<PackageReference Include="MSTest.TestFramework" />
|
||||
<PackageReference Include="MSTest" />
|
||||
<PackageReference Include="System.IO.Abstractions" />
|
||||
<PackageReference Include="System.Text.RegularExpressions" />
|
||||
<PackageReference Include="CoenM.ImageSharp.ImageHash" />
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<IsPackable>false</IsPackable>
|
||||
<OutputType>Exe</OutputType>
|
||||
<OutputType>Library</OutputType>
|
||||
<RootNamespace>Microsoft.Interop.Tests</RootNamespace>
|
||||
<AssemblyName>Microsoft.Interop.Tests</AssemblyName>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
|
||||
@@ -3,10 +3,6 @@
|
||||
<Import Project="$(RepoRoot)src\Common.Dotnet.CsWinRT.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<SelfContained>true</SelfContained>
|
||||
<RuntimeIdentifier Condition="'$(Platform)' == 'x64'">win-x64</RuntimeIdentifier>
|
||||
<RuntimeIdentifier Condition="'$(Platform)' == 'ARM64'">win-arm64</RuntimeIdentifier>
|
||||
<OutputType>Exe</OutputType>
|
||||
<IsPackable>false</IsPackable>
|
||||
<OutputPath>$(RepoRoot)$(Configuration)\$(Platform)\tests\PowerToys.DSC.Tests\</OutputPath>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -46,7 +46,6 @@
|
||||
<Message Text="Generating DSC resource JSON files to DSCModules subfolder..." Importance="high" />
|
||||
<MakeDir Directories="$(TargetDir)DSCModules" />
|
||||
|
||||
<Exec Command=""$(TargetDir)$(AssemblyName).exe" manifest --resource settings --outputDir "$(TargetDir)DSCModules"" Condition="'$(SelfContained)' == 'true'" />
|
||||
<Exec Command="dotnet "$(TargetPath)" manifest --resource settings --outputDir "$(TargetDir)DSCModules"" Condition="'$(SelfContained)' != 'true'" />
|
||||
<Exec Command="dotnet "$(TargetPath)" manifest --resource settings --outputDir "$(TargetDir)DSCModules"" />
|
||||
</Target>
|
||||
</Project>
|
||||
@@ -6,12 +6,6 @@
|
||||
<LangVersion>latest</LangVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<OutputType>Exe</OutputType>
|
||||
|
||||
<!-- exit code 8 means no tests ran. -->
|
||||
<!-- Doc: https://learn.microsoft.com/dotnet/core/testing/unit-testing-platform-exit-codes -->
|
||||
<!-- This test project doesn't seem to contain any tests. -->
|
||||
<TestingPlatformCommandLineArguments>$(TestingPlatformCommandLineArguments) --ignore-exit-code 8</TestingPlatformCommandLineArguments>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<OutputPath>$(RepoRoot)$(Platform)\$(Configuration)\tests\AdvancedPaste.FuzzTests\</OutputPath>
|
||||
|
||||
@@ -3,14 +3,11 @@
|
||||
<Import Project="$(RepoRoot)src\Common.Dotnet.CsWinRT.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<SelfContained>true</SelfContained>
|
||||
<RuntimeIdentifier Condition="'$(Platform)' == 'x64'">win-x64</RuntimeIdentifier>
|
||||
<RuntimeIdentifier Condition="'$(Platform)' == 'ARM64'">win-arm64</RuntimeIdentifier>
|
||||
<IsPackable>false</IsPackable>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\tests\AdvancedPaste.UnitTests\</OutputPath>
|
||||
<OutputType>Exe</OutputType>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -7,11 +7,6 @@
|
||||
<LangVersion>latest</LangVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<DefineConstants>TESTONLY</DefineConstants>
|
||||
|
||||
<!-- exit code 8 means no tests ran. -->
|
||||
<!-- Doc: https://learn.microsoft.com/dotnet/core/testing/unit-testing-platform-exit-codes -->
|
||||
<!-- This test project doesn't seem to contain any tests. -->
|
||||
<TestingPlatformCommandLineArguments>$(TestingPlatformCommandLineArguments) --ignore-exit-code 8</TestingPlatformCommandLineArguments>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\tests\Hosts.Tests\</OutputPath>
|
||||
<RootNamespace>Hosts.Tests</RootNamespace>
|
||||
<AssemblyName>PowerToys.Hosts.Tests</AssemblyName>
|
||||
<OutputType>Exe</OutputType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<AssemblyName>PowerToys.MouseJump.Common.UnitTests</AssemblyName>
|
||||
<AssemblyTitle>PowerToys.MouseJump.Common.UnitTests</AssemblyTitle>
|
||||
<AssemblyDescription>PowerToys MouseJump.Common.UnitTests</AssemblyDescription>
|
||||
<OutputType>Exe</OutputType>
|
||||
<OutputType>Library</OutputType>
|
||||
<OutputPath>$(RepoRoot)$(Platform)\$(Configuration)\tests\MouseJump.Common.UnitTests\</OutputPath>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
|
||||
@@ -6,13 +6,7 @@
|
||||
<PropertyGroup>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<OutputType>Exe</OutputType>
|
||||
|
||||
<!-- exit code 8 means no tests ran. -->
|
||||
<!-- Doc: https://learn.microsoft.com/dotnet/core/testing/unit-testing-platform-exit-codes -->
|
||||
<!-- This test project contains a single test but it's ignored. -->
|
||||
<!-- Remove this line if more tests are added or if the test is un-ignored -->
|
||||
<TestingPlatformCommandLineArguments>$(TestingPlatformCommandLineArguments) --ignore-exit-code 8</TestingPlatformCommandLineArguments>
|
||||
<OutputType>Library</OutputType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -2,13 +2,10 @@
|
||||
<Import Project="$(RepoRoot)src\Common.Dotnet.CsWinRT.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<SelfContained>true</SelfContained>
|
||||
<RuntimeIdentifier Condition="'$(Platform)' == 'x64'">win-x64</RuntimeIdentifier>
|
||||
<RuntimeIdentifier Condition="'$(Platform)' == 'ARM64'">win-arm64</RuntimeIdentifier>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<OutputType>Exe</OutputType>
|
||||
<OutputType>Library</OutputType>
|
||||
<RunVSTest>false</RunVSTest>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<IsPackable>false</IsPackable>
|
||||
|
||||
@@ -34,15 +34,13 @@ namespace winrt
|
||||
using namespace Windows::Devices::Enumeration;
|
||||
}
|
||||
|
||||
AudioSampleGenerator::AudioSampleGenerator(bool captureMicrophone, bool captureSystemAudio, bool micMonoMix)
|
||||
AudioSampleGenerator::AudioSampleGenerator(bool captureMicrophone, bool captureSystemAudio)
|
||||
: m_captureMicrophone(captureMicrophone)
|
||||
, m_captureSystemAudio(captureSystemAudio)
|
||||
, m_micMonoMix(micMonoMix)
|
||||
{
|
||||
OutputDebugStringA(("AudioSampleGenerator created, captureMicrophone=" +
|
||||
std::string(captureMicrophone ? "true" : "false") +
|
||||
", captureSystemAudio=" + std::string(captureSystemAudio ? "true" : "false") +
|
||||
", micMonoMix=" + std::string(micMonoMix ? "true" : "false") + "\n").c_str());
|
||||
", captureSystemAudio=" + std::string(captureSystemAudio ? "true" : "false") + "\n").c_str());
|
||||
m_audioEvent.create(wil::EventOptions::ManualReset);
|
||||
m_endEvent.create(wil::EventOptions::ManualReset);
|
||||
m_startEvent.create(wil::EventOptions::ManualReset);
|
||||
@@ -633,30 +631,6 @@ void AudioSampleGenerator::OnAudioQuantumStarted(winrt::AudioGraph const& sender
|
||||
uint32_t expectedSamplesPerQuantum = (m_graphSampleRate / 100) * m_graphChannels;
|
||||
uint32_t numMicSamples = audioBuffer.Length() / sizeof(float);
|
||||
|
||||
// Apply mono mixing to microphone audio if enabled
|
||||
// This converts stereo mic input (with same signal on both channels) to true mono
|
||||
// by averaging the channels and writing the result to both channels
|
||||
if (m_micMonoMix && m_captureMicrophone && numMicSamples > 0 && m_graphChannels >= 2)
|
||||
{
|
||||
float* micData = reinterpret_cast<float*>(sampleBuffer.data());
|
||||
uint32_t numFrames = numMicSamples / m_graphChannels;
|
||||
for (uint32_t i = 0; i < numFrames; i++)
|
||||
{
|
||||
// Sum all channels for this frame
|
||||
float sum = 0.0f;
|
||||
for (uint32_t ch = 0; ch < m_graphChannels; ch++)
|
||||
{
|
||||
sum += micData[i * m_graphChannels + ch];
|
||||
}
|
||||
// Power-preserving mix: divide by sqrt(N) to maintain perceived loudness
|
||||
float mono = sum / std::sqrt(static_cast<float>(m_graphChannels));
|
||||
for (uint32_t ch = 0; ch < m_graphChannels; ch++)
|
||||
{
|
||||
micData[i * m_graphChannels + ch] = mono;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Drain loopback samples regardless of whether we have mic audio
|
||||
if (m_loopbackCapture)
|
||||
{
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
class AudioSampleGenerator
|
||||
{
|
||||
public:
|
||||
AudioSampleGenerator(bool captureMicrophone = true, bool captureSystemAudio = true, bool micMonoMix = false);
|
||||
AudioSampleGenerator(bool captureMicrophone = true, bool captureSystemAudio = true);
|
||||
~AudioSampleGenerator();
|
||||
|
||||
winrt::Windows::Foundation::IAsyncAction InitializeAsync();
|
||||
@@ -70,5 +70,4 @@ private:
|
||||
std::atomic<bool> m_started = false;
|
||||
bool m_captureMicrophone = true;
|
||||
bool m_captureSystemAudio = true;
|
||||
bool m_micMonoMix = false;
|
||||
};
|
||||
@@ -861,7 +861,6 @@ VideoRecordingSession::VideoRecordingSession(
|
||||
uint32_t frameRate,
|
||||
bool captureAudio,
|
||||
bool captureSystemAudio,
|
||||
bool micMonoMix,
|
||||
winrt::Streams::IRandomAccessStream const& stream)
|
||||
{
|
||||
m_device = device;
|
||||
@@ -965,7 +964,7 @@ VideoRecordingSession::VideoRecordingSession(
|
||||
winrt::check_hresult(m_d3dDevice->CreateRenderTargetView(backBuffer.get(), nullptr, m_renderTargetView.put()));
|
||||
|
||||
// Always create audio generator for loopback capture; captureAudio controls microphone
|
||||
m_audioGenerator = std::make_unique<AudioSampleGenerator>(captureAudio, captureSystemAudio, micMonoMix);
|
||||
m_audioGenerator = std::make_unique<AudioSampleGenerator>(captureAudio, captureSystemAudio);
|
||||
}
|
||||
|
||||
|
||||
@@ -1113,10 +1112,9 @@ std::shared_ptr<VideoRecordingSession> VideoRecordingSession::Create(
|
||||
uint32_t frameRate,
|
||||
bool captureAudio,
|
||||
bool captureSystemAudio,
|
||||
bool micMonoMix,
|
||||
winrt::Streams::IRandomAccessStream const& stream)
|
||||
{
|
||||
return std::shared_ptr<VideoRecordingSession>(new VideoRecordingSession(device, item, crop, frameRate, captureAudio, captureSystemAudio, micMonoMix, stream));
|
||||
return std::shared_ptr<VideoRecordingSession>(new VideoRecordingSession(device, item, crop, frameRate, captureAudio, captureSystemAudio, stream));
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
|
||||
@@ -28,7 +28,6 @@ public:
|
||||
uint32_t frameRate,
|
||||
bool captureAudio,
|
||||
bool captureSystemAudio,
|
||||
bool micMonoMix,
|
||||
winrt::Streams::IRandomAccessStream const& stream);
|
||||
~VideoRecordingSession();
|
||||
|
||||
@@ -189,7 +188,6 @@ private:
|
||||
uint32_t frameRate,
|
||||
bool captureAudio,
|
||||
bool captureSystemAudio,
|
||||
bool micMonoMix,
|
||||
winrt::Streams::IRandomAccessStream const& stream);
|
||||
void CloseInternal();
|
||||
|
||||
|
||||
@@ -279,7 +279,6 @@ BEGIN
|
||||
LTEXT "To record a specific window, enter the hotkey with the Alt key in the opposite mode.",IDC_STATIC,7,55,251,19
|
||||
CONTROL "Capture &system audio",IDC_CAPTURE_SYSTEM_AUDIO,"Button",BS_AUTOCHECKBOX | BS_LEFTTEXT | WS_TABSTOP,7,149,83,10
|
||||
CONTROL "&Capture audio input:",IDC_CAPTURE_AUDIO,"Button",BS_AUTOCHECKBOX | BS_LEFTTEXT | WS_TABSTOP,7,161,83,10
|
||||
CONTROL "Mono",IDC_MIC_MONO_MIX,"Button",BS_AUTOCHECKBOX | BS_LEFTTEXT | WS_TABSTOP,98,161,30,10
|
||||
COMBOBOX IDC_MICROPHONE,81,176,152,30,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP
|
||||
LTEXT "Microphone:",IDC_MICROPHONE_LABEL,32,178,47,8
|
||||
END
|
||||
|
||||
@@ -51,7 +51,6 @@ DWORD g_RecordScalingMP4 = 100;
|
||||
RecordingFormat g_RecordingFormat = RecordingFormat::MP4;
|
||||
BOOLEAN g_CaptureSystemAudio = TRUE;
|
||||
BOOLEAN g_CaptureAudio = FALSE;
|
||||
BOOLEAN g_MicMonoMix = FALSE;
|
||||
TCHAR g_MicrophoneDeviceId[MAX_PATH] = {0};
|
||||
TCHAR g_RecordingSaveLocationBuffer[MAX_PATH] = {0};
|
||||
TCHAR g_ScreenshotSaveLocationBuffer[MAX_PATH] = {0};
|
||||
@@ -100,7 +99,6 @@ REG_SETTING RegSettings[] = {
|
||||
{ L"RecordScalingMP4", SETTING_TYPE_DWORD, 0, &g_RecordScalingMP4, static_cast<DOUBLE>(g_RecordScalingMP4) },
|
||||
{ L"CaptureAudio", SETTING_TYPE_BOOLEAN, 0, &g_CaptureAudio, static_cast<DOUBLE>(g_CaptureAudio) },
|
||||
{ L"CaptureSystemAudio", SETTING_TYPE_BOOLEAN, 0, &g_CaptureSystemAudio, static_cast<DOUBLE>(g_CaptureSystemAudio) },
|
||||
{ L"MicMonoMix", SETTING_TYPE_BOOLEAN, 0, &g_MicMonoMix, static_cast<DOUBLE>(g_MicMonoMix) },
|
||||
{ L"MicrophoneDeviceId", SETTING_TYPE_STRING, sizeof(g_MicrophoneDeviceId), g_MicrophoneDeviceId, static_cast<DOUBLE>(0) },
|
||||
{ L"RecordingSaveLocation", SETTING_TYPE_STRING, sizeof(g_RecordingSaveLocationBuffer), g_RecordingSaveLocationBuffer, static_cast<DOUBLE>(0) },
|
||||
{ L"ScreenshotSaveLocation", SETTING_TYPE_STRING, sizeof(g_ScreenshotSaveLocationBuffer), g_ScreenshotSaveLocationBuffer, static_cast<DOUBLE>(0) },
|
||||
|
||||
@@ -3840,9 +3840,6 @@ INT_PTR CALLBACK OptionsProc( HWND hDlg, UINT message,
|
||||
CheckDlgButton( g_OptionsTabs[RECORD_PAGE].hPage, IDC_CAPTURE_AUDIO,
|
||||
g_CaptureAudio ? BST_CHECKED: BST_UNCHECKED );
|
||||
|
||||
CheckDlgButton( g_OptionsTabs[RECORD_PAGE].hPage, IDC_MIC_MONO_MIX,
|
||||
g_MicMonoMix ? BST_CHECKED: BST_UNCHECKED );
|
||||
|
||||
//
|
||||
// The framerate drop down list is not used in the current version (might be added in the future)
|
||||
//
|
||||
@@ -4263,7 +4260,6 @@ INT_PTR CALLBACK OptionsProc( HWND hDlg, UINT message,
|
||||
g_ShowExpiredTime = IsDlgButtonChecked( g_OptionsTabs[BREAK_PAGE].hPage, IDC_CHECK_SHOW_EXPIRED ) == BST_CHECKED;
|
||||
g_CaptureSystemAudio = IsDlgButtonChecked(g_OptionsTabs[RECORD_PAGE].hPage, IDC_CAPTURE_SYSTEM_AUDIO) == BST_CHECKED;
|
||||
g_CaptureAudio = IsDlgButtonChecked(g_OptionsTabs[RECORD_PAGE].hPage, IDC_CAPTURE_AUDIO) == BST_CHECKED;
|
||||
g_MicMonoMix = IsDlgButtonChecked(g_OptionsTabs[RECORD_PAGE].hPage, IDC_MIC_MONO_MIX) == BST_CHECKED;
|
||||
GetDlgItemText( g_OptionsTabs[BREAK_PAGE].hPage, IDC_TIMER, text, 3 );
|
||||
text[2] = 0;
|
||||
newTimeout = _tstoi( text );
|
||||
@@ -5609,7 +5605,6 @@ winrt::fire_and_forget StartRecordingAsync( HWND hWnd, LPRECT rcCrop, HWND hWndR
|
||||
g_RecordFrameRate,
|
||||
g_CaptureAudio,
|
||||
g_CaptureSystemAudio,
|
||||
g_MicMonoMix,
|
||||
stream );
|
||||
|
||||
recordingStarted = (g_RecordingSession != nullptr);
|
||||
@@ -7296,8 +7291,7 @@ LRESULT APIENTRY MainWndProc(
|
||||
case WM_IME_CHAR:
|
||||
case WM_CHAR:
|
||||
|
||||
if( (g_TypeMode != TypeModeOff) &&
|
||||
(iswprint(static_cast<TCHAR>(wParam)) || (static_cast<TCHAR>(wParam) == L'&')) ) {
|
||||
if( (g_TypeMode != TypeModeOff) && iswprint(static_cast<TCHAR>(wParam)) || (static_cast<TCHAR>(wParam) == L'&')) {
|
||||
g_HaveTyped = TRUE;
|
||||
|
||||
TCHAR vKey = static_cast<TCHAR>(wParam);
|
||||
@@ -7405,8 +7399,9 @@ LRESULT APIENTRY MainWndProc(
|
||||
|
||||
case WM_KEYDOWN:
|
||||
|
||||
if( (g_TypeMode != TypeModeOff) && g_HaveTyped &&
|
||||
(wParam == VK_RETURN || wParam == VK_DELETE || wParam == VK_BACK) ) {
|
||||
if( (g_TypeMode != TypeModeOff) && g_HaveTyped && static_cast<char>(wParam) != VK_UP && static_cast<char>(wParam) != VK_DOWN &&
|
||||
(isprint( static_cast<char>(wParam)) ||
|
||||
wParam == VK_RETURN || wParam == VK_DELETE || wParam == VK_BACK )) {
|
||||
|
||||
if( wParam == VK_RETURN ) {
|
||||
|
||||
|
||||
@@ -111,7 +111,6 @@
|
||||
#define IDC_SMOOTH_IMAGE 1107
|
||||
#define IDC_CAPTURE_SYSTEM_AUDIO 1108
|
||||
#define IDC_MICROPHONE_LABEL 1109
|
||||
#define IDC_MIC_MONO_MIX 1110
|
||||
#define IDC_SAVE 40002
|
||||
#define IDC_COPY 40004
|
||||
#define IDC_RECORD 40006
|
||||
|
||||
@@ -26,6 +26,11 @@
|
||||
"input": "pushd .\\ExtensionTemplate\\ ; git archive -o ..\\Microsoft.CmdPal.UI.ViewModels\\Assets\\template.zip HEAD -- .\\TemplateCmdPalExtension\\ ; popd",
|
||||
"name": "Update template project",
|
||||
"description": "zips up the ExtensionTemplate into our assets. Run this in the cmdpal/ directory."
|
||||
},
|
||||
{
|
||||
"input": " .\\extensionsdk\\nuget\\BuildSDKHelper.ps1 -VersionOfSDK 0.0.1",
|
||||
"name": "Build SDK",
|
||||
"description": "Builds the SDK nuget package with the specified version."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
"path": "..\\..\\..\\PowerToys.slnx",
|
||||
"projects": [
|
||||
"src\\common\\CalculatorEngineCommon\\CalculatorEngineCommon.vcxproj",
|
||||
"src\\common\\Common.UI.Controls\\Common.UI.Controls.csproj",
|
||||
"src\\common\\ManagedCommon\\ManagedCommon.csproj",
|
||||
"src\\common\\ManagedCsWin32\\ManagedCsWin32.csproj",
|
||||
"src\\common\\ManagedTelemetry\\Telemetry\\ManagedTelemetry.csproj",
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common;
|
||||
|
||||
public static class CoreLogger
|
||||
@@ -15,6 +13,8 @@ public static class CoreLogger
|
||||
|
||||
private static ILogger? _logger;
|
||||
|
||||
public static ILogger? Instance => _logger;
|
||||
|
||||
public static void LogError(string message, Exception ex, [System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0)
|
||||
{
|
||||
_logger?.LogError(message, ex, memberName, sourceFilePath, sourceLineNumber);
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
// 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.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common.Helpers;
|
||||
|
||||
public partial class PinnedDockItem : WrappedDockItem
|
||||
{
|
||||
public override string Title => $"{base.Title} ({Properties.Resources.PinnedItemSuffix})";
|
||||
|
||||
public PinnedDockItem(ICommand command)
|
||||
: base(command, command.Name)
|
||||
{
|
||||
}
|
||||
|
||||
public PinnedDockItem(IListItem item, string id)
|
||||
: base([item], id, item.Title)
|
||||
{
|
||||
Icon = item.Icon;
|
||||
}
|
||||
}
|
||||
@@ -72,5 +72,14 @@ namespace Microsoft.CmdPal.Core.Common.Properties {
|
||||
return ResourceManager.GetString("ErrorReport_Global_Preamble", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Pinned.
|
||||
/// </summary>
|
||||
internal static string PinnedItemSuffix {
|
||||
get {
|
||||
return ResourceManager.GetString("PinnedItemSuffix", resourceCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
@@ -117,6 +117,10 @@
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="PinnedItemSuffix" xml:space="preserve">
|
||||
<value>Pinned</value>
|
||||
<comment>Suffix shown for pinned items in the dock</comment>
|
||||
</data>
|
||||
<data name="ErrorReport_Global_Preamble" xml:space="preserve">
|
||||
<value>This is an error report generated by Windows Command Palette.
|
||||
If you are seeing this, it means something went a little sideways in the app.
|
||||
@@ -124,4 +128,4 @@ You can help us fix it by filing a report at https://aka.ms/powerToysReportBug.
|
||||
|
||||
(While you’re at it, give the details below a quick skim — just to make sure there’s nothing personal you’d prefer not to share. It’s rare, but sometimes little surprises sneak in.)</value>
|
||||
</data>
|
||||
</root>
|
||||
</root>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
@@ -25,6 +25,8 @@ public abstract partial class AppExtensionHost : IExtensionHost
|
||||
|
||||
public ObservableCollection<StatusMessageViewModel> StatusMessages { get; } = [];
|
||||
|
||||
public virtual bool SupportsDockBands { get; set; }
|
||||
|
||||
public static void SetHostHwnd(ulong hostHwnd) => _hostingHwnd = hostHwnd;
|
||||
|
||||
public void DebugLog(string message)
|
||||
|
||||
@@ -96,9 +96,10 @@ public partial class CommandBarViewModel : ObservableObject,
|
||||
|
||||
SecondaryCommand = SelectedItem.SecondaryCommand;
|
||||
|
||||
ShouldShowContextMenu = SelectedItem.MoreCommands
|
||||
.OfType<CommandContextItemViewModel>()
|
||||
.Count() > 1;
|
||||
var hasMoreThanOneContextItem = SelectedItem.MoreCommands.Count() > 1;
|
||||
var hasMoreThanOneCommand = SelectedItem.MoreCommands.OfType<CommandContextItemViewModel>().Any();
|
||||
|
||||
ShouldShowContextMenu = hasMoreThanOneContextItem && hasMoreThanOneCommand;
|
||||
|
||||
OnPropertyChanged(nameof(HasSecondaryCommand));
|
||||
OnPropertyChanged(nameof(SecondaryCommand));
|
||||
|
||||
@@ -5,16 +5,15 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Models;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.ViewModels;
|
||||
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
|
||||
public partial class CommandContextItemViewModel(ICommandContextItem contextItem, WeakReference<IPageContext> context) : CommandItemViewModel(new(contextItem), context), IContextItemViewModel
|
||||
public partial class CommandContextItemViewModel : CommandItemViewModel, IContextItemViewModel
|
||||
{
|
||||
private readonly KeyChord nullKeyChord = new(0, 0, 0);
|
||||
|
||||
public new ExtensionObject<ICommandContextItem> Model { get; } = new(contextItem);
|
||||
public new ExtensionObject<ICommandContextItem> Model { get; }
|
||||
|
||||
public bool IsCritical { get; private set; }
|
||||
|
||||
@@ -22,6 +21,13 @@ public partial class CommandContextItemViewModel(ICommandContextItem contextItem
|
||||
|
||||
public bool HasRequestedShortcut => RequestedShortcut is not null && (RequestedShortcut.Value != nullKeyChord);
|
||||
|
||||
public CommandContextItemViewModel(ICommandContextItem contextItem, WeakReference<IPageContext> context)
|
||||
: base(new(contextItem), context)
|
||||
{
|
||||
Model = new(contextItem);
|
||||
IsContextMenuItem = true;
|
||||
}
|
||||
|
||||
public override void InitializeProperties()
|
||||
{
|
||||
if (IsInitialized)
|
||||
|
||||
@@ -22,6 +22,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
private ExtensionObject<IExtendedAttributesProvider>? ExtendedAttributesProvider { get; set; }
|
||||
|
||||
private readonly ExtensionObject<ICommandItem> _commandItemModel = new(null);
|
||||
private readonly IContextMenuFactory? _contextMenuFactory;
|
||||
private CommandContextItemViewModel? _defaultCommandContextItemViewModel;
|
||||
|
||||
private FuzzyTargetCache _titleCache;
|
||||
@@ -35,6 +36,8 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
|
||||
protected bool IsSelectedInitialized => IsInErrorState || Initialized.HasFlag(InitializedState.SelectionInitialized);
|
||||
|
||||
public bool IsContextMenuItem { get; protected init; }
|
||||
|
||||
public bool IsInErrorState => Initialized.HasFlag(InitializedState.Error);
|
||||
|
||||
// These are properties that are "observable" from the extension object
|
||||
@@ -47,7 +50,9 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
|
||||
private string _itemTitle = string.Empty;
|
||||
|
||||
public string Title => string.IsNullOrEmpty(_itemTitle) ? Name : _itemTitle;
|
||||
protected string ItemTitle => _itemTitle;
|
||||
|
||||
public virtual string Title => string.IsNullOrEmpty(_itemTitle) ? Name : _itemTitle;
|
||||
|
||||
public string Subtitle { get; private set; } = string.Empty;
|
||||
|
||||
@@ -69,10 +74,30 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
|
||||
public CommandItemViewModel? PrimaryCommand => this;
|
||||
|
||||
public CommandItemViewModel? SecondaryCommand => HasMoreCommands ? ActualCommands[0] : null;
|
||||
public CommandItemViewModel? SecondaryCommand
|
||||
{
|
||||
get
|
||||
{
|
||||
if (HasMoreCommands)
|
||||
{
|
||||
if (MoreCommands[0] is CommandContextItemViewModel command)
|
||||
{
|
||||
return command;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShouldBeVisible => !string.IsNullOrEmpty(Name);
|
||||
|
||||
public bool HasTitle => !string.IsNullOrEmpty(Title);
|
||||
|
||||
public bool HasSubtitle => !string.IsNullOrEmpty(Subtitle);
|
||||
|
||||
public virtual bool HasText => HasTitle || HasSubtitle;
|
||||
|
||||
public DataPackageView? DataPackage { get; private set; }
|
||||
|
||||
public List<IContextItemViewModel> AllCommands
|
||||
@@ -96,10 +121,14 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
_errorIcon.InitializeProperties();
|
||||
}
|
||||
|
||||
public CommandItemViewModel(ExtensionObject<ICommandItem> item, WeakReference<IPageContext> errorContext)
|
||||
public CommandItemViewModel(
|
||||
ExtensionObject<ICommandItem> item,
|
||||
WeakReference<IPageContext> errorContext,
|
||||
IContextMenuFactory? contextMenuFactory = null)
|
||||
: base(errorContext)
|
||||
{
|
||||
_commandItemModel = item;
|
||||
_contextMenuFactory = contextMenuFactory;
|
||||
Command = new(null, errorContext);
|
||||
}
|
||||
|
||||
@@ -197,27 +226,28 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
return;
|
||||
}
|
||||
|
||||
var more = model.MoreCommands;
|
||||
if (more is not null)
|
||||
{
|
||||
MoreCommands = more
|
||||
.Select<IContextItem, IContextItemViewModel>(item =>
|
||||
{
|
||||
return item is ICommandContextItem contextItem ? new CommandContextItemViewModel(contextItem, PageContext) : new SeparatorViewModel();
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
BuildAndInitMoreCommands();
|
||||
|
||||
// Here, we're already theoretically in the async context, so we can
|
||||
// use Initialize straight up
|
||||
MoreCommands
|
||||
.OfType<CommandContextItemViewModel>()
|
||||
.ToList()
|
||||
.ForEach(contextItem =>
|
||||
{
|
||||
contextItem.SlowInitializeProperties();
|
||||
});
|
||||
// var more = model.MoreCommands;
|
||||
// if (more is not null)
|
||||
// {
|
||||
// MoreCommands = more
|
||||
// .Select<IContextItem, IContextItemViewModel>(item =>
|
||||
// {
|
||||
// return item is ICommandContextItem contextItem ? new CommandContextItemViewModel(contextItem, PageContext) : new SeparatorViewModel();
|
||||
// })
|
||||
// .ToList();
|
||||
// }
|
||||
|
||||
// // Here, we're already theoretically in the async context, so we can
|
||||
// // use Initialize straight up
|
||||
// MoreCommands
|
||||
// .OfType<CommandContextItemViewModel>()
|
||||
// .ToList()
|
||||
// .ForEach(contextItem =>
|
||||
// {
|
||||
// contextItem.SlowInitializeProperties();
|
||||
// });
|
||||
if (!string.IsNullOrEmpty(model.Command?.Name))
|
||||
{
|
||||
_defaultCommandContextItemViewModel = new CommandContextItemViewModel(new CommandContextItem(model.Command!), PageContext)
|
||||
@@ -342,11 +372,13 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
UpdateProperty(nameof(Name));
|
||||
UpdateProperty(nameof(Title));
|
||||
UpdateProperty(nameof(Icon));
|
||||
UpdateProperty(nameof(HasText));
|
||||
break;
|
||||
|
||||
case nameof(Title):
|
||||
_itemTitle = model.Title;
|
||||
_titleCache.Invalidate();
|
||||
UpdateProperty(nameof(HasText));
|
||||
break;
|
||||
|
||||
case nameof(Subtitle):
|
||||
@@ -354,6 +386,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
this.Subtitle = modelSubtitle;
|
||||
_defaultCommandContextItemViewModel?.Subtitle = modelSubtitle;
|
||||
_subtitleCache.Invalidate();
|
||||
UpdateProperty(nameof(HasText));
|
||||
break;
|
||||
|
||||
case nameof(Icon):
|
||||
@@ -370,36 +403,36 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
break;
|
||||
|
||||
case nameof(model.MoreCommands):
|
||||
var more = model.MoreCommands;
|
||||
if (more is not null)
|
||||
{
|
||||
var newContextMenu = more
|
||||
.Select<IContextItem, IContextItemViewModel>(item =>
|
||||
{
|
||||
return item is ICommandContextItem contextItem ? new CommandContextItemViewModel(contextItem, PageContext) : new SeparatorViewModel();
|
||||
})
|
||||
.ToList();
|
||||
lock (MoreCommands)
|
||||
{
|
||||
ListHelpers.InPlaceUpdateList(MoreCommands, newContextMenu);
|
||||
}
|
||||
|
||||
newContextMenu
|
||||
.OfType<CommandContextItemViewModel>()
|
||||
.ToList()
|
||||
.ForEach(contextItem =>
|
||||
{
|
||||
contextItem.InitializeProperties();
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
lock (MoreCommands)
|
||||
{
|
||||
MoreCommands.Clear();
|
||||
}
|
||||
}
|
||||
// var more = model.MoreCommands;
|
||||
// if (more is not null)
|
||||
// {
|
||||
// var newContextMenu = more
|
||||
// .Select<IContextItem, IContextItemViewModel>(item =>
|
||||
// {
|
||||
// return item is ICommandContextItem contextItem ? new CommandContextItemViewModel(contextItem, PageContext) : new SeparatorViewModel();
|
||||
// })
|
||||
// .ToList();
|
||||
// lock (MoreCommands)
|
||||
// {
|
||||
// ListHelpers.InPlaceUpdateList(MoreCommands, newContextMenu);
|
||||
// }
|
||||
|
||||
// newContextMenu
|
||||
// .OfType<CommandContextItemViewModel>()
|
||||
// .ToList()
|
||||
// .ForEach(contextItem =>
|
||||
// {
|
||||
// contextItem.InitializeProperties();
|
||||
// });
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// lock (MoreCommands)
|
||||
// {
|
||||
// MoreCommands.Clear();
|
||||
// }
|
||||
// }
|
||||
BuildAndInitMoreCommands();
|
||||
UpdateProperty(nameof(SecondaryCommand));
|
||||
UpdateProperty(nameof(SecondaryCommandName));
|
||||
UpdateProperty(nameof(HasMoreCommands));
|
||||
@@ -441,11 +474,10 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateDefaultContextItemIcon()
|
||||
{
|
||||
private void UpdateDefaultContextItemIcon() =>
|
||||
|
||||
// Command icon takes precedence over our icon on the primary command
|
||||
_defaultCommandContextItemViewModel?.UpdateIcon(Command.Icon.IsSet ? Command.Icon : _icon);
|
||||
}
|
||||
|
||||
private void UpdateTitle(string? title)
|
||||
{
|
||||
@@ -510,6 +542,35 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
base.SafeCleanup();
|
||||
Initialized |= InitializedState.CleanedUp;
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// * Does call SlowInitializeProperties on the created items.
|
||||
/// * does NOT call UpdateProperty ; caller must do that.
|
||||
/// </remarks>
|
||||
private void BuildAndInitMoreCommands()
|
||||
{
|
||||
var model = _commandItemModel.Unsafe;
|
||||
if (model is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var more = model.MoreCommands;
|
||||
var factory = _contextMenuFactory ?? DefaultContextMenuFactory.Instance;
|
||||
var results = factory.UnsafeBuildAndInitMoreCommands(more, this);
|
||||
|
||||
// var oldMoreCommands = MoreCommands;
|
||||
// MoreCommands = results;
|
||||
List<IContextItemViewModel>? freedItems;
|
||||
lock (MoreCommands)
|
||||
{
|
||||
ListHelpers.InPlaceUpdateList(MoreCommands, results, out freedItems);
|
||||
}
|
||||
|
||||
freedItems.OfType<CommandContextItemViewModel>()
|
||||
.ToList()
|
||||
.ForEach(c => c.SafeCleanup());
|
||||
}
|
||||
}
|
||||
|
||||
[Flags]
|
||||
|
||||
@@ -59,11 +59,8 @@ public partial class ContextMenuViewModel : ObservableObject,
|
||||
{
|
||||
if (SelectedItem is not null)
|
||||
{
|
||||
if (SelectedItem.PrimaryCommand is not null || SelectedItem.HasMoreCommands)
|
||||
{
|
||||
ContextMenuStack.Clear();
|
||||
PushContextStack(SelectedItem.AllCommands);
|
||||
}
|
||||
ContextMenuStack.Clear();
|
||||
PushContextStack(SelectedItem.AllCommands);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
// 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;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.ViewModels;
|
||||
|
||||
public partial class DefaultContextMenuFactory : IContextMenuFactory
|
||||
{
|
||||
public static readonly DefaultContextMenuFactory Instance = new();
|
||||
|
||||
private DefaultContextMenuFactory()
|
||||
{
|
||||
}
|
||||
|
||||
public List<IContextItemViewModel> UnsafeBuildAndInitMoreCommands(
|
||||
IContextItem[] items,
|
||||
CommandItemViewModel commandItem)
|
||||
{
|
||||
List<IContextItemViewModel> results = [];
|
||||
if (items is null)
|
||||
{
|
||||
return results;
|
||||
}
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (item is ICommandContextItem contextItem)
|
||||
{
|
||||
var contextItemViewModel = new CommandContextItemViewModel(contextItem, commandItem.PageContext);
|
||||
contextItemViewModel.SlowInitializeProperties();
|
||||
results.Add(contextItemViewModel);
|
||||
}
|
||||
else
|
||||
{
|
||||
results.Add(new SeparatorViewModel());
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
@@ -59,7 +59,7 @@ public abstract partial class ExtensionObjectViewModel : ObservableObject, IBatc
|
||||
LogIfDefaultScheduler();
|
||||
}
|
||||
|
||||
private protected ExtensionObjectViewModel(WeakReference<IPageContext> contextRef)
|
||||
protected ExtensionObjectViewModel(WeakReference<IPageContext> contextRef)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(contextRef);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
@@ -8,6 +8,8 @@ public class GlobalLogPageContext : IPageContext
|
||||
{
|
||||
public TaskScheduler Scheduler { get; private init; }
|
||||
|
||||
bool IPageContext.ExtensionSupportsPinning => false;
|
||||
|
||||
public void ShowException(Exception ex, string? extensionHint)
|
||||
{ /*do nothing*/
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.ViewModels;
|
||||
|
||||
public interface IContextMenuFactory
|
||||
{
|
||||
List<IContextItemViewModel> UnsafeBuildAndInitMoreCommands(IContextItem[] items, CommandItemViewModel commandItem);
|
||||
}
|
||||
@@ -63,8 +63,8 @@ public partial class ListItemViewModel : CommandItemViewModel
|
||||
}
|
||||
}
|
||||
|
||||
public ListItemViewModel(IListItem model, WeakReference<IPageContext> context)
|
||||
: base(new(model), context)
|
||||
public ListItemViewModel(IListItem model, WeakReference<IPageContext> context, IContextMenuFactory? contextMenuFactory)
|
||||
: base(new(model), context, contextMenuFactory)
|
||||
{
|
||||
Model = new ExtensionObject<IListItem>(model);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
{
|
||||
// private readonly HashSet<ListItemViewModel> _itemCache = [];
|
||||
private readonly TaskFactory filterTaskFactory = new(new ConcurrentExclusiveSchedulerPair().ExclusiveScheduler);
|
||||
private readonly IContextMenuFactory? _contextMenuFactory;
|
||||
|
||||
// TODO: Do we want a base "ItemsPageViewModel" for anything that's going to have items?
|
||||
|
||||
@@ -89,10 +90,11 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
public ListViewModel(IListPage model, TaskScheduler scheduler, AppExtensionHost host)
|
||||
public ListViewModel(IListPage model, TaskScheduler scheduler, AppExtensionHost host, IContextMenuFactory? contextMenuFactory)
|
||||
: base(model, scheduler, host)
|
||||
{
|
||||
_model = new(model);
|
||||
_contextMenuFactory = contextMenuFactory;
|
||||
EmptyContent = new(new(null), PageContext);
|
||||
}
|
||||
|
||||
@@ -233,7 +235,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
return;
|
||||
}
|
||||
|
||||
ListItemViewModel viewModel = new(item, new(this));
|
||||
ListItemViewModel viewModel = new(item, new(this), _contextMenuFactory);
|
||||
|
||||
// If an item fails to load, silently ignore it.
|
||||
if (viewModel.SafeFastInit())
|
||||
|
||||
@@ -12,6 +12,7 @@ public partial class LoadingPageViewModel : PageViewModel
|
||||
: base(model, scheduler, host)
|
||||
{
|
||||
ModelIsLoading = true;
|
||||
HasBackButton = false;
|
||||
IsInitialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,4 +4,4 @@
|
||||
|
||||
namespace Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
|
||||
public record NavigateToPageMessage(PageViewModel Page, bool WithAnimation, CancellationToken CancellationToken);
|
||||
public record NavigateToPageMessage(PageViewModel Page, bool WithAnimation, CancellationToken CancellationToken, bool TransientPage = false);
|
||||
|
||||
@@ -18,6 +18,8 @@ public record PerformCommandMessage
|
||||
|
||||
public bool WithAnimation { get; set; } = true;
|
||||
|
||||
public bool TransientPage { get; set; }
|
||||
|
||||
public PerformCommandMessage(ExtensionObject<ICommand> command)
|
||||
{
|
||||
Command = command;
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
|
||||
public sealed record WindowHiddenMessage();
|
||||
@@ -27,7 +27,10 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext
|
||||
public partial string ErrorMessage { get; protected set; } = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsNested { get; set; } = true;
|
||||
public partial bool IsRootPage { get; set; } = true;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool HasBackButton { get; set; } = true;
|
||||
|
||||
// This is set from the SearchBar
|
||||
[ObservableProperty]
|
||||
@@ -76,6 +79,8 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext
|
||||
|
||||
public IconInfoViewModel Icon { get; protected set; }
|
||||
|
||||
bool IPageContext.ExtensionSupportsPinning => ExtensionHost.SupportsDockBands;
|
||||
|
||||
public PageViewModel(IPage? model, TaskScheduler scheduler, AppExtensionHost extensionHost)
|
||||
: base(scheduler)
|
||||
{
|
||||
@@ -264,6 +269,8 @@ public interface IPageContext
|
||||
void ShowException(Exception ex, string? extensionHint = null);
|
||||
|
||||
TaskScheduler Scheduler { get; }
|
||||
|
||||
bool ExtensionSupportsPinning { get; }
|
||||
}
|
||||
|
||||
public interface IPageViewModelFactoryService
|
||||
|
||||
@@ -121,4 +121,8 @@
|
||||
<value>Show details</value>
|
||||
<comment>Name for the command that shows details of an item</comment>
|
||||
</data>
|
||||
<data name="PinnedItemSuffix" xml:space="preserve">
|
||||
<value>Pinned</value>
|
||||
<comment>Suffix shown for pinned items in the dock</comment>
|
||||
</data>
|
||||
</root>
|
||||
@@ -16,7 +16,8 @@ namespace Microsoft.CmdPal.Core.ViewModels;
|
||||
public partial class ShellViewModel : ObservableObject,
|
||||
IDisposable,
|
||||
IRecipient<PerformCommandMessage>,
|
||||
IRecipient<HandleCommandResultMessage>
|
||||
IRecipient<HandleCommandResultMessage>,
|
||||
IRecipient<WindowHiddenMessage>
|
||||
{
|
||||
private readonly IRootPageService _rootPageService;
|
||||
private readonly IAppHostService _appHostService;
|
||||
@@ -79,8 +80,9 @@ public partial class ShellViewModel : ObservableObject,
|
||||
private IPage? _rootPage;
|
||||
|
||||
private bool _isNested;
|
||||
private bool _currentlyTransient;
|
||||
|
||||
public bool IsNested => _isNested;
|
||||
public bool IsNested => _isNested && !_currentlyTransient;
|
||||
|
||||
public PageViewModel NullPage { get; private set; }
|
||||
|
||||
@@ -96,11 +98,13 @@ public partial class ShellViewModel : ObservableObject,
|
||||
_appHostService = appHostService;
|
||||
|
||||
NullPage = new NullPageViewModel(_scheduler, appHostService.GetDefaultHost());
|
||||
NullPage.HasBackButton = false;
|
||||
_currentPage = new LoadingPageViewModel(null, _scheduler, appHostService.GetDefaultHost());
|
||||
|
||||
// Register to receive messages
|
||||
WeakReferenceMessenger.Default.Register<PerformCommandMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<HandleCommandResultMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<WindowHiddenMessage>(this);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
@@ -259,7 +263,7 @@ public partial class ShellViewModel : ObservableObject,
|
||||
|
||||
var host = _appHostService.GetHostForCommand(message.Context, CurrentPage.ExtensionHost);
|
||||
|
||||
_rootPageService.OnPerformCommand(message.Context, !CurrentPage.IsNested, host);
|
||||
_rootPageService.OnPerformCommand(message.Context, CurrentPage.IsRootPage, host);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -269,6 +273,7 @@ public partial class ShellViewModel : ObservableObject,
|
||||
|
||||
var isMainPage = command == _rootPage;
|
||||
_isNested = !isMainPage;
|
||||
_currentlyTransient = message.TransientPage;
|
||||
|
||||
// Telemetry: Track extension page navigation for session metrics
|
||||
if (host is not null)
|
||||
@@ -288,6 +293,9 @@ public partial class ShellViewModel : ObservableObject,
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
pageViewModel.IsRootPage = isMainPage;
|
||||
pageViewModel.HasBackButton = IsNested;
|
||||
|
||||
// Clear command bar, ViewModel initialization can already set new commands if it wants to
|
||||
OnUIThread(() => WeakReferenceMessenger.Default.Send<UpdateCommandBarMessage>(new(null)));
|
||||
|
||||
@@ -307,7 +315,8 @@ public partial class ShellViewModel : ObservableObject,
|
||||
_scheduler);
|
||||
|
||||
// While we're loading in the background, immediately move to the next page.
|
||||
WeakReferenceMessenger.Default.Send<NavigateToPageMessage>(new(pageViewModel, message.WithAnimation, navigationToken));
|
||||
NavigateToPageMessage msg = new(pageViewModel, message.WithAnimation, navigationToken, message.TransientPage);
|
||||
WeakReferenceMessenger.Default.Send(msg);
|
||||
|
||||
// Note: Originally we set our page back in the ViewModel here, but that now happens in response to the Frame navigating triggered from the above
|
||||
// See RootFrame_Navigated event handler.
|
||||
@@ -478,6 +487,19 @@ public partial class ShellViewModel : ObservableObject,
|
||||
UnsafeHandleCommandResult(message.Result.Unsafe);
|
||||
}
|
||||
|
||||
public void Receive(WindowHiddenMessage message)
|
||||
{
|
||||
// If the window was hidden while we had a transient page, we need to reset that state.
|
||||
if (_currentlyTransient)
|
||||
{
|
||||
_currentlyTransient = false;
|
||||
|
||||
// navigate back to the main page without animation
|
||||
GoHome(withAnimation: false, focusSearch: false);
|
||||
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(new ExtensionObject<ICommand>(_rootPage)));
|
||||
}
|
||||
}
|
||||
|
||||
private void OnUIThread(Action action)
|
||||
{
|
||||
_ = Task.Factory.StartNew(
|
||||
|
||||
@@ -18,9 +18,9 @@ namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDisposable
|
||||
{
|
||||
private static readonly Color DefaultTintColor = Color.FromArgb(255, 0, 120, 212);
|
||||
internal static readonly Color DefaultTintColor = Color.FromArgb(255, 0, 120, 212);
|
||||
|
||||
private static readonly ObservableCollection<Color> WindowsColorSwatches = [
|
||||
internal static readonly ObservableCollection<Color> WindowsColorSwatches = [
|
||||
|
||||
// row 0
|
||||
Color.FromArgb(255, 255, 185, 0), // #ffb900
|
||||
|
||||
@@ -11,17 +11,19 @@ public class CommandPalettePageViewModelFactory
|
||||
: IPageViewModelFactoryService
|
||||
{
|
||||
private readonly TaskScheduler _scheduler;
|
||||
private readonly IContextMenuFactory _contextMenuFactory;
|
||||
|
||||
public CommandPalettePageViewModelFactory(TaskScheduler scheduler)
|
||||
public CommandPalettePageViewModelFactory(TaskScheduler scheduler, IContextMenuFactory contextMenuFactory)
|
||||
{
|
||||
_scheduler = scheduler;
|
||||
_contextMenuFactory = contextMenuFactory;
|
||||
}
|
||||
|
||||
public PageViewModel? TryCreatePageViewModel(IPage page, bool nested, AppExtensionHost host)
|
||||
{
|
||||
return page switch
|
||||
{
|
||||
IListPage listPage => new ListViewModel(listPage, _scheduler, host) { IsNested = nested },
|
||||
IListPage listPage => new ListViewModel(listPage, _scheduler, host, _contextMenuFactory) { IsRootPage = !nested },
|
||||
IContentPage contentPage => new CommandPaletteContentPageViewModel(contentPage, _scheduler, host),
|
||||
_ => null,
|
||||
};
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.Core.Common;
|
||||
using Microsoft.CmdPal.Core.Common.Services;
|
||||
using Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Models;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
using Windows.Foundation;
|
||||
@@ -30,6 +32,8 @@ public sealed class CommandProviderWrapper
|
||||
|
||||
public TopLevelViewModel[] FallbackItems { get; private set; } = [];
|
||||
|
||||
public TopLevelViewModel[] DockBandItems { get; private set; } = [];
|
||||
|
||||
public string DisplayName { get; private set; } = string.Empty;
|
||||
|
||||
public IExtensionWrapper? Extension { get; }
|
||||
@@ -141,23 +145,38 @@ public sealed class CommandProviderWrapper
|
||||
return;
|
||||
}
|
||||
|
||||
ICommandItem[]? commands = null;
|
||||
IFallbackCommandItem[]? fallbacks = null;
|
||||
ICommandItem[] dockBands = []; // do not initialize me to null
|
||||
var displayInfoInitialized = false;
|
||||
|
||||
try
|
||||
{
|
||||
var model = _commandProvider.Unsafe!;
|
||||
|
||||
Task<ICommandItem[]> loadTopLevelCommandsTask = new(model.TopLevelCommands);
|
||||
loadTopLevelCommandsTask.Start();
|
||||
var commands = await loadTopLevelCommandsTask.ConfigureAwait(false);
|
||||
commands = await loadTopLevelCommandsTask.ConfigureAwait(false);
|
||||
|
||||
// On a BG thread here
|
||||
var fallbacks = model.FallbackCommands();
|
||||
fallbacks = model.FallbackCommands();
|
||||
|
||||
if (model is ICommandProvider2 two)
|
||||
{
|
||||
UnsafePreCacheApiAdditions(two);
|
||||
}
|
||||
|
||||
if (model is ICommandProvider3 supportsDockBands)
|
||||
{
|
||||
ExtensionHost.SupportsDockBands = true;
|
||||
var bands = supportsDockBands.GetDockBands();
|
||||
if (bands is not null)
|
||||
{
|
||||
CoreLogger.LogDebug($"Found {bands.Length} bands on {DisplayName} ({ProviderId}) ");
|
||||
dockBands = bands;
|
||||
}
|
||||
}
|
||||
|
||||
Id = model.Id;
|
||||
DisplayName = model.DisplayName;
|
||||
Icon = new(model.Icon);
|
||||
@@ -175,7 +194,8 @@ public sealed class CommandProviderWrapper
|
||||
Settings = new(model.Settings, this, _taskScheduler);
|
||||
|
||||
// We do need to explicitly initialize commands though
|
||||
InitializeCommands(commands, fallbacks, serviceProvider, pageContext);
|
||||
var objects = new TopLevelObjects(commands, fallbacks, dockBands);
|
||||
InitializeCommands(objects, serviceProvider, pageContext);
|
||||
|
||||
Logger.LogDebug($"Loaded commands from {DisplayName} ({ProviderId})");
|
||||
}
|
||||
@@ -206,33 +226,77 @@ public sealed class CommandProviderWrapper
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeCommands(ICommandItem[] commands, IFallbackCommandItem[] fallbacks, IServiceProvider serviceProvider, WeakReference<IPageContext> pageContext)
|
||||
private record TopLevelObjects(
|
||||
ICommandItem[]? Commands,
|
||||
IFallbackCommandItem[]? Fallbacks,
|
||||
ICommandItem[]? DockBands);
|
||||
|
||||
private void InitializeCommands(
|
||||
TopLevelObjects objects,
|
||||
IServiceProvider serviceProvider,
|
||||
WeakReference<IPageContext> pageContext)
|
||||
{
|
||||
var settings = serviceProvider.GetService<SettingsModel>()!;
|
||||
var state = serviceProvider.GetService<AppStateModel>()!;
|
||||
var providerSettings = GetProviderSettings(settings);
|
||||
|
||||
var makeAndAdd = (ICommandItem? i, bool fallback) =>
|
||||
Func<ICommandItem?, TopLevelType, TopLevelViewModel> make = (ICommandItem? i, TopLevelType t) =>
|
||||
{
|
||||
CommandItemViewModel commandItemViewModel = new(new(i), pageContext);
|
||||
TopLevelViewModel topLevelViewModel = new(commandItemViewModel, fallback, ExtensionHost, ProviderId, settings, providerSettings, serviceProvider, i);
|
||||
TopLevelViewModel topLevelViewModel = new(
|
||||
item: commandItemViewModel,
|
||||
topLevelType: t,
|
||||
extensionHost: ExtensionHost,
|
||||
commandProviderId: ProviderId,
|
||||
settings: settings,
|
||||
providerSettings: providerSettings,
|
||||
serviceProvider: serviceProvider,
|
||||
commandItem: i/*,
|
||||
providerSupportsPinning: SupportsDockBands*/);
|
||||
topLevelViewModel.InitializeProperties();
|
||||
|
||||
return topLevelViewModel;
|
||||
};
|
||||
|
||||
if (commands is not null)
|
||||
if (objects.Commands is not null)
|
||||
{
|
||||
TopLevelItems = commands
|
||||
.Select(c => makeAndAdd(c, false))
|
||||
TopLevelItems = objects.Commands
|
||||
.Select(c => make(c, TopLevelType.Normal))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
if (fallbacks is not null)
|
||||
if (objects.Fallbacks is not null)
|
||||
{
|
||||
FallbackItems = fallbacks
|
||||
.Select(c => makeAndAdd(c, true))
|
||||
FallbackItems = objects.Fallbacks
|
||||
.Select(c => make(c, TopLevelType.Fallback))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
if (objects.DockBands is not null)
|
||||
{
|
||||
List<TopLevelViewModel> bands = new();
|
||||
foreach (var b in objects.DockBands)
|
||||
{
|
||||
var bandVm = make(b, TopLevelType.DockBand);
|
||||
bands.Add(bandVm);
|
||||
}
|
||||
|
||||
foreach (var c in TopLevelItems)
|
||||
{
|
||||
foreach (var pinnedId in settings.DockSettings.PinnedCommands)
|
||||
{
|
||||
if (pinnedId == c.Id)
|
||||
{
|
||||
var bandModel = c.ToPinnedDockBandItem();
|
||||
var bandVm = make(bandModel, TopLevelType.DockBand);
|
||||
bands.Add(bandVm);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DockBandItems = bands.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
private void UnsafePreCacheApiAdditions(ICommandProvider2 provider)
|
||||
@@ -245,6 +309,10 @@ public sealed class CommandProviderWrapper
|
||||
{
|
||||
Logger.LogDebug($"{ProviderId}: Found an IExtendedAttributesProvider");
|
||||
}
|
||||
else if (a is ICommandItem[] commands)
|
||||
{
|
||||
Logger.LogDebug($"{ProviderId}: Found an ICommandItem[]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,4 +329,14 @@ public sealed class CommandProviderWrapper
|
||||
// In handling this, a call will be made to `LoadTopLevelCommands` to
|
||||
// retrieve the new items.
|
||||
this.CommandsChanged?.Invoke(this, args);
|
||||
|
||||
internal void PinDockBand(TopLevelViewModel bandVm)
|
||||
{
|
||||
Logger.LogDebug($"CommandProviderWrapper.PinDockBand: {ProviderId} - {bandVm.Id}");
|
||||
|
||||
var bands = this.DockBandItems.ToList();
|
||||
bands.Add(bandVm);
|
||||
this.DockBandItems = bands.ToArray();
|
||||
this.CommandsChanged?.Invoke(this, new ItemsChangedEventArgs());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CmdPal.Core.Common.Helpers;
|
||||
using Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Windows.Foundation.Collections;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.BuiltinCommands;
|
||||
|
||||
@@ -18,6 +21,8 @@ public sealed partial class BuiltInsCommandProvider : CommandProvider
|
||||
private readonly FallbackLogItem _fallbackLogItem = new();
|
||||
private readonly NewExtensionPage _newExtension = new();
|
||||
|
||||
private readonly IRootPageService _rootPageService;
|
||||
|
||||
public override ICommandItem[] TopLevelCommands() =>
|
||||
[
|
||||
new CommandItem(openSettings) { },
|
||||
@@ -37,11 +42,22 @@ public sealed partial class BuiltInsCommandProvider : CommandProvider
|
||||
_fallbackLogItem,
|
||||
];
|
||||
|
||||
public BuiltInsCommandProvider()
|
||||
public BuiltInsCommandProvider(IRootPageService rootPageService)
|
||||
{
|
||||
Id = "com.microsoft.cmdpal.builtin.core";
|
||||
DisplayName = Properties.Resources.builtin_display_name;
|
||||
Icon = IconHelpers.FromRelativePath("Assets\\StoreLogo.scale-200.png");
|
||||
Icon = IconHelpers.FromRelativePath("Assets\\Square44x44Logo.altform-unplated_targetsize-256.png");
|
||||
|
||||
_rootPageService = rootPageService;
|
||||
}
|
||||
|
||||
public override ICommandItem[]? GetDockBands()
|
||||
{
|
||||
var rootPage = _rootPageService.GetRootPage();
|
||||
List<ICommandItem> bandItems = new();
|
||||
bandItems.Add(new WrappedDockItem(rootPage, Properties.Resources.builtin_command_palette_title));
|
||||
|
||||
return bandItems.ToArray();
|
||||
}
|
||||
|
||||
public override void InitializeWithHost(IExtensionHost host) => BuiltinsExtensionHost.Instance.Initialize(host);
|
||||
|
||||
@@ -61,6 +61,7 @@ public sealed partial class MainListPage : DynamicListPage,
|
||||
AppStateModel appStateModel,
|
||||
IFuzzyMatcherProvider fuzzyMatcherProvider)
|
||||
{
|
||||
Id = "com.microsoft.cmdpal.home";
|
||||
Title = Resources.builtin_home_name;
|
||||
Icon = IconHelpers.FromRelativePath("Assets\\StoreLogo.scale-200.png");
|
||||
PlaceholderText = Properties.Resources.builtin_main_list_page_searchbar_placeholder;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
@@ -70,6 +70,15 @@ public partial class ContentFormViewModel(IFormContent _form, WeakReference<IPag
|
||||
StateJson = model.StateJson;
|
||||
DataJson = model.DataJson;
|
||||
|
||||
RenderCard();
|
||||
|
||||
UpdateProperty(nameof(Card));
|
||||
|
||||
model.PropChanged += Model_PropChanged;
|
||||
}
|
||||
|
||||
private void RenderCard()
|
||||
{
|
||||
if (TryBuildCard(TemplateJson, DataJson, out var builtCard, out var renderingError))
|
||||
{
|
||||
Card = builtCard;
|
||||
@@ -93,8 +102,41 @@ public partial class ContentFormViewModel(IFormContent _form, WeakReference<IPag
|
||||
UpdateProperty(nameof(Card));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
UpdateProperty(nameof(Card));
|
||||
private void Model_PropChanged(object sender, IPropChangedEventArgs args)
|
||||
{
|
||||
try
|
||||
{
|
||||
FetchProperty(args.PropertyName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ShowException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void FetchProperty(string propertyName)
|
||||
{
|
||||
var model = this._formModel.Unsafe;
|
||||
if (model is null)
|
||||
{
|
||||
return; // throw?
|
||||
}
|
||||
|
||||
switch (propertyName)
|
||||
{
|
||||
case nameof(DataJson):
|
||||
DataJson = model.DataJson;
|
||||
RenderCard();
|
||||
break;
|
||||
case nameof(TemplateJson):
|
||||
TemplateJson = model.TemplateJson;
|
||||
RenderCard();
|
||||
break;
|
||||
}
|
||||
|
||||
UpdateProperty(propertyName);
|
||||
}
|
||||
|
||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(AdaptiveOpenUrlAction))]
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
// 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.Globalization;
|
||||
using System.Text;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Dock;
|
||||
#pragma warning disable SA1402 // File may only contain a single type
|
||||
|
||||
public partial class DockBandSettingsViewModel : ObservableObject
|
||||
{
|
||||
private static readonly CompositeFormat PluralItemsFormatString = CompositeFormat.Parse(Properties.Resources.dock_item_count_plural);
|
||||
private readonly SettingsModel _settingsModel;
|
||||
private readonly DockBandSettings _dockSettingsModel;
|
||||
private readonly TopLevelViewModel _adapter;
|
||||
private readonly DockBandViewModel? _bandViewModel;
|
||||
|
||||
public string Title => _adapter.Title;
|
||||
|
||||
public string Description
|
||||
{
|
||||
get
|
||||
{
|
||||
List<string> parts = [_adapter.ExtensionName];
|
||||
|
||||
// Add the number of items in the band
|
||||
var itemCount = NumItemsInBand();
|
||||
if (itemCount > 0)
|
||||
{
|
||||
var itemsString = itemCount == 1 ?
|
||||
Properties.Resources.dock_item_count_singular :
|
||||
string.Format(CultureInfo.CurrentCulture, PluralItemsFormatString, itemCount);
|
||||
parts.Add(itemsString);
|
||||
}
|
||||
|
||||
return string.Join(" - ", parts);
|
||||
}
|
||||
}
|
||||
|
||||
public string ProviderId => _adapter.CommandProviderId;
|
||||
|
||||
public IconInfoViewModel Icon => _adapter.IconViewModel;
|
||||
|
||||
private ShowLabelsOption _showLabels;
|
||||
|
||||
public ShowLabelsOption ShowLabels
|
||||
{
|
||||
get => _showLabels;
|
||||
set
|
||||
{
|
||||
if (value != _showLabels)
|
||||
{
|
||||
_showLabels = value;
|
||||
_dockSettingsModel.ShowLabels = value switch
|
||||
{
|
||||
ShowLabelsOption.Default => null,
|
||||
ShowLabelsOption.ShowLabels => true,
|
||||
ShowLabelsOption.HideLabels => false,
|
||||
_ => null,
|
||||
};
|
||||
Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ShowLabelsOption FetchShowLabels()
|
||||
{
|
||||
if (_dockSettingsModel.ShowLabels == null)
|
||||
{
|
||||
return ShowLabelsOption.Default;
|
||||
}
|
||||
|
||||
return _dockSettingsModel.ShowLabels.Value ? ShowLabelsOption.ShowLabels : ShowLabelsOption.HideLabels;
|
||||
}
|
||||
|
||||
// used to map to ComboBox selection
|
||||
public int ShowLabelsIndex
|
||||
{
|
||||
get => (int)ShowLabels;
|
||||
set => ShowLabels = (ShowLabelsOption)value;
|
||||
}
|
||||
|
||||
private DockPinSide PinSide
|
||||
{
|
||||
get => _pinSide;
|
||||
set
|
||||
{
|
||||
if (value != _pinSide)
|
||||
{
|
||||
UpdatePinSide(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private DockPinSide _pinSide;
|
||||
|
||||
public int PinSideIndex
|
||||
{
|
||||
get => (int)PinSide;
|
||||
set => PinSide = (DockPinSide)value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the band is pinned to the dock.
|
||||
/// When enabled, pins to Center. When disabled, removes from all sides.
|
||||
/// </summary>
|
||||
public bool IsPinned
|
||||
{
|
||||
get => PinSide != DockPinSide.None;
|
||||
set
|
||||
{
|
||||
if (value && PinSide == DockPinSide.None)
|
||||
{
|
||||
// Pin to Center by default when enabling
|
||||
PinSide = DockPinSide.Center;
|
||||
}
|
||||
else if (!value && PinSide != DockPinSide.None)
|
||||
{
|
||||
// Remove from dock when disabling
|
||||
PinSide = DockPinSide.None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public DockBandSettingsViewModel(
|
||||
DockBandSettings dockSettingsModel,
|
||||
TopLevelViewModel topLevelAdapter,
|
||||
DockBandViewModel? bandViewModel,
|
||||
SettingsModel settingsModel)
|
||||
{
|
||||
_dockSettingsModel = dockSettingsModel;
|
||||
_adapter = topLevelAdapter;
|
||||
_bandViewModel = bandViewModel;
|
||||
_settingsModel = settingsModel;
|
||||
_pinSide = FetchPinSide();
|
||||
_showLabels = FetchShowLabels();
|
||||
}
|
||||
|
||||
private DockPinSide FetchPinSide()
|
||||
{
|
||||
var dockSettings = _settingsModel.DockSettings;
|
||||
var inStart = dockSettings.StartBands.Any(b => b.Id == _dockSettingsModel.Id);
|
||||
if (inStart)
|
||||
{
|
||||
return DockPinSide.Start;
|
||||
}
|
||||
|
||||
var inCenter = dockSettings.CenterBands.Any(b => b.Id == _dockSettingsModel.Id);
|
||||
if (inCenter)
|
||||
{
|
||||
return DockPinSide.Center;
|
||||
}
|
||||
|
||||
var inEnd = dockSettings.EndBands.Any(b => b.Id == _dockSettingsModel.Id);
|
||||
if (inEnd)
|
||||
{
|
||||
return DockPinSide.End;
|
||||
}
|
||||
|
||||
return DockPinSide.None;
|
||||
}
|
||||
|
||||
private int NumItemsInBand()
|
||||
{
|
||||
var bandVm = _bandViewModel;
|
||||
if (bandVm is null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return _bandViewModel!.Items.Count;
|
||||
}
|
||||
|
||||
private void Save()
|
||||
{
|
||||
SettingsModel.SaveSettings(_settingsModel);
|
||||
}
|
||||
|
||||
private void UpdatePinSide(DockPinSide value)
|
||||
{
|
||||
OnPinSideChanged(value);
|
||||
OnPropertyChanged(nameof(PinSideIndex));
|
||||
OnPropertyChanged(nameof(PinSide));
|
||||
OnPropertyChanged(nameof(IsPinned));
|
||||
}
|
||||
|
||||
public void SetBandPosition(DockPinSide side, int? index)
|
||||
{
|
||||
var dockSettings = _settingsModel.DockSettings;
|
||||
|
||||
// Remove from all sides first
|
||||
dockSettings.StartBands.RemoveAll(b => b.Id == _dockSettingsModel.Id);
|
||||
dockSettings.CenterBands.RemoveAll(b => b.Id == _dockSettingsModel.Id);
|
||||
dockSettings.EndBands.RemoveAll(b => b.Id == _dockSettingsModel.Id);
|
||||
|
||||
// Add to the selected side
|
||||
switch (side)
|
||||
{
|
||||
case DockPinSide.Start:
|
||||
{
|
||||
var insertIndex = index ?? dockSettings.StartBands.Count;
|
||||
dockSettings.StartBands.Insert(insertIndex, _dockSettingsModel);
|
||||
break;
|
||||
}
|
||||
|
||||
case DockPinSide.Center:
|
||||
{
|
||||
var insertIndex = index ?? dockSettings.CenterBands.Count;
|
||||
dockSettings.CenterBands.Insert(insertIndex, _dockSettingsModel);
|
||||
break;
|
||||
}
|
||||
|
||||
case DockPinSide.End:
|
||||
{
|
||||
var insertIndex = index ?? dockSettings.EndBands.Count;
|
||||
dockSettings.EndBands.Insert(insertIndex, _dockSettingsModel);
|
||||
break;
|
||||
}
|
||||
|
||||
case DockPinSide.None:
|
||||
default:
|
||||
// Do nothing
|
||||
break;
|
||||
}
|
||||
|
||||
Save();
|
||||
}
|
||||
|
||||
private void OnPinSideChanged(DockPinSide value)
|
||||
{
|
||||
SetBandPosition(value, null);
|
||||
_pinSide = value;
|
||||
}
|
||||
}
|
||||
|
||||
public enum DockPinSide
|
||||
{
|
||||
None,
|
||||
Start,
|
||||
Center,
|
||||
End,
|
||||
}
|
||||
|
||||
public enum ShowLabelsOption
|
||||
{
|
||||
Default,
|
||||
ShowLabels,
|
||||
HideLabels,
|
||||
}
|
||||
|
||||
#pragma warning restore SA1402 // File may only contain a single type
|
||||
@@ -0,0 +1,274 @@
|
||||
// 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.CmdPal.Core.ViewModels;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Models;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Dock;
|
||||
|
||||
#pragma warning disable SA1402 // File may only contain a single type
|
||||
|
||||
public sealed partial class DockBandViewModel : ExtensionObjectViewModel
|
||||
{
|
||||
private readonly CommandItemViewModel _rootItem;
|
||||
private readonly DockBandSettings _bandSettings;
|
||||
private readonly DockSettings _dockSettings;
|
||||
private readonly Action _saveSettings;
|
||||
|
||||
public ObservableCollection<DockItemViewModel> Items { get; } = new();
|
||||
|
||||
private bool _showTitles = true;
|
||||
private bool _showSubtitles = true;
|
||||
private bool? _showTitlesSnapshot;
|
||||
private bool? _showSubtitlesSnapshot;
|
||||
|
||||
public string Id => _rootItem.Command.Id;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether titles are shown for items in this band.
|
||||
/// This is a preview value - call <see cref="SaveLabelSettings"/> to persist or
|
||||
/// <see cref="RestoreLabelSettings"/> to discard changes.
|
||||
/// </summary>
|
||||
public bool ShowTitles
|
||||
{
|
||||
get => _showTitles;
|
||||
set
|
||||
{
|
||||
if (_showTitles != value)
|
||||
{
|
||||
_showTitles = value;
|
||||
foreach (var item in Items)
|
||||
{
|
||||
item.ShowTitle = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether subtitles are shown for items in this band.
|
||||
/// This is a preview value - call <see cref="SaveLabelSettings"/> to persist or
|
||||
/// <see cref="RestoreLabelSettings"/> to discard changes.
|
||||
/// </summary>
|
||||
public bool ShowSubtitles
|
||||
{
|
||||
get => _showSubtitles;
|
||||
set
|
||||
{
|
||||
if (_showSubtitles != value)
|
||||
{
|
||||
_showSubtitles = value;
|
||||
foreach (var item in Items)
|
||||
{
|
||||
item.ShowSubtitle = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether labels (both titles and subtitles) are shown.
|
||||
/// Provided for backward compatibility - setting this sets both ShowTitles and ShowSubtitles.
|
||||
/// </summary>
|
||||
public bool ShowLabels
|
||||
{
|
||||
get => _showTitles && _showSubtitles;
|
||||
set
|
||||
{
|
||||
ShowTitles = value;
|
||||
ShowSubtitles = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Takes a snapshot of the current label settings before editing.
|
||||
/// </summary>
|
||||
internal void SnapshotShowLabels()
|
||||
{
|
||||
_showTitlesSnapshot = _showTitles;
|
||||
_showSubtitlesSnapshot = _showSubtitles;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves the current label settings to settings.
|
||||
/// </summary>
|
||||
internal void SaveShowLabels()
|
||||
{
|
||||
_bandSettings.ShowTitles = _showTitles;
|
||||
_bandSettings.ShowSubtitles = _showSubtitles;
|
||||
_showTitlesSnapshot = null;
|
||||
_showSubtitlesSnapshot = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restores the label settings from the snapshot.
|
||||
/// </summary>
|
||||
internal void RestoreShowLabels()
|
||||
{
|
||||
if (_showTitlesSnapshot.HasValue)
|
||||
{
|
||||
ShowTitles = _showTitlesSnapshot.Value;
|
||||
_showTitlesSnapshot = null;
|
||||
}
|
||||
|
||||
if (_showSubtitlesSnapshot.HasValue)
|
||||
{
|
||||
ShowSubtitles = _showSubtitlesSnapshot.Value;
|
||||
_showSubtitlesSnapshot = null;
|
||||
}
|
||||
}
|
||||
|
||||
internal DockBandViewModel(
|
||||
CommandItemViewModel commandItemViewModel,
|
||||
WeakReference<IPageContext> errorContext,
|
||||
DockBandSettings settings,
|
||||
DockSettings dockSettings,
|
||||
Action saveSettings)
|
||||
: base(errorContext)
|
||||
{
|
||||
_rootItem = commandItemViewModel;
|
||||
_bandSettings = settings;
|
||||
_dockSettings = dockSettings;
|
||||
_saveSettings = saveSettings;
|
||||
|
||||
_showTitles = settings.ResolveShowTitles(dockSettings.ShowLabels);
|
||||
_showSubtitles = settings.ResolveShowSubtitles(dockSettings.ShowLabels);
|
||||
}
|
||||
|
||||
private void InitializeFromList(IListPage list)
|
||||
{
|
||||
var items = list.GetItems();
|
||||
var newViewModels = new List<DockItemViewModel>();
|
||||
foreach (var item in items)
|
||||
{
|
||||
var newItemVm = new DockItemViewModel(new(item), this.PageContext, _showTitles, _showSubtitles);
|
||||
newItemVm.SlowInitializeProperties();
|
||||
newViewModels.Add(newItemVm);
|
||||
}
|
||||
|
||||
DoOnUiThread(() =>
|
||||
{
|
||||
ListHelpers.InPlaceUpdateList(Items, newViewModels, out var removed);
|
||||
});
|
||||
|
||||
// TODO! dispose removed VMs
|
||||
}
|
||||
|
||||
public override void InitializeProperties()
|
||||
{
|
||||
var command = _rootItem.Command;
|
||||
var list = command.Model.Unsafe as IListPage;
|
||||
if (list is not null)
|
||||
{
|
||||
InitializeFromList(list);
|
||||
list.ItemsChanged += HandleItemsChanged;
|
||||
}
|
||||
else
|
||||
{
|
||||
DoOnUiThread(() =>
|
||||
{
|
||||
var dockItem = new DockItemViewModel(_rootItem, _showTitles, _showSubtitles);
|
||||
dockItem.SlowInitializeProperties();
|
||||
Items.Add(dockItem);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleItemsChanged(object sender, IItemsChangedEventArgs args)
|
||||
{
|
||||
if (_rootItem.Command.Model.Unsafe is IListPage p)
|
||||
{
|
||||
InitializeFromList(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public partial class DockItemViewModel : CommandItemViewModel
|
||||
{
|
||||
private bool _showTitle = true;
|
||||
private bool _showSubtitle = true;
|
||||
|
||||
public bool ShowTitle
|
||||
{
|
||||
get => _showTitle;
|
||||
internal set
|
||||
{
|
||||
if (_showTitle != value)
|
||||
{
|
||||
_showTitle = value;
|
||||
UpdateProperty(nameof(ShowTitle));
|
||||
UpdateProperty(nameof(ShowLabel));
|
||||
UpdateProperty(nameof(HasText));
|
||||
UpdateProperty(nameof(Title));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShowSubtitle
|
||||
{
|
||||
get => _showSubtitle;
|
||||
internal set
|
||||
{
|
||||
if (_showSubtitle != value)
|
||||
{
|
||||
_showSubtitle = value;
|
||||
UpdateProperty(nameof(ShowSubtitle));
|
||||
UpdateProperty(nameof(ShowLabel));
|
||||
UpdateProperty(nameof(Subtitle));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether labels are shown (either titles or subtitles).
|
||||
/// Setting this sets both ShowTitle and ShowSubtitle.
|
||||
/// </summary>
|
||||
public bool ShowLabel
|
||||
{
|
||||
get => _showTitle || _showSubtitle;
|
||||
internal set
|
||||
{
|
||||
ShowTitle = value;
|
||||
ShowSubtitle = value;
|
||||
}
|
||||
}
|
||||
|
||||
public override string Title => _showTitle ? ItemTitle : string.Empty;
|
||||
|
||||
public new string Subtitle => _showSubtitle ? base.Subtitle : string.Empty;
|
||||
|
||||
public override bool HasText => (_showTitle && !string.IsNullOrEmpty(ItemTitle)) || (_showSubtitle && !string.IsNullOrEmpty(base.Subtitle));
|
||||
|
||||
/// <summary>
|
||||
/// Gets the tooltip for the dock item, which includes the title and
|
||||
/// subtitle. If it doesn't have one part, it just returns the other.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Trickery: in the case one is empty, we can just concatenate, and it will
|
||||
/// always only be the one that's non-empty
|
||||
/// </remarks>
|
||||
public string Tooltip =>
|
||||
!string.IsNullOrEmpty(ItemTitle) && !string.IsNullOrEmpty(base.Subtitle) ?
|
||||
$"{ItemTitle}\n{base.Subtitle}" :
|
||||
ItemTitle + base.Subtitle;
|
||||
|
||||
public DockItemViewModel(CommandItemViewModel root, bool showTitle, bool showSubtitle)
|
||||
: this(root.Model, root.PageContext, showTitle, showSubtitle)
|
||||
{
|
||||
}
|
||||
|
||||
public DockItemViewModel(ExtensionObject<ICommandItem> item, WeakReference<IPageContext> errorContext, bool showTitle, bool showSubtitle)
|
||||
: base(item, errorContext)
|
||||
{
|
||||
_showTitle = showTitle;
|
||||
_showSubtitle = showSubtitle;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma warning restore SA1402 // File may only contain a single type
|
||||
@@ -0,0 +1,604 @@
|
||||
// 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.Messaging;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.Core.Common;
|
||||
using Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.CmdPal.UI.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Dock;
|
||||
|
||||
public sealed partial class DockViewModel : IDisposable,
|
||||
IRecipient<CommandsReloadedMessage>,
|
||||
IPageContext
|
||||
{
|
||||
private readonly TopLevelCommandManager _topLevelCommandManager;
|
||||
private readonly SettingsModel _settingsModel;
|
||||
|
||||
private DockSettings _settings;
|
||||
|
||||
public TaskScheduler Scheduler { get; }
|
||||
|
||||
public ObservableCollection<DockBandViewModel> StartItems { get; } = new();
|
||||
|
||||
public ObservableCollection<DockBandViewModel> CenterItems { get; } = new();
|
||||
|
||||
public ObservableCollection<DockBandViewModel> EndItems { get; } = new();
|
||||
|
||||
public ObservableCollection<TopLevelViewModel> AllItems => _topLevelCommandManager.DockBands;
|
||||
|
||||
bool IPageContext.ExtensionSupportsPinning => false;
|
||||
|
||||
public DockViewModel(
|
||||
TopLevelCommandManager tlcManager,
|
||||
SettingsModel settings,
|
||||
TaskScheduler scheduler)
|
||||
{
|
||||
_topLevelCommandManager = tlcManager;
|
||||
_settingsModel = settings;
|
||||
_settings = settings.DockSettings;
|
||||
Scheduler = scheduler;
|
||||
WeakReferenceMessenger.Default.Register<CommandsReloadedMessage>(this);
|
||||
}
|
||||
|
||||
public void UpdateSettings(DockSettings settings)
|
||||
{
|
||||
Logger.LogDebug($"DockViewModel.UpdateSettings");
|
||||
_settings = settings;
|
||||
SetupBands();
|
||||
}
|
||||
|
||||
private void SetupBands()
|
||||
{
|
||||
Logger.LogDebug($"Setting up dock bands");
|
||||
SetupBands(_settings.StartBands, StartItems);
|
||||
SetupBands(_settings.CenterBands, CenterItems);
|
||||
SetupBands(_settings.EndBands, EndItems);
|
||||
}
|
||||
|
||||
private void SetupBands(
|
||||
List<DockBandSettings> bands,
|
||||
ObservableCollection<DockBandViewModel> target)
|
||||
{
|
||||
List<DockBandViewModel> newBands = new();
|
||||
foreach (var band in bands)
|
||||
{
|
||||
var commandId = band.Id;
|
||||
var topLevelCommand = _topLevelCommandManager.LookupDockBand(commandId);
|
||||
|
||||
if (topLevelCommand is null)
|
||||
{
|
||||
Logger.LogWarning($"Failed to find band {commandId}");
|
||||
}
|
||||
|
||||
if (topLevelCommand is not null)
|
||||
{
|
||||
var bandVm = CreateBandItem(band, topLevelCommand.ItemViewModel);
|
||||
newBands.Add(bandVm);
|
||||
}
|
||||
}
|
||||
|
||||
var beforeCount = target.Count;
|
||||
var afterCount = newBands.Count;
|
||||
|
||||
DoOnUiThread(() =>
|
||||
{
|
||||
ListHelpers.InPlaceUpdateList(target, newBands, out var removed);
|
||||
var isStartBand = target == StartItems;
|
||||
var label = isStartBand ? "Start bands:" : "End bands:";
|
||||
Logger.LogDebug($"{label} ({beforeCount}) -> ({afterCount}), Removed {removed?.Count ?? 0} items");
|
||||
});
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
|
||||
public void Receive(CommandsReloadedMessage message)
|
||||
{
|
||||
SetupBands();
|
||||
CoreLogger.LogDebug("Bands reloaded");
|
||||
}
|
||||
|
||||
private DockBandViewModel CreateBandItem(
|
||||
DockBandSettings bandSettings,
|
||||
CommandItemViewModel commandItem)
|
||||
{
|
||||
DockBandViewModel band = new(commandItem, new(this), bandSettings, _settings, SaveSettings);
|
||||
band.InitializeProperties(); // TODO! make async
|
||||
return band;
|
||||
}
|
||||
|
||||
private void SaveSettings()
|
||||
{
|
||||
SettingsModel.SaveSettings(_settingsModel);
|
||||
}
|
||||
|
||||
public DockBandViewModel? FindBandByTopLevel(TopLevelViewModel tlc)
|
||||
{
|
||||
var id = tlc.Id;
|
||||
return FindBandById(id);
|
||||
}
|
||||
|
||||
public DockBandViewModel? FindBandById(string id)
|
||||
{
|
||||
foreach (var band in StartItems)
|
||||
{
|
||||
if (band.Id == id)
|
||||
{
|
||||
return band;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var band in CenterItems)
|
||||
{
|
||||
if (band.Id == id)
|
||||
{
|
||||
return band;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var band in EndItems)
|
||||
{
|
||||
if (band.Id == id)
|
||||
{
|
||||
return band;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Syncs the band position in settings after a same-list reorder.
|
||||
/// Does not save to disk - call SaveBandOrder() when done editing.
|
||||
/// </summary>
|
||||
public void SyncBandPosition(DockBandViewModel band, DockPinSide targetSide, int targetIndex)
|
||||
{
|
||||
var bandId = band.Id;
|
||||
var dockSettings = _settingsModel.DockSettings;
|
||||
|
||||
var bandSettings = dockSettings.StartBands.FirstOrDefault(b => b.Id == bandId)
|
||||
?? dockSettings.CenterBands.FirstOrDefault(b => b.Id == bandId)
|
||||
?? dockSettings.EndBands.FirstOrDefault(b => b.Id == bandId);
|
||||
|
||||
if (bandSettings == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove from all settings lists
|
||||
dockSettings.StartBands.RemoveAll(b => b.Id == bandId);
|
||||
dockSettings.CenterBands.RemoveAll(b => b.Id == bandId);
|
||||
dockSettings.EndBands.RemoveAll(b => b.Id == bandId);
|
||||
|
||||
// Add to target settings list at the correct index
|
||||
var targetSettings = targetSide switch
|
||||
{
|
||||
DockPinSide.Start => dockSettings.StartBands,
|
||||
DockPinSide.Center => dockSettings.CenterBands,
|
||||
DockPinSide.End => dockSettings.EndBands,
|
||||
_ => dockSettings.StartBands,
|
||||
};
|
||||
var insertIndex = Math.Min(targetIndex, targetSettings.Count);
|
||||
targetSettings.Insert(insertIndex, bandSettings);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Moves a dock band to a new position (cross-list drop).
|
||||
/// Does not save to disk - call SaveBandOrder() when done editing.
|
||||
/// </summary>
|
||||
public void MoveBandWithoutSaving(DockBandViewModel band, DockPinSide targetSide, int targetIndex)
|
||||
{
|
||||
var bandId = band.Id;
|
||||
var dockSettings = _settingsModel.DockSettings;
|
||||
|
||||
var bandSettings = dockSettings.StartBands.FirstOrDefault(b => b.Id == bandId)
|
||||
?? dockSettings.CenterBands.FirstOrDefault(b => b.Id == bandId)
|
||||
?? dockSettings.EndBands.FirstOrDefault(b => b.Id == bandId);
|
||||
|
||||
if (bandSettings == null)
|
||||
{
|
||||
Logger.LogWarning($"Could not find band settings for band {bandId}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove from all sides (settings and UI)
|
||||
dockSettings.StartBands.RemoveAll(b => b.Id == bandId);
|
||||
dockSettings.CenterBands.RemoveAll(b => b.Id == bandId);
|
||||
dockSettings.EndBands.RemoveAll(b => b.Id == bandId);
|
||||
StartItems.Remove(band);
|
||||
CenterItems.Remove(band);
|
||||
EndItems.Remove(band);
|
||||
|
||||
// Add to the target side at the specified index
|
||||
switch (targetSide)
|
||||
{
|
||||
case DockPinSide.Start:
|
||||
{
|
||||
var settingsIndex = Math.Min(targetIndex, dockSettings.StartBands.Count);
|
||||
dockSettings.StartBands.Insert(settingsIndex, bandSettings);
|
||||
|
||||
var uiIndex = Math.Min(targetIndex, StartItems.Count);
|
||||
StartItems.Insert(uiIndex, band);
|
||||
break;
|
||||
}
|
||||
|
||||
case DockPinSide.Center:
|
||||
{
|
||||
var settingsIndex = Math.Min(targetIndex, dockSettings.CenterBands.Count);
|
||||
dockSettings.CenterBands.Insert(settingsIndex, bandSettings);
|
||||
|
||||
var uiIndex = Math.Min(targetIndex, CenterItems.Count);
|
||||
CenterItems.Insert(uiIndex, band);
|
||||
break;
|
||||
}
|
||||
|
||||
case DockPinSide.End:
|
||||
{
|
||||
var settingsIndex = Math.Min(targetIndex, dockSettings.EndBands.Count);
|
||||
dockSettings.EndBands.Insert(settingsIndex, bandSettings);
|
||||
|
||||
var uiIndex = Math.Min(targetIndex, EndItems.Count);
|
||||
EndItems.Insert(uiIndex, band);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Logger.LogDebug($"Moved band {bandId} to {targetSide} at index {targetIndex} (not saved yet)");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves the current band order and label settings to settings.
|
||||
/// Call this when exiting edit mode.
|
||||
/// </summary>
|
||||
public void SaveBandOrder()
|
||||
{
|
||||
// Save ShowLabels for all bands
|
||||
foreach (var band in StartItems.Concat(CenterItems).Concat(EndItems))
|
||||
{
|
||||
band.SaveShowLabels();
|
||||
}
|
||||
|
||||
_snapshotStartBands = null;
|
||||
_snapshotCenterBands = null;
|
||||
_snapshotEndBands = null;
|
||||
_snapshotPinnedCommands = null;
|
||||
_snapshotBandViewModels = null;
|
||||
SettingsModel.SaveSettings(_settingsModel);
|
||||
Logger.LogDebug("Saved band order to settings");
|
||||
}
|
||||
|
||||
private List<DockBandSettings>? _snapshotStartBands;
|
||||
private List<DockBandSettings>? _snapshotCenterBands;
|
||||
private List<DockBandSettings>? _snapshotEndBands;
|
||||
private List<string>? _snapshotPinnedCommands;
|
||||
private Dictionary<string, DockBandViewModel>? _snapshotBandViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// Takes a snapshot of the current band order and label settings before editing.
|
||||
/// Call this when entering edit mode.
|
||||
/// </summary>
|
||||
public void SnapshotBandOrder()
|
||||
{
|
||||
var dockSettings = _settingsModel.DockSettings;
|
||||
_snapshotStartBands = dockSettings.StartBands.Select(b => new DockBandSettings { Id = b.Id, ShowLabels = b.ShowLabels }).ToList();
|
||||
_snapshotCenterBands = dockSettings.CenterBands.Select(b => new DockBandSettings { Id = b.Id, ShowLabels = b.ShowLabels }).ToList();
|
||||
_snapshotEndBands = dockSettings.EndBands.Select(b => new DockBandSettings { Id = b.Id, ShowLabels = b.ShowLabels }).ToList();
|
||||
_snapshotPinnedCommands = dockSettings.PinnedCommands.ToList();
|
||||
|
||||
// Snapshot band ViewModels so we can restore unpinned bands
|
||||
// Use a dictionary but handle potential duplicates gracefully
|
||||
_snapshotBandViewModels = new Dictionary<string, DockBandViewModel>();
|
||||
foreach (var band in StartItems.Concat(CenterItems).Concat(EndItems))
|
||||
{
|
||||
_snapshotBandViewModels.TryAdd(band.Id, band);
|
||||
}
|
||||
|
||||
// Snapshot ShowLabels for all bands
|
||||
foreach (var band in _snapshotBandViewModels.Values)
|
||||
{
|
||||
band.SnapshotShowLabels();
|
||||
}
|
||||
|
||||
Logger.LogDebug($"Snapshot taken: {_snapshotStartBands.Count} start bands, {_snapshotCenterBands.Count} center bands, {_snapshotEndBands.Count} end bands");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restores the band order and label settings from the snapshot taken when entering edit mode.
|
||||
/// Call this when discarding edit mode changes.
|
||||
/// </summary>
|
||||
public void RestoreBandOrder()
|
||||
{
|
||||
if (_snapshotStartBands == null || _snapshotCenterBands == null || _snapshotEndBands == null || _snapshotBandViewModels == null || _snapshotPinnedCommands == null)
|
||||
{
|
||||
Logger.LogWarning("No snapshot to restore from");
|
||||
return;
|
||||
}
|
||||
|
||||
// Restore ShowLabels for all snapshotted bands
|
||||
foreach (var band in _snapshotBandViewModels.Values)
|
||||
{
|
||||
band.RestoreShowLabels();
|
||||
}
|
||||
|
||||
var dockSettings = _settingsModel.DockSettings;
|
||||
|
||||
// Restore PinnedCommands from snapshot
|
||||
dockSettings.PinnedCommands.Clear();
|
||||
dockSettings.PinnedCommands.AddRange(_snapshotPinnedCommands);
|
||||
|
||||
// Restore settings from snapshot
|
||||
dockSettings.StartBands.Clear();
|
||||
dockSettings.CenterBands.Clear();
|
||||
dockSettings.EndBands.Clear();
|
||||
|
||||
foreach (var bandSnapshot in _snapshotStartBands)
|
||||
{
|
||||
var bandSettings = new DockBandSettings { Id = bandSnapshot.Id, ShowLabels = bandSnapshot.ShowLabels };
|
||||
dockSettings.StartBands.Add(bandSettings);
|
||||
}
|
||||
|
||||
foreach (var bandSnapshot in _snapshotCenterBands)
|
||||
{
|
||||
var bandSettings = new DockBandSettings { Id = bandSnapshot.Id, ShowLabels = bandSnapshot.ShowLabels };
|
||||
dockSettings.CenterBands.Add(bandSettings);
|
||||
}
|
||||
|
||||
foreach (var bandSnapshot in _snapshotEndBands)
|
||||
{
|
||||
var bandSettings = new DockBandSettings { Id = bandSnapshot.Id, ShowLabels = bandSnapshot.ShowLabels };
|
||||
dockSettings.EndBands.Add(bandSettings);
|
||||
}
|
||||
|
||||
// Rebuild UI collections from restored settings using the snapshotted ViewModels
|
||||
RebuildUICollectionsFromSnapshot();
|
||||
|
||||
_snapshotStartBands = null;
|
||||
_snapshotCenterBands = null;
|
||||
_snapshotEndBands = null;
|
||||
_snapshotPinnedCommands = null;
|
||||
_snapshotBandViewModels = null;
|
||||
Logger.LogDebug("Restored band order from snapshot");
|
||||
}
|
||||
|
||||
private void RebuildUICollectionsFromSnapshot()
|
||||
{
|
||||
if (_snapshotBandViewModels == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var dockSettings = _settingsModel.DockSettings;
|
||||
|
||||
StartItems.Clear();
|
||||
CenterItems.Clear();
|
||||
EndItems.Clear();
|
||||
|
||||
foreach (var bandSettings in dockSettings.StartBands)
|
||||
{
|
||||
if (_snapshotBandViewModels.TryGetValue(bandSettings.Id, out var bandVM))
|
||||
{
|
||||
StartItems.Add(bandVM);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var bandSettings in dockSettings.CenterBands)
|
||||
{
|
||||
if (_snapshotBandViewModels.TryGetValue(bandSettings.Id, out var bandVM))
|
||||
{
|
||||
CenterItems.Add(bandVM);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var bandSettings in dockSettings.EndBands)
|
||||
{
|
||||
if (_snapshotBandViewModels.TryGetValue(bandSettings.Id, out var bandVM))
|
||||
{
|
||||
EndItems.Add(bandVM);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RebuildUICollections()
|
||||
{
|
||||
var dockSettings = _settingsModel.DockSettings;
|
||||
|
||||
// Create a lookup of all current band ViewModels
|
||||
var allBands = StartItems.Concat(CenterItems).Concat(EndItems).ToDictionary(b => b.Id);
|
||||
|
||||
StartItems.Clear();
|
||||
CenterItems.Clear();
|
||||
EndItems.Clear();
|
||||
|
||||
foreach (var bandSettings in dockSettings.StartBands)
|
||||
{
|
||||
if (allBands.TryGetValue(bandSettings.Id, out var bandVM))
|
||||
{
|
||||
StartItems.Add(bandVM);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var bandSettings in dockSettings.CenterBands)
|
||||
{
|
||||
if (allBands.TryGetValue(bandSettings.Id, out var bandVM))
|
||||
{
|
||||
CenterItems.Add(bandVM);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var bandSettings in dockSettings.EndBands)
|
||||
{
|
||||
if (allBands.TryGetValue(bandSettings.Id, out var bandVM))
|
||||
{
|
||||
EndItems.Add(bandVM);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of dock bands that are not currently pinned to any section.
|
||||
/// </summary>
|
||||
public IEnumerable<TopLevelViewModel> GetAvailableBandsToAdd()
|
||||
{
|
||||
// Get IDs of all bands currently in the dock
|
||||
var pinnedBandIds = new HashSet<string>();
|
||||
foreach (var band in StartItems)
|
||||
{
|
||||
pinnedBandIds.Add(band.Id);
|
||||
}
|
||||
|
||||
foreach (var band in CenterItems)
|
||||
{
|
||||
pinnedBandIds.Add(band.Id);
|
||||
}
|
||||
|
||||
foreach (var band in EndItems)
|
||||
{
|
||||
pinnedBandIds.Add(band.Id);
|
||||
}
|
||||
|
||||
// Return all dock bands that are not already pinned
|
||||
return AllItems.Where(tlc => !pinnedBandIds.Contains(tlc.Id));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a band to the specified dock section.
|
||||
/// Does not save to disk - call SaveBandOrder() when done editing.
|
||||
/// </summary>
|
||||
public void AddBandToSection(TopLevelViewModel topLevel, DockPinSide targetSide)
|
||||
{
|
||||
var bandId = topLevel.Id;
|
||||
|
||||
// Check if already in the dock
|
||||
if (FindBandById(bandId) != null)
|
||||
{
|
||||
Logger.LogWarning($"Band {bandId} is already in the dock");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create settings for the new band
|
||||
var bandSettings = new DockBandSettings { Id = bandId, ShowLabels = null };
|
||||
var dockSettings = _settingsModel.DockSettings;
|
||||
|
||||
// If this is not an explicit dock band (i.e., it's from TopLevelCommands),
|
||||
// add it to PinnedCommands so it gets loaded as a dock band on restart
|
||||
if (!topLevel.IsDockBand && !dockSettings.PinnedCommands.Contains(bandId))
|
||||
{
|
||||
dockSettings.PinnedCommands.Add(bandId);
|
||||
}
|
||||
|
||||
// Create the band view model
|
||||
var bandVm = CreateBandItem(bandSettings, topLevel.ItemViewModel);
|
||||
|
||||
// Add to the appropriate section
|
||||
switch (targetSide)
|
||||
{
|
||||
case DockPinSide.Start:
|
||||
dockSettings.StartBands.Add(bandSettings);
|
||||
StartItems.Add(bandVm);
|
||||
break;
|
||||
case DockPinSide.Center:
|
||||
dockSettings.CenterBands.Add(bandSettings);
|
||||
CenterItems.Add(bandVm);
|
||||
break;
|
||||
case DockPinSide.End:
|
||||
dockSettings.EndBands.Add(bandSettings);
|
||||
EndItems.Add(bandVm);
|
||||
break;
|
||||
}
|
||||
|
||||
// Snapshot the new band so it can be removed on discard
|
||||
bandVm.SnapshotShowLabels();
|
||||
|
||||
Logger.LogDebug($"Added band {bandId} to {targetSide} (not saved yet)");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unpins a band from the dock, removing it from whichever section it's in.
|
||||
/// Does not save to disk - call SaveBandOrder() when done editing.
|
||||
/// </summary>
|
||||
public void UnpinBand(DockBandViewModel band)
|
||||
{
|
||||
var bandId = band.Id;
|
||||
var dockSettings = _settingsModel.DockSettings;
|
||||
|
||||
// Remove from settings
|
||||
dockSettings.StartBands.RemoveAll(b => b.Id == bandId);
|
||||
dockSettings.CenterBands.RemoveAll(b => b.Id == bandId);
|
||||
dockSettings.EndBands.RemoveAll(b => b.Id == bandId);
|
||||
|
||||
// Also remove from PinnedCommands if it was pinned from TopLevelCommands
|
||||
dockSettings.PinnedCommands.Remove(bandId);
|
||||
|
||||
// Remove from UI collections
|
||||
StartItems.Remove(band);
|
||||
CenterItems.Remove(band);
|
||||
EndItems.Remove(band);
|
||||
|
||||
Logger.LogDebug($"Unpinned band {bandId} (not saved yet)");
|
||||
}
|
||||
|
||||
public void ShowException(Exception ex, string? extensionHint = null)
|
||||
{
|
||||
var extensionText = extensionHint ?? "<unknown>";
|
||||
CoreLogger.LogError($"Error in extension {extensionText}", ex);
|
||||
}
|
||||
|
||||
private void DoOnUiThread(Action action)
|
||||
{
|
||||
Task.Factory.StartNew(
|
||||
action,
|
||||
CancellationToken.None,
|
||||
TaskCreationOptions.None,
|
||||
Scheduler);
|
||||
}
|
||||
|
||||
public CommandItemViewModel GetContextMenuForDock()
|
||||
{
|
||||
var model = new DockContextMenuItem();
|
||||
var vm = new CommandItemViewModel(new(model), new(this));
|
||||
vm.SlowInitializeProperties();
|
||||
return vm;
|
||||
}
|
||||
|
||||
private sealed partial class DockContextMenuItem : CommandItem
|
||||
{
|
||||
public DockContextMenuItem()
|
||||
{
|
||||
var editDockCommand = new AnonymousCommand(
|
||||
action: () =>
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send(new EnterDockEditModeMessage());
|
||||
})
|
||||
{
|
||||
Name = Properties.Resources.dock_edit_dock_name,
|
||||
Icon = Icons.EditIcon,
|
||||
};
|
||||
|
||||
var openSettingsCommand = new AnonymousCommand(
|
||||
action: () =>
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send(new OpenSettingsMessage("Dock"));
|
||||
})
|
||||
{
|
||||
Name = Properties.Resources.dock_settings_name,
|
||||
Icon = Icons.SettingsIcon,
|
||||
};
|
||||
|
||||
MoreCommands = new CommandContextItem[]
|
||||
{
|
||||
new CommandContextItem(editDockCommand),
|
||||
new CommandContextItem(openSettingsCommand),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#pragma warning restore SA1402 // File may only contain a single type
|
||||
@@ -0,0 +1,90 @@
|
||||
// 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.CmdPal.UI.ViewModels.Settings;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Dock;
|
||||
|
||||
public partial class DockWindowViewModel : 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; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool ShowColorizationOverlay { get; private set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial Color ColorizationColor { get; private set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial double ColorizationOpacity { get; private set; }
|
||||
|
||||
public DockWindowViewModel(IThemeService themeService)
|
||||
{
|
||||
_themeService = themeService;
|
||||
_themeService.ThemeChanged += ThemeService_ThemeChanged;
|
||||
UpdateFromThemeSnapshot();
|
||||
}
|
||||
|
||||
private void ThemeService_ThemeChanged(object? sender, ThemeChangedEventArgs e)
|
||||
{
|
||||
_uiDispatcherQueue.TryEnqueue(UpdateFromThemeSnapshot);
|
||||
}
|
||||
|
||||
private void UpdateFromThemeSnapshot()
|
||||
{
|
||||
var snapshot = _themeService.CurrentDockTheme;
|
||||
|
||||
BackgroundImageSource = snapshot.BackgroundImageSource;
|
||||
BackgroundImageStretch = snapshot.BackgroundImageStretch;
|
||||
BackgroundImageOpacity = snapshot.BackgroundImageOpacity;
|
||||
|
||||
BackgroundImageBrightness = snapshot.BackgroundBrightness;
|
||||
BackgroundImageTint = snapshot.Tint;
|
||||
BackgroundImageTintIntensity = snapshot.TintIntensity;
|
||||
BackgroundImageBlurAmount = snapshot.BlurAmount;
|
||||
|
||||
ShowBackgroundImage = BackgroundImageSource != null;
|
||||
|
||||
// Colorization overlay for transparent backdrop
|
||||
ShowColorizationOverlay = snapshot.Backdrop == DockBackdrop.Transparent && snapshot.TintIntensity > 0;
|
||||
ColorizationColor = snapshot.Tint;
|
||||
ColorizationOpacity = snapshot.TintIntensity;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_themeService.ThemeChanged -= ThemeService_ThemeChanged;
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
// 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.CmdPal.UI.ViewModels.Settings;
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// View model for dock appearance settings, controlling theme, backdrop, colorization,
|
||||
/// and background image settings for the dock.
|
||||
/// </summary>
|
||||
public sealed partial class DockAppearanceSettingsViewModel : ObservableObject, IDisposable
|
||||
{
|
||||
private readonly SettingsModel _settings;
|
||||
private readonly DockSettings _dockSettings;
|
||||
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 => AppearanceSettingsViewModel.WindowsColorSwatches;
|
||||
|
||||
public int ThemeIndex
|
||||
{
|
||||
get => (int)_dockSettings.Theme;
|
||||
set => Theme = (UserTheme)value;
|
||||
}
|
||||
|
||||
public UserTheme Theme
|
||||
{
|
||||
get => _dockSettings.Theme;
|
||||
set
|
||||
{
|
||||
if (_dockSettings.Theme != value)
|
||||
{
|
||||
_dockSettings.Theme = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(ThemeIndex));
|
||||
Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int BackdropIndex
|
||||
{
|
||||
get => (int)_dockSettings.Backdrop;
|
||||
set => Backdrop = (DockBackdrop)value;
|
||||
}
|
||||
|
||||
public DockBackdrop Backdrop
|
||||
{
|
||||
get => _dockSettings.Backdrop;
|
||||
set
|
||||
{
|
||||
if (_dockSettings.Backdrop != value)
|
||||
{
|
||||
_dockSettings.Backdrop = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(BackdropIndex));
|
||||
Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ColorizationMode ColorizationMode
|
||||
{
|
||||
get => _dockSettings.ColorizationMode;
|
||||
set
|
||||
{
|
||||
if (_dockSettings.ColorizationMode != value)
|
||||
{
|
||||
_dockSettings.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)_dockSettings.ColorizationMode;
|
||||
set => ColorizationMode = (ColorizationMode)value;
|
||||
}
|
||||
|
||||
public Color ThemeColor
|
||||
{
|
||||
get => _dockSettings.CustomThemeColor;
|
||||
set
|
||||
{
|
||||
if (_dockSettings.CustomThemeColor != value)
|
||||
{
|
||||
_dockSettings.CustomThemeColor = value;
|
||||
|
||||
OnPropertyChanged();
|
||||
|
||||
if (ColorIntensity == 0)
|
||||
{
|
||||
ColorIntensity = 100;
|
||||
}
|
||||
|
||||
Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int ColorIntensity
|
||||
{
|
||||
get => _dockSettings.CustomThemeColorIntensity;
|
||||
set
|
||||
{
|
||||
_dockSettings.CustomThemeColorIntensity = value;
|
||||
OnPropertyChanged();
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
public string BackgroundImagePath
|
||||
{
|
||||
get => _dockSettings.BackgroundImagePath ?? string.Empty;
|
||||
set
|
||||
{
|
||||
if (_dockSettings.BackgroundImagePath != value)
|
||||
{
|
||||
_dockSettings.BackgroundImagePath = value;
|
||||
OnPropertyChanged();
|
||||
|
||||
if (BackgroundImageOpacity == 0)
|
||||
{
|
||||
BackgroundImageOpacity = 100;
|
||||
}
|
||||
|
||||
Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int BackgroundImageOpacity
|
||||
{
|
||||
get => _dockSettings.BackgroundImageOpacity;
|
||||
set
|
||||
{
|
||||
if (_dockSettings.BackgroundImageOpacity != value)
|
||||
{
|
||||
_dockSettings.BackgroundImageOpacity = value;
|
||||
OnPropertyChanged();
|
||||
Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int BackgroundImageBrightness
|
||||
{
|
||||
get => _dockSettings.BackgroundImageBrightness;
|
||||
set
|
||||
{
|
||||
if (_dockSettings.BackgroundImageBrightness != value)
|
||||
{
|
||||
_dockSettings.BackgroundImageBrightness = value;
|
||||
OnPropertyChanged();
|
||||
Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int BackgroundImageBlurAmount
|
||||
{
|
||||
get => _dockSettings.BackgroundImageBlurAmount;
|
||||
set
|
||||
{
|
||||
if (_dockSettings.BackgroundImageBlurAmount != value)
|
||||
{
|
||||
_dockSettings.BackgroundImageBlurAmount = value;
|
||||
OnPropertyChanged();
|
||||
Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public BackgroundImageFit BackgroundImageFit
|
||||
{
|
||||
get => _dockSettings.BackgroundImageFit;
|
||||
set
|
||||
{
|
||||
if (_dockSettings.BackgroundImageFit != value)
|
||||
{
|
||||
_dockSettings.BackgroundImageFit = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(BackgroundImageFitIndex));
|
||||
Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int BackgroundImageFitIndex
|
||||
{
|
||||
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 => _dockSettings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.Image;
|
||||
|
||||
public bool IsCustomTintIntensityVisible => _dockSettings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.WindowsAccentColor or ColorizationMode.Image;
|
||||
|
||||
public bool IsBackgroundControlsVisible => _dockSettings.ColorizationMode is ColorizationMode.Image;
|
||||
|
||||
public bool IsNoBackgroundVisible => _dockSettings.ColorizationMode is ColorizationMode.None;
|
||||
|
||||
public bool IsAccentColorControlsVisible => _dockSettings.ColorizationMode is ColorizationMode.WindowsAccentColor;
|
||||
|
||||
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 DockAppearanceSettingsViewModel(IThemeService themeService, SettingsModel settings)
|
||||
{
|
||||
_themeService = themeService;
|
||||
_themeService.ThemeChanged += ThemeServiceOnThemeChanged;
|
||||
_settings = settings;
|
||||
_dockSettings = settings.DockSettings;
|
||||
|
||||
_uiSettings = new UISettings();
|
||||
_uiSettings.ColorValuesChanged += UiSettingsOnColorValuesChanged;
|
||||
UpdateAccentColor(_uiSettings);
|
||||
|
||||
Reapply();
|
||||
|
||||
IsColorizationDetailsExpanded = _dockSettings.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()
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
18
src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Icons.cs
Normal file
18
src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Icons.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
internal sealed class Icons
|
||||
{
|
||||
internal static IconInfo PinIcon => new("\uE718"); // Pin icon
|
||||
|
||||
internal static IconInfo UnpinIcon => new("\uE77A"); // Unpin icon
|
||||
|
||||
internal static IconInfo SettingsIcon => new("\uE713"); // Settings icon
|
||||
|
||||
internal static IconInfo EditIcon => new("\uE70F"); // Edit icon
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
|
||||
public record CommandsReloadedMessage();
|
||||
@@ -0,0 +1,7 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
|
||||
public record EnterDockEditModeMessage();
|
||||
@@ -0,0 +1,7 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
|
||||
public record ShowHideDockMessage(bool ShowDock);
|
||||
@@ -60,6 +60,15 @@ namespace Microsoft.CmdPal.UI.ViewModels.Properties {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Open Command Palette.
|
||||
/// </summary>
|
||||
public static string builtin_command_palette_title {
|
||||
get {
|
||||
return ResourceManager.GetString("builtin_command_palette_title", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Create another.
|
||||
/// </summary>
|
||||
@@ -288,6 +297,12 @@ namespace Microsoft.CmdPal.UI.ViewModels.Properties {
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Built-in.
|
||||
/// </summary>
|
||||
public static string builtin_extension_name_fallback {
|
||||
get {
|
||||
return ResourceManager.GetString("builtin_extension_name_fallback", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string builtin_extension_name {
|
||||
get {
|
||||
return ResourceManager.GetString("builtin_extension_name", resourceCulture);
|
||||
@@ -465,6 +480,60 @@ namespace Microsoft.CmdPal.UI.ViewModels.Properties {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Edit dock.
|
||||
/// </summary>
|
||||
public static string dock_edit_dock_name {
|
||||
get {
|
||||
return ResourceManager.GetString("dock_edit_dock_name", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to {0} items.
|
||||
/// </summary>
|
||||
public static string dock_item_count_plural {
|
||||
get {
|
||||
return ResourceManager.GetString("dock_item_count_plural", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to 1 item.
|
||||
/// </summary>
|
||||
public static string dock_item_count_singular {
|
||||
get {
|
||||
return ResourceManager.GetString("dock_item_count_singular", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Pin to dock.
|
||||
/// </summary>
|
||||
public static string dock_pin_command_name {
|
||||
get {
|
||||
return ResourceManager.GetString("dock_pin_command_name", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Dock settings.
|
||||
/// </summary>
|
||||
public static string dock_settings_name {
|
||||
get {
|
||||
return ResourceManager.GetString("dock_settings_name", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Unpin from dock.
|
||||
/// </summary>
|
||||
public static string dock_unpin_command_name {
|
||||
get {
|
||||
return ResourceManager.GetString("dock_unpin_command_name", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Fallbacks.
|
||||
/// </summary>
|
||||
|
||||
@@ -254,10 +254,42 @@
|
||||
<data name="builtin_settings_extension_n_extensions_installed" xml:space="preserve">
|
||||
<value>{0} extensions installed</value>
|
||||
</data>
|
||||
<data name="builtin_extension_name_fallback" xml:space="preserve">
|
||||
<value>Built-in</value>
|
||||
<comment>Fallback name for built-in extensions</comment>
|
||||
</data>
|
||||
<data name="dock_pin_command_name" xml:space="preserve">
|
||||
<value>Pin to dock</value>
|
||||
<comment>Command name for pinning an item to the dock</comment>
|
||||
</data>
|
||||
<data name="dock_unpin_command_name" xml:space="preserve">
|
||||
<value>Unpin from dock</value>
|
||||
<comment>Command name for unpinning an item from the dock</comment>
|
||||
</data>
|
||||
<data name="dock_item_count_singular" xml:space="preserve">
|
||||
<value>1 item</value>
|
||||
<comment>Singular form for item count in dock band</comment>
|
||||
</data>
|
||||
<data name="dock_item_count_plural" xml:space="preserve">
|
||||
<value>{0} items</value>
|
||||
<comment>Plural form for item count in dock band</comment>
|
||||
</data>
|
||||
<data name="builtin_command_palette_title" xml:space="preserve">
|
||||
<value>Open Command Palette</value>
|
||||
<comment>Title for the command to open the command palette</comment>
|
||||
</data>
|
||||
<data name="builtin_settings_appearance_pick_background_image_title" xml:space="preserve">
|
||||
<value>Pick background image</value>
|
||||
</data>
|
||||
<data name="fallbacks" xml:space="preserve">
|
||||
<value>Fallbacks</value>
|
||||
</data>
|
||||
<data name="dock_edit_dock_name" xml:space="preserve">
|
||||
<value>Edit dock</value>
|
||||
<comment>Command name for editing the dock</comment>
|
||||
</data>
|
||||
<data name="dock_settings_name" xml:space="preserve">
|
||||
<value>Dock settings</value>
|
||||
<comment>Command name for opening dock settings</comment>
|
||||
</data>
|
||||
</root>
|
||||
@@ -0,0 +1,73 @@
|
||||
// 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.Settings;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a snapshot of dock theme-related visual settings, including accent color, theme preference,
|
||||
/// backdrop, and background image configuration, for use in rendering the Dock UI.
|
||||
/// </summary>
|
||||
public sealed class DockThemeSnapshot
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the accent tint color used by the Dock visuals.
|
||||
/// </summary>
|
||||
public required Color Tint { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the intensity of the accent tint color (0-1 range).
|
||||
/// </summary>
|
||||
public required float TintIntensity { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the configured application theme preference for the Dock.
|
||||
/// </summary>
|
||||
public required ElementTheme Theme { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the backdrop type for the Dock.
|
||||
/// </summary>
|
||||
public required DockBackdrop Backdrop { 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>
|
||||
public required BackdropParameters BackdropParameters { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the blur amount for the background image.
|
||||
/// </summary>
|
||||
public required int BlurAmount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the brightness adjustment for the background (0-1 range).
|
||||
/// </summary>
|
||||
public required float BackgroundBrightness { get; init; }
|
||||
}
|
||||
@@ -2,6 +2,8 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
|
||||
/// <summary>
|
||||
@@ -36,4 +38,9 @@ public interface IThemeService
|
||||
/// Gets the current theme settings.
|
||||
/// </summary>
|
||||
ThemeSnapshot Current { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current dock theme settings.
|
||||
/// </summary>
|
||||
DockThemeSnapshot CurrentDockTheme { get; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
// 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 Windows.UI;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
|
||||
#pragma warning disable SA1402 // File may only contain a single type
|
||||
|
||||
/// <summary>
|
||||
/// Settings for the Dock. These are settings for _the whole dock_. Band-specific
|
||||
/// settings are in <see cref="DockBandSettings"/>.
|
||||
/// </summary>
|
||||
public class DockSettings
|
||||
{
|
||||
public DockSide Side { get; set; } = DockSide.Top;
|
||||
|
||||
public DockSize DockSize { get; set; } = DockSize.Small;
|
||||
|
||||
public DockSize DockIconsSize { get; set; } = DockSize.Small;
|
||||
|
||||
// Theme settings
|
||||
public DockBackdrop Backdrop { get; set; } = DockBackdrop.Acrylic;
|
||||
|
||||
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; }
|
||||
|
||||
// /Theme settings
|
||||
public List<string> PinnedCommands { get; set; } = [];
|
||||
|
||||
public List<CommandItemIdentity> PinnedCommandItems { get; set; } = [];
|
||||
|
||||
public List<DockBandSettings> StartBands { get; set; } = [];
|
||||
|
||||
public List<DockBandSettings> CenterBands { get; set; } = [];
|
||||
|
||||
public List<DockBandSettings> EndBands { get; set; } = [];
|
||||
|
||||
public bool ShowLabels { get; set; } = true;
|
||||
|
||||
public DockSettings()
|
||||
{
|
||||
// Initialize with default values
|
||||
PinnedCommands = [
|
||||
"com.microsoft.cmdpal.winget"
|
||||
];
|
||||
|
||||
StartBands.Add(new DockBandSettings { Id = "com.microsoft.cmdpal.home" });
|
||||
StartBands.Add(new DockBandSettings { Id = "com.microsoft.cmdpal.winget", ShowLabels = false });
|
||||
|
||||
EndBands.Add(new DockBandSettings { Id = "com.microsoft.cmdpal.performanceWidget" });
|
||||
EndBands.Add(new DockBandSettings { Id = "com.microsoft.cmdpal.timedate.dockband" });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Settings for a specific dock band. These are per-band settings stored
|
||||
/// within the overall <see cref="DockSettings"/>.
|
||||
/// </summary>
|
||||
public class DockBandSettings
|
||||
{
|
||||
public string Id { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether titles are shown for items in this band.
|
||||
/// If null, falls back to dock-wide ShowLabels setting.
|
||||
/// </summary>
|
||||
public bool? ShowTitles { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether subtitles are shown for items in this band.
|
||||
/// If null, falls back to dock-wide ShowLabels setting.
|
||||
/// </summary>
|
||||
public bool? ShowSubtitles { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value for backward compatibility. Maps to ShowTitles.
|
||||
/// </summary>
|
||||
[System.Text.Json.Serialization.JsonIgnore]
|
||||
public bool? ShowLabels
|
||||
{
|
||||
get => ShowTitles;
|
||||
set => ShowTitles = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the effective value of <see cref="ShowTitles"/> for this band.
|
||||
/// If this band doesn't have a specific value set, we'll fall back to the
|
||||
/// dock-wide setting (passed as <paramref name="defaultValue"/>).
|
||||
/// </summary>
|
||||
public bool ResolveShowTitles(bool defaultValue) => ShowTitles ?? defaultValue;
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the effective value of <see cref="ShowSubtitles"/> for this band.
|
||||
/// If this band doesn't have a specific value set, we'll fall back to the
|
||||
/// dock-wide setting (passed as <paramref name="defaultValue"/>).
|
||||
/// </summary>
|
||||
public bool ResolveShowSubtitles(bool defaultValue) => ShowSubtitles ?? defaultValue;
|
||||
}
|
||||
|
||||
public enum DockSide
|
||||
{
|
||||
Left = 0,
|
||||
Top = 1,
|
||||
Right = 2,
|
||||
Bottom = 3,
|
||||
}
|
||||
|
||||
public enum DockSize
|
||||
{
|
||||
Small,
|
||||
Medium,
|
||||
Large,
|
||||
}
|
||||
|
||||
public enum DockBackdrop
|
||||
{
|
||||
Transparent,
|
||||
Acrylic,
|
||||
}
|
||||
|
||||
public record CommandItemIdentity(string ProviderId, string CommandId);
|
||||
|
||||
#pragma warning restore SA1402 // File may only contain a single type
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
@@ -66,6 +66,11 @@ public partial class SettingsModel : ObservableObject
|
||||
|
||||
public EscapeKeyBehavior EscapeKeyBehaviorSetting { get; set; } = EscapeKeyBehavior.ClearSearchFirstThenGoBack;
|
||||
|
||||
public bool EnableDock { get; set; }
|
||||
|
||||
public DockSettings DockSettings { get; set; } = new();
|
||||
|
||||
// Theme settings
|
||||
public UserTheme Theme { get; set; } = UserTheme.Default;
|
||||
|
||||
public ColorizationMode ColorizationMode { get; set; }
|
||||
@@ -90,6 +95,8 @@ public partial class SettingsModel : ObservableObject
|
||||
|
||||
public int BackdropOpacity { get; set; } = 100;
|
||||
|
||||
// </Theme settings>
|
||||
|
||||
// END SETTINGS
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -309,6 +316,7 @@ public partial class SettingsModel : ObservableObject
|
||||
[JsonSerializable(typeof(int))]
|
||||
[JsonSerializable(typeof(string))]
|
||||
[JsonSerializable(typeof(bool))]
|
||||
[JsonSerializable(typeof(Color))]
|
||||
[JsonSerializable(typeof(HistoryItem))]
|
||||
[JsonSerializable(typeof(SettingsModel))]
|
||||
[JsonSerializable(typeof(WindowPosition))]
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.CmdPal.Core.Common.Services;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
@@ -32,6 +35,8 @@ public partial class SettingsViewModel : INotifyPropertyChanged
|
||||
|
||||
public AppearanceSettingsViewModel Appearance { get; }
|
||||
|
||||
public DockAppearanceSettingsViewModel DockAppearance { get; }
|
||||
|
||||
public HotkeySettings? Hotkey
|
||||
{
|
||||
get => _settings.Hotkey;
|
||||
@@ -173,6 +178,58 @@ public partial class SettingsViewModel : INotifyPropertyChanged
|
||||
}
|
||||
}
|
||||
|
||||
public DockSide Dock_Side
|
||||
{
|
||||
get => _settings.DockSettings.Side;
|
||||
set
|
||||
{
|
||||
_settings.DockSettings.Side = value;
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
public DockSize Dock_DockSize
|
||||
{
|
||||
get => _settings.DockSettings.DockSize;
|
||||
set
|
||||
{
|
||||
_settings.DockSettings.DockSize = value;
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
public DockBackdrop Dock_Backdrop
|
||||
{
|
||||
get => _settings.DockSettings.Backdrop;
|
||||
set
|
||||
{
|
||||
_settings.DockSettings.Backdrop = value;
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
public bool Dock_ShowLabels
|
||||
{
|
||||
get => _settings.DockSettings.ShowLabels;
|
||||
set
|
||||
{
|
||||
_settings.DockSettings.ShowLabels = value;
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
public bool EnableDock
|
||||
{
|
||||
get => _settings.EnableDock;
|
||||
set
|
||||
{
|
||||
_settings.EnableDock = value;
|
||||
Save();
|
||||
WeakReferenceMessenger.Default.Send(new ShowHideDockMessage(value));
|
||||
WeakReferenceMessenger.Default.Send(new ReloadCommandsMessage()); // TODO! we need to update the MoreCommands of all top level items, but we don't _really_ want to reload
|
||||
}
|
||||
}
|
||||
|
||||
public ObservableCollection<ProviderSettingsViewModel> CommandProviders { get; } = new();
|
||||
|
||||
public ObservableCollection<FallbackSettingsViewModel> FallbackRankings { get; set; } = new();
|
||||
@@ -185,6 +242,7 @@ public partial class SettingsViewModel : INotifyPropertyChanged
|
||||
_topLevelCommandManager = topLevelCommandManager;
|
||||
|
||||
Appearance = new AppearanceSettingsViewModel(themeService, _settings);
|
||||
DockAppearance = new DockAppearanceSettingsViewModel(themeService, _settings);
|
||||
|
||||
var activeProviders = GetCommandProviders();
|
||||
var allProviderSettings = _settings.ProviderSettings;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
@@ -47,6 +47,8 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
|
||||
public ObservableCollection<TopLevelViewModel> TopLevelCommands { get; set; } = [];
|
||||
|
||||
public ObservableCollection<TopLevelViewModel> DockBands { get; set; } = [];
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsLoading { get; private set; } = true;
|
||||
|
||||
@@ -61,6 +63,8 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
}
|
||||
}
|
||||
|
||||
bool IPageContext.ExtensionSupportsPinning => false;
|
||||
|
||||
public async Task<bool> LoadBuiltinsAsync()
|
||||
{
|
||||
var s = new Stopwatch();
|
||||
@@ -82,12 +86,23 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
_builtInCommands.Add(wrapper);
|
||||
}
|
||||
|
||||
var commands = await LoadTopLevelCommandsFromProvider(wrapper);
|
||||
var objects = await LoadTopLevelCommandsFromProvider(wrapper);
|
||||
lock (TopLevelCommands)
|
||||
{
|
||||
foreach (var c in commands)
|
||||
if (objects.Commands is IEnumerable<TopLevelViewModel> commands)
|
||||
{
|
||||
TopLevelCommands.Add(c);
|
||||
foreach (var c in commands)
|
||||
{
|
||||
TopLevelCommands.Add(c);
|
||||
}
|
||||
}
|
||||
|
||||
if (objects.DockBands is IEnumerable<TopLevelViewModel> bands)
|
||||
{
|
||||
foreach (var c in bands)
|
||||
{
|
||||
DockBands.Add(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -100,7 +115,7 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
}
|
||||
|
||||
// May be called from a background thread
|
||||
private async Task<IEnumerable<TopLevelViewModel>> LoadTopLevelCommandsFromProvider(CommandProviderWrapper commandProvider)
|
||||
private async Task<TopLevelObjectSets> LoadTopLevelCommandsFromProvider(CommandProviderWrapper commandProvider)
|
||||
{
|
||||
WeakReference<IPageContext> weakSelf = new(this);
|
||||
|
||||
@@ -110,6 +125,7 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
() =>
|
||||
{
|
||||
List<TopLevelViewModel> commands = [];
|
||||
List<TopLevelViewModel> bands = [];
|
||||
foreach (var item in commandProvider.TopLevelItems)
|
||||
{
|
||||
commands.Add(item);
|
||||
@@ -123,7 +139,15 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
}
|
||||
}
|
||||
|
||||
return commands;
|
||||
foreach (var item in commandProvider.DockBandItems)
|
||||
{
|
||||
bands.Add(item);
|
||||
}
|
||||
|
||||
var commandsCount = commands.Count;
|
||||
var bandsCount = bands.Count;
|
||||
Logger.LogDebug($"{commandProvider.ProviderId}: Loaded {commandsCount} commands, {bandsCount} bands");
|
||||
return new TopLevelObjectSets(commands, bands);
|
||||
},
|
||||
CancellationToken.None,
|
||||
TaskCreationOptions.None,
|
||||
@@ -163,6 +187,8 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
}
|
||||
}
|
||||
|
||||
List<TopLevelViewModel> newBands = [.. sender.DockBandItems];
|
||||
|
||||
// modify the TopLevelCommands under shared lock; event if we clone it, we don't want
|
||||
// TopLevelCommands to get modified while we're working on it. Otherwise, we might
|
||||
// out clone would be stale at the end of this method.
|
||||
@@ -179,6 +205,13 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
clone.InsertRange(startIndex, newItems);
|
||||
|
||||
ListHelpers.InPlaceUpdateList(TopLevelCommands, clone);
|
||||
|
||||
// same idea for DockBands
|
||||
List<TopLevelViewModel> dockClone = [.. DockBands];
|
||||
var dockStartIndex = FindIndexForFirstProviderItem(dockClone, sender.ProviderId);
|
||||
dockClone.RemoveAll(item => item.CommandProviderId == sender.ProviderId);
|
||||
dockClone.InsertRange(dockStartIndex, newBands);
|
||||
ListHelpers.InPlaceUpdateList(DockBands, dockClone);
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -225,6 +258,7 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
lock (TopLevelCommands)
|
||||
{
|
||||
TopLevelCommands.Clear();
|
||||
DockBands.Clear();
|
||||
}
|
||||
|
||||
await LoadBuiltinsAsync();
|
||||
@@ -303,17 +337,34 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
|
||||
lock (TopLevelCommands)
|
||||
{
|
||||
foreach (var commands in commandSets)
|
||||
foreach (var providerObjects in commandSets)
|
||||
{
|
||||
foreach (var c in commands)
|
||||
var commandsCount = providerObjects.Commands?.Count() ?? 0;
|
||||
var bandsCount = providerObjects.DockBands?.Count() ?? 0;
|
||||
Logger.LogDebug($"(some provider) Loaded {commandsCount} commands and {bandsCount} bands");
|
||||
|
||||
if (providerObjects.Commands is IEnumerable<TopLevelViewModel> commands)
|
||||
{
|
||||
TopLevelCommands.Add(c);
|
||||
foreach (var c in commands)
|
||||
{
|
||||
TopLevelCommands.Add(c);
|
||||
}
|
||||
}
|
||||
|
||||
if (providerObjects.DockBands is IEnumerable<TopLevelViewModel> bands)
|
||||
{
|
||||
foreach (var c in bands)
|
||||
{
|
||||
DockBands.Add(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
timer.Stop();
|
||||
Logger.LogDebug($"Loading extensions took {timer.ElapsedMilliseconds} ms");
|
||||
|
||||
WeakReferenceMessenger.Default.Send<CommandsReloadedMessage>();
|
||||
}
|
||||
|
||||
private async Task<CommandProviderWrapper?> StartExtensionWithTimeoutAsync(IExtensionWrapper extension)
|
||||
@@ -331,7 +382,9 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<TopLevelViewModel>?> LoadCommandsWithTimeoutAsync(CommandProviderWrapper wrapper)
|
||||
private record TopLevelObjectSets(IEnumerable<TopLevelViewModel>? Commands, IEnumerable<TopLevelViewModel>? DockBands);
|
||||
|
||||
private async Task<TopLevelObjectSets?> LoadCommandsWithTimeoutAsync(CommandProviderWrapper wrapper)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -411,6 +464,23 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
return null;
|
||||
}
|
||||
|
||||
public TopLevelViewModel? LookupDockBand(string id)
|
||||
{
|
||||
// TODO! bad that we're using TopLevelCommands as the object to lock, even for bands
|
||||
lock (TopLevelCommands)
|
||||
{
|
||||
foreach (var command in DockBands)
|
||||
{
|
||||
if (command.Id == id)
|
||||
{
|
||||
return command;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void Receive(ReloadCommandsMessage message) =>
|
||||
ReloadAllCommandsAsync().ConfigureAwait(false);
|
||||
|
||||
@@ -429,6 +499,52 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
}
|
||||
}
|
||||
|
||||
internal void PinDockBand(TopLevelViewModel bandVm)
|
||||
{
|
||||
lock (DockBands)
|
||||
{
|
||||
foreach (var existing in DockBands)
|
||||
{
|
||||
if (existing.Id == bandVm.Id)
|
||||
{
|
||||
// already pinned
|
||||
Logger.LogDebug($"Dock band '{bandVm.Id}' is already pinned.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Logger.LogDebug($"Attempting to pin dock band '{bandVm.Id}' from provider '{bandVm.CommandProviderId}'.");
|
||||
var providerId = bandVm.CommandProviderId;
|
||||
var foundProvider = false;
|
||||
foreach (var provider in CommandProviders)
|
||||
{
|
||||
if (provider.Id == providerId)
|
||||
{
|
||||
Logger.LogDebug($"Found provider '{providerId}' to pin dock band '{bandVm.Id}'.");
|
||||
provider.PinDockBand(bandVm);
|
||||
foundProvider = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundProvider)
|
||||
{
|
||||
Logger.LogWarning($"Could not find provider '{providerId}' to pin dock band '{bandVm.Id}'.");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Add the band to DockBands if not already present
|
||||
if (!DockBands.Any(b => b.Id == bandVm.Id))
|
||||
{
|
||||
DockBands.Add(bandVm);
|
||||
}
|
||||
|
||||
// Notify DockViewModel to update its collections
|
||||
WeakReferenceMessenger.Default.Send<CommandsReloadedMessage>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_reloadCommandsGate.Dispose();
|
||||
|
||||
@@ -10,6 +10,7 @@ using Microsoft.CmdPal.Core.Common.Helpers;
|
||||
using Microsoft.CmdPal.Core.Common.Text;
|
||||
using Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Dock;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
@@ -26,9 +27,11 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
|
||||
private readonly ProviderSettings _providerSettings;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly CommandItemViewModel _commandItemViewModel;
|
||||
private readonly DockViewModel? _dockViewModel;
|
||||
|
||||
private readonly string _commandProviderId;
|
||||
|
||||
// private readonly bool _providerSupportsPinning;
|
||||
private string IdFromModel => IsFallback && !string.IsNullOrWhiteSpace(_fallbackId) ? _fallbackId : _commandItemViewModel.Command.Id;
|
||||
|
||||
private string _fallbackId = string.Empty;
|
||||
@@ -53,39 +56,30 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
|
||||
|
||||
public CommandPaletteHost ExtensionHost { get; private set; }
|
||||
|
||||
public string ExtensionName => ExtensionHost.GetExtensionDisplayName() ?? string.Empty;
|
||||
|
||||
public CommandViewModel CommandViewModel => _commandItemViewModel.Command;
|
||||
|
||||
public CommandItemViewModel ItemViewModel => _commandItemViewModel;
|
||||
|
||||
public string CommandProviderId => _commandProviderId;
|
||||
|
||||
public IconInfoViewModel IconViewModel => _commandItemViewModel.Icon;
|
||||
|
||||
// public bool ProviderSupportsPinning => _providerSupportsPinning;
|
||||
|
||||
////// ICommandItem
|
||||
public string Title => _commandItemViewModel.Title;
|
||||
|
||||
public string Subtitle => _commandItemViewModel.Subtitle;
|
||||
|
||||
public IIconInfo Icon => _commandItemViewModel.Icon;
|
||||
public IIconInfo Icon => (IIconInfo)IconViewModel;
|
||||
|
||||
public IIconInfo InitialIcon => _initialIcon ?? _commandItemViewModel.Icon;
|
||||
|
||||
ICommand? ICommandItem.Command => _commandItemViewModel.Command.Model.Unsafe;
|
||||
|
||||
IContextItem?[] ICommandItem.MoreCommands => _commandItemViewModel.MoreCommands
|
||||
.Select(item =>
|
||||
{
|
||||
if (item is ISeparatorContextItem)
|
||||
{
|
||||
return item as IContextItem;
|
||||
}
|
||||
else if (item is CommandContextItemViewModel commandItem)
|
||||
{
|
||||
return commandItem.Model.Unsafe;
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}).ToArray();
|
||||
IContextItem?[] ICommandItem.MoreCommands => BuildContextMenu();
|
||||
|
||||
////// IListItem
|
||||
ITag[] IListItem.Tags => Tags.ToArray();
|
||||
@@ -184,41 +178,70 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
|
||||
}
|
||||
}
|
||||
|
||||
public string ExtensionName => ExtensionHost.GetExtensionDisplayName() ?? string.Empty;
|
||||
// Dock properties
|
||||
public bool IsDockBand { get; private set; }
|
||||
|
||||
public DockBandSettings? DockBandSettings
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!IsDockBand)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var bandSettings = _settings.DockSettings.StartBands
|
||||
.Concat(_settings.DockSettings.EndBands)
|
||||
.FirstOrDefault(band => band.Id == this.Id);
|
||||
if (bandSettings is null)
|
||||
{
|
||||
return new DockBandSettings()
|
||||
{
|
||||
Id = this.Id,
|
||||
ShowLabels = true,
|
||||
};
|
||||
}
|
||||
|
||||
return bandSettings;
|
||||
}
|
||||
}
|
||||
|
||||
public TopLevelViewModel(
|
||||
CommandItemViewModel item,
|
||||
bool isFallback,
|
||||
TopLevelType topLevelType,
|
||||
CommandPaletteHost extensionHost,
|
||||
string commandProviderId,
|
||||
SettingsModel settings,
|
||||
ProviderSettings providerSettings,
|
||||
IServiceProvider serviceProvider,
|
||||
ICommandItem? commandItem)
|
||||
ICommandItem? commandItem)/*,
|
||||
bool providerSupportsPinning*/
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
_settings = settings;
|
||||
_providerSettings = providerSettings;
|
||||
_commandProviderId = commandProviderId;
|
||||
|
||||
// _providerSupportsPinning = providerSupportsPinning;
|
||||
_commandItemViewModel = item;
|
||||
|
||||
IsFallback = isFallback;
|
||||
IsFallback = topLevelType == TopLevelType.Fallback;
|
||||
IsDockBand = topLevelType == TopLevelType.DockBand;
|
||||
ExtensionHost = extensionHost;
|
||||
if (isFallback && commandItem is FallbackCommandItem fallback)
|
||||
if (IsFallback && commandItem is FallbackCommandItem fallback)
|
||||
{
|
||||
_fallbackId = fallback.Id;
|
||||
}
|
||||
|
||||
item.PropertyChangedBackground += Item_PropertyChanged;
|
||||
|
||||
// UpdateAlias();
|
||||
// UpdateHotkey();
|
||||
// UpdateTags();
|
||||
_dockViewModel = serviceProvider.GetService<DockViewModel>();
|
||||
}
|
||||
|
||||
internal void InitializeProperties()
|
||||
{
|
||||
ItemViewModel.SlowInitializeProperties();
|
||||
GenerateId();
|
||||
|
||||
if (IsFallback)
|
||||
{
|
||||
@@ -279,7 +302,7 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
|
||||
return;
|
||||
}
|
||||
|
||||
_initialIcon = _commandItemViewModel.Icon;
|
||||
_initialIcon = (IIconInfo?)_commandItemViewModel.Icon;
|
||||
|
||||
if (raiseNotification)
|
||||
{
|
||||
@@ -453,4 +476,169 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
|
||||
{
|
||||
return ToString();
|
||||
}
|
||||
|
||||
private IContextItem?[] BuildContextMenu()
|
||||
{
|
||||
List<IContextItem?> contextItems = new();
|
||||
|
||||
foreach (var item in _commandItemViewModel.MoreCommands)
|
||||
{
|
||||
if (item is ISeparatorContextItem)
|
||||
{
|
||||
contextItems.Add(item as IContextItem);
|
||||
}
|
||||
else if (item is CommandContextItemViewModel commandItem)
|
||||
{
|
||||
contextItems.Add(commandItem.Model.Unsafe);
|
||||
}
|
||||
}
|
||||
|
||||
var dockEnabled = _settings.EnableDock;
|
||||
if (dockEnabled && _dockViewModel is not null)
|
||||
{
|
||||
// Add a separator
|
||||
contextItems.Add(new Separator());
|
||||
|
||||
var inStartBands = _settings.DockSettings.StartBands.Any(band => band.Id == this.Id);
|
||||
var inCenterBands = _settings.DockSettings.CenterBands.Any(band => band.Id == this.Id);
|
||||
var inEndBands = _settings.DockSettings.EndBands.Any(band => band.Id == this.Id);
|
||||
var alreadyPinned = (inStartBands || inCenterBands || inEndBands) &&
|
||||
_settings.DockSettings.PinnedCommands.Contains(this.Id);
|
||||
|
||||
var pinCommand = new PinToDockCommand(
|
||||
this,
|
||||
!alreadyPinned,
|
||||
_dockViewModel,
|
||||
_settings,
|
||||
_serviceProvider.GetService<TopLevelCommandManager>()!);
|
||||
|
||||
var contextItem = new CommandContextItem(pinCommand);
|
||||
|
||||
contextItems.Add(contextItem);
|
||||
}
|
||||
|
||||
return contextItems.ToArray();
|
||||
}
|
||||
|
||||
internal ICommandItem ToPinnedDockBandItem()
|
||||
{
|
||||
var item = new PinnedDockItem(item: this, id: Id);
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
internal TopLevelViewModel CloneAsBand()
|
||||
{
|
||||
return new TopLevelViewModel(
|
||||
_commandItemViewModel,
|
||||
TopLevelType.DockBand,
|
||||
ExtensionHost,
|
||||
_commandProviderId,
|
||||
_settings,
|
||||
_providerSettings,
|
||||
_serviceProvider,
|
||||
_commandItemViewModel.Model.Unsafe);
|
||||
}
|
||||
|
||||
private sealed partial class PinToDockCommand : InvokableCommand
|
||||
{
|
||||
private readonly TopLevelViewModel _topLevelViewModel;
|
||||
private readonly DockViewModel _dockViewModel;
|
||||
private readonly SettingsModel _settings;
|
||||
private readonly TopLevelCommandManager _topLevelCommandManager;
|
||||
private readonly bool _pin;
|
||||
|
||||
public override IconInfo Icon => _pin ? Icons.PinIcon : Icons.UnpinIcon;
|
||||
|
||||
public override string Name => _pin ? Properties.Resources.dock_pin_command_name : Properties.Resources.dock_unpin_command_name;
|
||||
|
||||
public PinToDockCommand(
|
||||
TopLevelViewModel topLevelViewModel,
|
||||
bool pin,
|
||||
DockViewModel dockViewModel,
|
||||
SettingsModel settings,
|
||||
TopLevelCommandManager topLevelCommandManager)
|
||||
{
|
||||
_topLevelViewModel = topLevelViewModel;
|
||||
_dockViewModel = dockViewModel;
|
||||
_settings = settings;
|
||||
_topLevelCommandManager = topLevelCommandManager;
|
||||
_pin = pin;
|
||||
}
|
||||
|
||||
public override CommandResult Invoke()
|
||||
{
|
||||
Logger.LogDebug($"PinToDockCommand.Invoke({_pin}): {_topLevelViewModel.Id}");
|
||||
if (_pin)
|
||||
{
|
||||
PinToDock();
|
||||
}
|
||||
else
|
||||
{
|
||||
UnpinFromDock();
|
||||
}
|
||||
|
||||
// Notify that the MoreCommands have changed, so the context menu updates
|
||||
_topLevelViewModel.PropChanged?.Invoke(
|
||||
_topLevelViewModel,
|
||||
new PropChangedEventArgs(nameof(ICommandItem.MoreCommands)));
|
||||
return CommandResult.GoHome();
|
||||
}
|
||||
|
||||
private void PinToDock()
|
||||
{
|
||||
// It's possible that the top-level command shares an ID with a
|
||||
// band. In that case, we don't want to add it to PinnedCommands.
|
||||
// PinnedCommands is just for top-level commands IDs that aren't
|
||||
// otherwise bands.
|
||||
//
|
||||
// Check the top-level command ID against the bands first.
|
||||
if (_topLevelCommandManager.DockBands.Any(band => band.Id == _topLevelViewModel.Id))
|
||||
{
|
||||
}
|
||||
else
|
||||
{
|
||||
// In this case, the ID isn't another band, so add it to PinnedCommands.
|
||||
if (!_settings.DockSettings.PinnedCommands.Contains(_topLevelViewModel.Id))
|
||||
{
|
||||
_settings.DockSettings.PinnedCommands.Add(_topLevelViewModel.Id);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO! Deal with "the command ID is already pinned in
|
||||
// PinnedCommands but not in one of StartBands/EndBands". I think
|
||||
// we're already avoiding adding it to PinnedCommands above, but I
|
||||
// think that PinDockBand below will create a duplicate VM for it.
|
||||
_settings.DockSettings.StartBands.Add(new DockBandSettings()
|
||||
{
|
||||
Id = _topLevelViewModel.Id,
|
||||
ShowLabels = true,
|
||||
});
|
||||
|
||||
// Create a new band VM from our current TLVM. This will allow us to
|
||||
// update the bands in the CommandProviderWrapper and the TLCM,
|
||||
// without forcing a whole reload
|
||||
var bandVm = _topLevelViewModel.CloneAsBand();
|
||||
_topLevelCommandManager.PinDockBand(bandVm);
|
||||
|
||||
_topLevelViewModel.Save();
|
||||
}
|
||||
|
||||
private void UnpinFromDock()
|
||||
{
|
||||
_settings.DockSettings.PinnedCommands.Remove(_topLevelViewModel.Id);
|
||||
_settings.DockSettings.StartBands.RemoveAll(band => band.Id == _topLevelViewModel.Id);
|
||||
_settings.DockSettings.CenterBands.RemoveAll(band => band.Id == _topLevelViewModel.Id);
|
||||
_settings.DockSettings.EndBands.RemoveAll(band => band.Id == _topLevelViewModel.Id);
|
||||
|
||||
_topLevelViewModel.Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum TopLevelType
|
||||
{
|
||||
Normal,
|
||||
Fallback,
|
||||
DockBand,
|
||||
}
|
||||
|
||||
@@ -4,23 +4,22 @@
|
||||
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:local="using:Microsoft.CmdPal.UI"
|
||||
xmlns:ptcontrols="using:Microsoft.PowerToys.Common.UI.Controls"
|
||||
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="ms-appx:///Styles/Colors.xaml" />
|
||||
<ResourceDictionary Source="ms-appx:///Styles/TeachingTip.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:///PowerToys.Common.UI.Controls/Controls/KeyVisual/KeyVisual.xaml" />
|
||||
<ResourceDictionary Source="ms-appx:///PowerToys.Common.UI.Controls/Controls/KeyVisual/KeyCharPresenter.xaml" />
|
||||
<ResourceDictionary Source="ms-appx:///PowerToys.Common.UI.Controls/Controls/ShortcutWithTextLabelControl/ShortcutWithTextLabelControl.xaml" />
|
||||
<ResourceDictionary Source="ms-appx:///PowerToys.Common.UI.Controls/Controls/IsEnabledTextBlock/IsEnabledTextBlock.xaml" />
|
||||
<ResourceDictionary Source="ms-appx:///Controls/KeyVisual/KeyVisual.xaml" />
|
||||
<ResourceDictionary Source="ms-appx:///Controls/IsEnabledTextBlock.xaml" />
|
||||
<ResourceDictionary Source="ms-appx:///Dock/DockItemControl.xaml" />
|
||||
<!-- Default theme dictionary -->
|
||||
<ResourceDictionary Source="ms-appx:///Styles/Theme.Normal.xaml" />
|
||||
<services:MutableOverridesDictionary />
|
||||
@@ -28,7 +27,15 @@
|
||||
<!-- Other app resources here -->
|
||||
|
||||
<x:Double x:Key="SettingActionControlMinWidth">240</x:Double>
|
||||
<Style BasedOn="{StaticResource DefaultCheckBoxStyle}" TargetType="ptcontrols:CheckBoxWithDescriptionControl" />
|
||||
<Style BasedOn="{StaticResource DefaultCheckBoxStyle}" TargetType="controls:CheckBoxWithDescriptionControl" />
|
||||
|
||||
<converters:StringVisibilityConverter
|
||||
x:Key="StringNotEmptyToVisibilityConverter"
|
||||
EmptyValue="Collapsed"
|
||||
NotEmptyValue="Visible" />
|
||||
|
||||
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
|
||||
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
|
||||
@@ -29,6 +29,7 @@ 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.Dock;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Models;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
@@ -221,6 +222,8 @@ public partial class App : Application, IDisposable
|
||||
|
||||
// ViewModels
|
||||
services.AddSingleton<ShellViewModel>();
|
||||
services.AddSingleton<DockViewModel>();
|
||||
services.AddSingleton<IContextMenuFactory, CommandPaletteContextMenuFactory>();
|
||||
services.AddSingleton<IPageViewModelFactoryService, CommandPalettePageViewModelFactory>();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
// To learn more about WinUI, the WinUI project structure,
|
||||
// and more about our project templates, see: http://aka.ms/winui-project-info.
|
||||
namespace Microsoft.CmdPal.UI;
|
||||
|
||||
internal sealed partial class CommandPaletteContextMenuFactory : IContextMenuFactory
|
||||
{
|
||||
private readonly SettingsModel _settingsModel;
|
||||
private readonly TopLevelCommandManager _topLevelCommandManager;
|
||||
|
||||
public CommandPaletteContextMenuFactory(SettingsModel settingsModel, TopLevelCommandManager topLevelCommandManager)
|
||||
{
|
||||
_settingsModel = settingsModel;
|
||||
_topLevelCommandManager = topLevelCommandManager;
|
||||
}
|
||||
|
||||
public List<IContextItemViewModel> UnsafeBuildAndInitMoreCommands(
|
||||
IContextItem[] items,
|
||||
CommandItemViewModel commandItem)
|
||||
{
|
||||
var results = DefaultContextMenuFactory.Instance.UnsafeBuildAndInitMoreCommands(items, commandItem);
|
||||
|
||||
// Don't add pin to dock option if the dock isn't enabled
|
||||
if (!_settingsModel.EnableDock)
|
||||
{
|
||||
return results;
|
||||
}
|
||||
|
||||
// TODO! move to a factory or something, so that the UI layer is
|
||||
// responsible for stealth-adding this pin command.
|
||||
if (commandItem.PageContext.TryGetTarget(out var pageContext))
|
||||
{
|
||||
// * The extension needs to support the GetCommandItem API to support pinning
|
||||
// * We need to have an ID to pin it by
|
||||
// * We don't want to show "Pin to Dock" on items that are already context menu items
|
||||
if (pageContext.ExtensionSupportsPinning &&
|
||||
!string.IsNullOrEmpty(commandItem.Command.Id) &&
|
||||
!commandItem.IsContextMenuItem)
|
||||
{
|
||||
results.Add(new SeparatorViewModel());
|
||||
|
||||
var inStartBands = _settingsModel.DockSettings.StartBands.Any(band => band.Id == this.Id);
|
||||
var inCenterBands = _settingsModel.DockSettings.CenterBands.Any(band => band.Id == this.Id);
|
||||
var inEndBands = _settingsModel.DockSettings.EndBands.Any(band => band.Id == this.Id);
|
||||
var alreadyPinned = (inStartBands || inCenterBands || inEndBands) &&
|
||||
_settingsModel.DockSettings.PinnedCommands.Contains(this.Id);
|
||||
|
||||
var pinCommand = new PinToDockItem(commandItem, !alreadyPinned, _topLevelCommandManager, _settingsModel);
|
||||
var pinCommandViewModel = new CommandContextItemViewModel(pinCommand, commandItem.PageContext);
|
||||
pinCommandViewModel.SlowInitializeProperties();
|
||||
results.Add(pinCommandViewModel);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private sealed partial class PinToDockItem : CommandContextItem
|
||||
{
|
||||
private static readonly PinToDockCommand PinCommand = new();
|
||||
private readonly CommandItemViewModel _owner;
|
||||
private readonly SettingsModel _settings;
|
||||
private readonly TopLevelCommandManager _topLevelCommandManager;
|
||||
private readonly bool _pin;
|
||||
|
||||
internal CommandItemViewModel Owner => _owner;
|
||||
|
||||
// public override IconInfo Icon => _pin ? Icons.PinIcon : Icons.UnpinIcon;
|
||||
|
||||
// public override string Title => _pin ? Properties.Resources.dock_pin_command_name : Properties.Resources.dock_unpin_command_name;
|
||||
private string Id => _owner.Command.Id;
|
||||
|
||||
public PinToDockItem(
|
||||
CommandItemViewModel owner,
|
||||
bool pin,
|
||||
TopLevelCommandManager topLevelCommandManager,
|
||||
SettingsModel settingsModel)
|
||||
: base(PinCommand)
|
||||
{
|
||||
_pin = pin;
|
||||
_owner = owner;
|
||||
_topLevelCommandManager = topLevelCommandManager;
|
||||
_settings = settingsModel;
|
||||
}
|
||||
|
||||
public CommandResult Invoke()
|
||||
{
|
||||
CoreLogger.LogDebug($"PinToDockItem.Invoke({_pin}): {Id}");
|
||||
|
||||
if (_pin)
|
||||
{
|
||||
PinToDock();
|
||||
}
|
||||
else
|
||||
{
|
||||
UnpinFromDock();
|
||||
}
|
||||
|
||||
// // Notify that the MoreCommands have changed, so the context menu updates
|
||||
// _topLevelViewModel.PropChanged?.Invoke(
|
||||
// _topLevelViewModel,
|
||||
// new PropChangedEventArgs(nameof(ICommandItem.MoreCommands)));
|
||||
|
||||
// TODO! what's the least gross way to cause the
|
||||
// CommandItemViewModel to re-fetch it's context menu?
|
||||
return CommandResult.GoHome();
|
||||
}
|
||||
|
||||
private void PinToDock()
|
||||
{
|
||||
// It's possible that the top-level command shares an ID with a
|
||||
// band. In that case, we don't want to add it to PinnedCommands.
|
||||
// PinnedCommands is just for top-level commands IDs that aren't
|
||||
// otherwise bands.
|
||||
//
|
||||
// Check the top-level command ID against the bands first.
|
||||
if (_topLevelCommandManager.DockBands.Any(band => band.Id == Id))
|
||||
{
|
||||
}
|
||||
else
|
||||
{
|
||||
// In this case, the ID isn't another band, so add it to
|
||||
// PinnedCommands.
|
||||
//
|
||||
// TODO!! We need to include the extension ID in the pinned
|
||||
// command somehow, so that we know where to look this command
|
||||
// up later.
|
||||
if (!_settings.DockSettings.PinnedCommands.Contains(Id))
|
||||
{
|
||||
_settings.DockSettings.PinnedCommands.Add(Id);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO! Deal with "the command ID is already pinned in
|
||||
// PinnedCommands but not in one of StartBands/EndBands". I think
|
||||
// we're already avoiding adding it to PinnedCommands above, but I
|
||||
// think that PinDockBand below will create a duplicate VM for it.
|
||||
_settings.DockSettings.StartBands.Add(new DockBandSettings()
|
||||
{
|
||||
Id = Id,
|
||||
ShowLabels = true,
|
||||
});
|
||||
|
||||
// Create a new band VM from our current TLVM. This will allow us to
|
||||
// update the bands in the CommandProviderWrapper and the TLCM,
|
||||
// without forcing a whole reload
|
||||
var bandVm = _topLevelViewModel.CloneAsBand();
|
||||
_topLevelCommandManager.PinDockBand(bandVm);
|
||||
|
||||
// _topLevelViewModel.Save();
|
||||
SettingsModel.SaveSettings(_settings);
|
||||
}
|
||||
|
||||
private void UnpinFromDock()
|
||||
{
|
||||
_settings.DockSettings.PinnedCommands.Remove(Id);
|
||||
_settings.DockSettings.StartBands.RemoveAll(band => band.Id == Id);
|
||||
_settings.DockSettings.CenterBands.RemoveAll(band => band.Id == Id);
|
||||
_settings.DockSettings.EndBands.RemoveAll(band => band.Id == Id);
|
||||
|
||||
// _topLevelViewModel.Save();
|
||||
SettingsModel.SaveSettings(_settings);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed partial class PinToDockCommand : InvokableCommand
|
||||
{
|
||||
public override string Name => "Toggle pinned to dock"; // TODO!LOC
|
||||
|
||||
public PinToDockCommand()
|
||||
{
|
||||
}
|
||||
|
||||
public override ICommandResult Invoke(object? sender)
|
||||
{
|
||||
if (sender is PinToDockItem pinItem)
|
||||
{
|
||||
pinItem.Invoke();
|
||||
return CommandResult.ShowToast($"Attempted to toggle pin to dock for {pinItem.Owner.Title}");
|
||||
}
|
||||
|
||||
return CommandResult.ShowToast($"Failed to get sender for command");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// 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.ComponentModel;
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Automation;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Controls;
|
||||
|
||||
public partial class CheckBoxWithDescriptionControl : CheckBox
|
||||
{
|
||||
private CheckBoxWithDescriptionControl _checkBoxSubTextControl;
|
||||
|
||||
public CheckBoxWithDescriptionControl()
|
||||
{
|
||||
_checkBoxSubTextControl = (CheckBoxWithDescriptionControl)this;
|
||||
this.Loaded += CheckBoxSubTextControl_Loaded;
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate()
|
||||
{
|
||||
Update();
|
||||
base.OnApplyTemplate();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(Header))
|
||||
{
|
||||
AutomationProperties.SetName(this, Header);
|
||||
}
|
||||
}
|
||||
|
||||
private void CheckBoxSubTextControl_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
StackPanel panel = new StackPanel() { Orientation = Orientation.Vertical };
|
||||
panel.Children.Add(new TextBlock() { Text = Header, TextWrapping = TextWrapping.WrapWholeWords });
|
||||
|
||||
// Add text box only if the description is not empty. Required for additional plugin options.
|
||||
if (!string.IsNullOrWhiteSpace(Description))
|
||||
{
|
||||
panel.Children.Add(new IsEnabledTextBlock() { Style = (Style)App.Current.Resources["SecondaryIsEnabledTextBlockStyle"], Text = Description });
|
||||
}
|
||||
|
||||
_checkBoxSubTextControl.Content = panel;
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty HeaderProperty = DependencyProperty.Register(
|
||||
"Header",
|
||||
typeof(string),
|
||||
typeof(CheckBoxWithDescriptionControl),
|
||||
new PropertyMetadata(default(string)));
|
||||
|
||||
public static readonly DependencyProperty DescriptionProperty = DependencyProperty.Register(
|
||||
"Description",
|
||||
typeof(string),
|
||||
typeof(CheckBoxWithDescriptionControl),
|
||||
new PropertyMetadata(default(string)));
|
||||
|
||||
[Localizable(true)]
|
||||
public string Header
|
||||
{
|
||||
get => (string)GetValue(HeaderProperty);
|
||||
set => SetValue(HeaderProperty, value);
|
||||
}
|
||||
|
||||
[Localizable(true)]
|
||||
public string Description
|
||||
{
|
||||
get => (string)GetValue(DescriptionProperty);
|
||||
set => SetValue(DescriptionProperty, value);
|
||||
}
|
||||
}
|
||||
@@ -98,7 +98,7 @@
|
||||
<Grid
|
||||
x:Name="IconRoot"
|
||||
Margin="3,0,-5,0"
|
||||
Visibility="{x:Bind CurrentPageViewModel.IsNested, Mode=OneWay}">
|
||||
Visibility="{x:Bind CurrentPageViewModel.IsRootPage, Mode=OneWay, Converter={StaticResource BoolToInvertedVisibilityConverter}}">
|
||||
<Button
|
||||
x:Name="StatusMessagesButton"
|
||||
x:Uid="StatusMessagesButton"
|
||||
@@ -135,7 +135,7 @@
|
||||
x:Uid="SettingsButton"
|
||||
Click="SettingsIcon_Clicked"
|
||||
Style="{StaticResource SubtleButtonStyle}"
|
||||
Visibility="{x:Bind CurrentPageViewModel.IsNested, Mode=OneWay, Converter={StaticResource BoolToInvertedVisibilityConverter}}">
|
||||
Visibility="{x:Bind CurrentPageViewModel.IsRootPage, Mode=OneWay}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<FontIcon
|
||||
VerticalAlignment="Center"
|
||||
@@ -154,7 +154,7 @@
|
||||
Text="{x:Bind CurrentPageViewModel.Title, Mode=OneWay}"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
TextWrapping="NoWrap"
|
||||
Visibility="{x:Bind CurrentPageViewModel.IsNested, Mode=OneWay}" />
|
||||
Visibility="{x:Bind CurrentPageViewModel.IsRootPage, Mode=OneWay, Converter={StaticResource BoolToInvertedVisibilityConverter}}" />
|
||||
<StackPanel
|
||||
Grid.Column="2"
|
||||
Padding="0,0,4,0"
|
||||
@@ -205,7 +205,7 @@
|
||||
TextWrapping="NoWrap" />
|
||||
<StackPanel Orientation="Horizontal" Spacing="4">
|
||||
<Border Padding="4,2,4,2" Style="{StaticResource HotkeyStyle}">
|
||||
<TextBlock x:Uid="CommandBar_SecondaryButton_HotkeyCtrl" Style="{StaticResource HotkeyTextBlockStyle}" />
|
||||
<TextBlock Style="{StaticResource HotkeyTextBlockStyle}" Text="Ctrl" />
|
||||
</Border>
|
||||
<Border Style="{StaticResource HotkeyStyle}">
|
||||
<FontIcon Glyph="" Style="{StaticResource HotkeyFontIconStyle}" />
|
||||
@@ -220,20 +220,21 @@
|
||||
AutomationProperties.AutomationId="MoreContextMenuButton"
|
||||
Click="MoreCommandsButton_Clicked"
|
||||
Style="{StaticResource SubtleButtonStyle}"
|
||||
ToolTipService.ToolTip="Ctrl+K"
|
||||
Visibility="{x:Bind ViewModel.ShouldShowContextMenu, Mode=OneWay}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<TextBlock
|
||||
x:Uid="MoreCommandsButton_Label"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="More"
|
||||
TextTrimming="WordEllipsis"
|
||||
TextWrapping="NoWrap" />
|
||||
<StackPanel Orientation="Horizontal" Spacing="4">
|
||||
<Border Padding="4,2,4,2" Style="{StaticResource HotkeyStyle}">
|
||||
<TextBlock x:Uid="CommandBar_MoreCommandsButtonButton_HotkeyCtrl" Style="{StaticResource HotkeyTextBlockStyle}" />
|
||||
<TextBlock Style="{StaticResource HotkeyTextBlockStyle}" Text="Ctrl" />
|
||||
</Border>
|
||||
<Border Padding="4,2,4,2" Style="{StaticResource HotkeyStyle}">
|
||||
<TextBlock x:Uid="CommandBar_MoreCommandsButtonButton_HotkeyCtrl2" Style="{StaticResource HotkeyTextBlockStyle}" />
|
||||
<TextBlock Style="{StaticResource HotkeyTextBlockStyle}" Text="K" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<ResourceDictionary
|
||||
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">
|
||||
|
||||
<Style x:Key="DefaultIsEnabledTextBlockStyle" TargetType="controls:IsEnabledTextBlock">
|
||||
<Setter Property="Foreground" Value="{ThemeResource DefaultTextForegroundThemeBrush}" />
|
||||
<Setter Property="IsTabStop" Value="False" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="controls:IsEnabledTextBlock">
|
||||
<Grid>
|
||||
<TextBlock
|
||||
x:Name="Label"
|
||||
FontFamily="{TemplateBinding FontFamily}"
|
||||
FontSize="{TemplateBinding FontSize}"
|
||||
FontWeight="{TemplateBinding FontWeight}"
|
||||
Foreground="{TemplateBinding Foreground}"
|
||||
Text="{TemplateBinding Text}"
|
||||
TextWrapping="WrapWholeWords" />
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="Disabled">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="Label.Foreground" Value="{ThemeResource TextFillColorDisabledBrush}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
<Style
|
||||
x:Key="SecondaryIsEnabledTextBlockStyle"
|
||||
BasedOn="{StaticResource DefaultIsEnabledTextBlockStyle}"
|
||||
TargetType="controls:IsEnabledTextBlock">
|
||||
<Setter Property="Foreground" Value="{ThemeResource TextFillColorSecondaryBrush}" />
|
||||
<Setter Property="FontSize" Value="12" />
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
@@ -0,0 +1,51 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.ComponentModel;
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Controls;
|
||||
|
||||
[TemplateVisualState(Name = "Normal", GroupName = "CommonStates")]
|
||||
[TemplateVisualState(Name = "Disabled", GroupName = "CommonStates")]
|
||||
public partial class IsEnabledTextBlock : Control
|
||||
{
|
||||
public IsEnabledTextBlock()
|
||||
{
|
||||
this.Style = (Style)App.Current.Resources["DefaultIsEnabledTextBlockStyle"];
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate()
|
||||
{
|
||||
IsEnabledChanged -= IsEnabledTextBlock_IsEnabledChanged;
|
||||
SetEnabledState();
|
||||
IsEnabledChanged += IsEnabledTextBlock_IsEnabledChanged;
|
||||
base.OnApplyTemplate();
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty TextProperty = DependencyProperty.Register(
|
||||
"Text",
|
||||
typeof(string),
|
||||
typeof(IsEnabledTextBlock),
|
||||
null);
|
||||
|
||||
[Localizable(true)]
|
||||
public string Text
|
||||
{
|
||||
get => (string)GetValue(TextProperty);
|
||||
set => SetValue(TextProperty, value);
|
||||
}
|
||||
|
||||
private void IsEnabledTextBlock_IsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
SetEnabledState();
|
||||
}
|
||||
|
||||
private void SetEnabledState()
|
||||
{
|
||||
VisualStateManager.GoToState(this, IsEnabled ? "Normal" : "Disabled", true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Markup;
|
||||
using Windows.System;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Controls;
|
||||
|
||||
[TemplatePart(Name = KeyPresenter, Type = typeof(ContentPresenter))]
|
||||
[TemplateVisualState(Name = "Normal", GroupName = "CommonStates")]
|
||||
[TemplateVisualState(Name = "Disabled", GroupName = "CommonStates")]
|
||||
[TemplateVisualState(Name = "Default", GroupName = "StateStates")]
|
||||
[TemplateVisualState(Name = "Error", GroupName = "StateStates")]
|
||||
public sealed partial class KeyVisual : Control
|
||||
{
|
||||
private const string KeyPresenter = "KeyPresenter";
|
||||
private KeyVisual? _keyVisual;
|
||||
private ContentPresenter _keyPresenter = new();
|
||||
|
||||
public object Content
|
||||
{
|
||||
get => GetValue(ContentProperty);
|
||||
set => SetValue(ContentProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty ContentProperty = DependencyProperty.Register("Content", typeof(object), typeof(KeyVisual), new PropertyMetadata(default(string), OnContentChanged));
|
||||
|
||||
public VisualType VisualType
|
||||
{
|
||||
get => (VisualType)GetValue(VisualTypeProperty);
|
||||
set => SetValue(VisualTypeProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty VisualTypeProperty = DependencyProperty.Register("VisualType", typeof(VisualType), typeof(KeyVisual), new PropertyMetadata(default(VisualType), OnSizeChanged));
|
||||
|
||||
public bool IsError
|
||||
{
|
||||
get => (bool)GetValue(IsErrorProperty);
|
||||
set => SetValue(IsErrorProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty IsErrorProperty = DependencyProperty.Register("IsError", typeof(bool), typeof(KeyVisual), new PropertyMetadata(false, OnIsErrorChanged));
|
||||
|
||||
public KeyVisual()
|
||||
{
|
||||
this.DefaultStyleKey = typeof(KeyVisual);
|
||||
this.Style = GetStyleSize("TextKeyVisualStyle");
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate()
|
||||
{
|
||||
IsEnabledChanged -= KeyVisual_IsEnabledChanged;
|
||||
_keyVisual = this;
|
||||
_keyPresenter = (ContentPresenter)_keyVisual.GetTemplateChild(KeyPresenter);
|
||||
Update();
|
||||
SetEnabledState();
|
||||
SetErrorState();
|
||||
IsEnabledChanged += KeyVisual_IsEnabledChanged;
|
||||
base.OnApplyTemplate();
|
||||
}
|
||||
|
||||
private static void OnContentChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
((KeyVisual)d).Update();
|
||||
}
|
||||
|
||||
private static void OnSizeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
((KeyVisual)d).Update();
|
||||
}
|
||||
|
||||
private static void OnIsErrorChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
((KeyVisual)d).SetErrorState();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (_keyVisual is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_keyVisual.Content is not null)
|
||||
{
|
||||
if (_keyVisual.Content.GetType() == typeof(string))
|
||||
{
|
||||
_keyVisual.Style = GetStyleSize("TextKeyVisualStyle");
|
||||
_keyVisual._keyPresenter.Content = _keyVisual.Content;
|
||||
}
|
||||
else
|
||||
{
|
||||
_keyVisual.Style = GetStyleSize("IconKeyVisualStyle");
|
||||
|
||||
switch ((int)_keyVisual.Content)
|
||||
{
|
||||
/* We can enable other glyphs in the future
|
||||
case 13: // The Enter key or button.
|
||||
_keyVisual._keyPresenter.Content = "\uE751"; break;
|
||||
|
||||
case 8: // The Back key or button.
|
||||
_keyVisual._keyPresenter.Content = "\uE750"; break;
|
||||
|
||||
case 16: // The right Shift key or button.
|
||||
case 160: // The left Shift key or button.
|
||||
case 161: // The Shift key or button.
|
||||
_keyVisual._keyPresenter.Content = "\uE752"; break; */
|
||||
|
||||
case 38: _keyVisual._keyPresenter.Content = "\uE0E4"; break; // The Up Arrow key or button.
|
||||
case 40: _keyVisual._keyPresenter.Content = "\uE0E5"; break; // The Down Arrow key or button.
|
||||
case 37: _keyVisual._keyPresenter.Content = "\uE0E2"; break; // The Left Arrow key or button.
|
||||
case 39: _keyVisual._keyPresenter.Content = "\uE0E3"; break; // The Right Arrow key or button.
|
||||
|
||||
case 91: // The left Windows key
|
||||
case 92: // The right Windows key
|
||||
var winIcon = XamlReader.Load(@"<PathIcon xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation"" Data=""M683 1229H0V546h683v683zm819 0H819V546h683v683zm-819 819H0v-683h683v683zm819 0H819v-683h683v683z"" />") as PathIcon;
|
||||
var winIconContainer = new Viewbox
|
||||
{
|
||||
Child = winIcon,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
|
||||
var iconDimensions = GetIconSize();
|
||||
winIconContainer.Height = iconDimensions;
|
||||
winIconContainer.Width = iconDimensions;
|
||||
_keyVisual._keyPresenter.Content = winIconContainer;
|
||||
break;
|
||||
default: _keyVisual._keyPresenter.Content = ((VirtualKey)_keyVisual.Content).ToString(); break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Style GetStyleSize(string styleName)
|
||||
{
|
||||
return VisualType == VisualType.Small
|
||||
? (Style)App.Current.Resources["Small" + styleName]
|
||||
: VisualType == VisualType.SmallOutline
|
||||
? (Style)App.Current.Resources["SmallOutline" + styleName]
|
||||
: VisualType == VisualType.TextOnly
|
||||
? (Style)App.Current.Resources["Only" + styleName]
|
||||
: (Style)App.Current.Resources["Default" + styleName];
|
||||
}
|
||||
|
||||
public double GetIconSize()
|
||||
{
|
||||
return VisualType == VisualType.Small || VisualType == VisualType.SmallOutline
|
||||
? (double)App.Current.Resources["SmallIconSize"]
|
||||
: (double)App.Current.Resources["DefaultIconSize"];
|
||||
}
|
||||
|
||||
private void KeyVisual_IsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
SetEnabledState();
|
||||
}
|
||||
|
||||
private void SetErrorState()
|
||||
{
|
||||
VisualStateManager.GoToState(this, IsError ? "Error" : "Default", true);
|
||||
}
|
||||
|
||||
private void SetEnabledState()
|
||||
{
|
||||
VisualStateManager.GoToState(this, IsEnabled ? "Normal" : "Disabled", true);
|
||||
}
|
||||
}
|
||||
|
||||
public enum VisualType
|
||||
{
|
||||
Small,
|
||||
SmallOutline,
|
||||
TextOnly,
|
||||
Large,
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
<ResourceDictionary
|
||||
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">
|
||||
|
||||
<x:Double x:Key="DefaultIconSize">16</x:Double>
|
||||
<x:Double x:Key="SmallIconSize">12</x:Double>
|
||||
<Style x:Key="DefaultTextKeyVisualStyle" TargetType="controls:KeyVisual">
|
||||
<Setter Property="MinWidth" Value="56" />
|
||||
<Setter Property="MinHeight" Value="48" />
|
||||
<Setter Property="Background" Value="{ThemeResource AccentButtonBackground}" />
|
||||
<Setter Property="Foreground" Value="{ThemeResource AccentButtonForeground}" />
|
||||
<Setter Property="BorderBrush" Value="{ThemeResource AccentButtonBorderBrush}" />
|
||||
<Setter Property="BorderThickness" Value="{ThemeResource ButtonBorderThemeThickness}" />
|
||||
<Setter Property="Padding" Value="16,8,16,8" />
|
||||
<Setter Property="FontWeight" Value="SemiBold" />
|
||||
<Setter Property="HorizontalAlignment" Value="Center" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center" />
|
||||
<Setter Property="FontSize" Value="18" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="controls:KeyVisual">
|
||||
<Grid>
|
||||
<Grid>
|
||||
<Rectangle
|
||||
x:Name="ContentHolder"
|
||||
Height="{TemplateBinding Height}"
|
||||
MinWidth="{TemplateBinding MinWidth}"
|
||||
Fill="{TemplateBinding Background}"
|
||||
RadiusX="4"
|
||||
RadiusY="4"
|
||||
Stroke="{TemplateBinding BorderBrush}"
|
||||
StrokeThickness="{TemplateBinding BorderThickness}" />
|
||||
<ContentPresenter
|
||||
x:Name="KeyPresenter"
|
||||
Margin="{TemplateBinding Padding}"
|
||||
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalAlignment="Center"
|
||||
Content="{TemplateBinding Content}"
|
||||
FontSize="{TemplateBinding FontSize}"
|
||||
FontWeight="{TemplateBinding FontWeight}"
|
||||
Foreground="{TemplateBinding Foreground}" />
|
||||
</Grid>
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="Disabled">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="ContentHolder.Fill" Value="{ThemeResource AccentButtonBackgroundDisabled}" />
|
||||
<Setter Target="KeyPresenter.Foreground" Value="{ThemeResource AccentButtonForegroundDisabled}" />
|
||||
<Setter Target="ContentHolder.Stroke" Value="{ThemeResource AccentButtonBorderBrushDisabled}" />
|
||||
<!--<Setter Target="ContentHolder.StrokeThickness" Value="{TemplateBinding BorderThickness}" />-->
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
<VisualStateGroup x:Name="StateStates">
|
||||
<VisualState x:Name="Default" />
|
||||
<VisualState x:Name="Error">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="ContentHolder.Fill" Value="{ThemeResource InfoBarErrorSeverityBackgroundBrush}" />
|
||||
<Setter Target="KeyPresenter.Foreground" Value="{ThemeResource InfoBarErrorSeverityIconBackground}" />
|
||||
<Setter Target="ContentHolder.Stroke" Value="{ThemeResource InfoBarErrorSeverityIconBackground}" />
|
||||
<Setter Target="ContentHolder.StrokeThickness" Value="2" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style
|
||||
x:Key="SmallTextKeyVisualStyle"
|
||||
BasedOn="{StaticResource DefaultTextKeyVisualStyle}"
|
||||
TargetType="controls:KeyVisual">
|
||||
<Setter Property="MinWidth" Value="40" />
|
||||
<Setter Property="Height" Value="36" />
|
||||
<Setter Property="FontWeight" Value="SemiBold" />
|
||||
<Setter Property="Padding" Value="12,0,12,2" />
|
||||
<Setter Property="FontSize" Value="14" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center" />
|
||||
</Style>
|
||||
|
||||
<Style
|
||||
x:Key="SmallOutlineTextKeyVisualStyle"
|
||||
BasedOn="{StaticResource DefaultTextKeyVisualStyle}"
|
||||
TargetType="controls:KeyVisual">
|
||||
<Setter Property="MinWidth" Value="40" />
|
||||
<Setter Property="Background" Value="{ThemeResource ButtonBackground}" />
|
||||
<Setter Property="Foreground" Value="{ThemeResource ButtonForeground}" />
|
||||
<Setter Property="BorderBrush" Value="{ThemeResource ButtonBorderBrush}" />
|
||||
<Setter Property="Height" Value="36" />
|
||||
<Setter Property="FontWeight" Value="SemiBold" />
|
||||
<Setter Property="Padding" Value="8,0,8,2" />
|
||||
<Setter Property="FontSize" Value="13" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center" />
|
||||
</Style>
|
||||
|
||||
|
||||
|
||||
<Style
|
||||
x:Key="DefaultIconKeyVisualStyle"
|
||||
BasedOn="{StaticResource DefaultTextKeyVisualStyle}"
|
||||
TargetType="controls:KeyVisual">
|
||||
<Setter Property="MinWidth" Value="56" />
|
||||
<Setter Property="MinHeight" Value="48" />
|
||||
<Setter Property="FontFamily" Value="{ThemeResource SymbolThemeFontFamily}" />
|
||||
<Setter Property="Padding" Value="16,8,16,8" />
|
||||
<Setter Property="FontSize" Value="14" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center" />
|
||||
</Style>
|
||||
|
||||
<Style
|
||||
x:Key="SmallIconKeyVisualStyle"
|
||||
BasedOn="{StaticResource DefaultTextKeyVisualStyle}"
|
||||
TargetType="controls:KeyVisual">
|
||||
<Setter Property="MinWidth" Value="40" />
|
||||
<Setter Property="Height" Value="36" />
|
||||
<Setter Property="FontFamily" Value="{ThemeResource SymbolThemeFontFamily}" />
|
||||
<Setter Property="FontWeight" Value="Normal" />
|
||||
<Setter Property="Padding" Value="0" />
|
||||
<Setter Property="FontSize" Value="10" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center" />
|
||||
</Style>
|
||||
|
||||
<Style
|
||||
x:Key="SmallOutlineIconKeyVisualStyle"
|
||||
BasedOn="{StaticResource DefaultTextKeyVisualStyle}"
|
||||
TargetType="controls:KeyVisual">
|
||||
<Setter Property="MinWidth" Value="40" />
|
||||
<Setter Property="Background" Value="{ThemeResource ButtonBackground}" />
|
||||
<Setter Property="Foreground" Value="{ThemeResource ButtonForeground}" />
|
||||
<Setter Property="BorderBrush" Value="{ThemeResource ButtonBorderBrush}" />
|
||||
<Setter Property="FontFamily" Value="{ThemeResource SymbolThemeFontFamily}" />
|
||||
<Setter Property="Height" Value="36" />
|
||||
<Setter Property="FontWeight" Value="SemiBold" />
|
||||
<Setter Property="Padding" Value="0" />
|
||||
<Setter Property="FontSize" Value="9" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center" />
|
||||
</Style>
|
||||
|
||||
<Style
|
||||
x:Key="OnlyTextKeyVisualStyle"
|
||||
BasedOn="{StaticResource DefaultTextKeyVisualStyle}"
|
||||
TargetType="controls:KeyVisual">
|
||||
<Setter Property="MinHeight" Value="12" />
|
||||
<Setter Property="MinWidth" Value="12" />
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="Foreground" Value="{ThemeResource ButtonForeground}" />
|
||||
<Setter Property="BorderBrush" Value="Transparent" />
|
||||
<Setter Property="FontWeight" Value="Normal" />
|
||||
<Setter Property="Padding" Value="0" />
|
||||
<Setter Property="FontSize" Value="12" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center" />
|
||||
</Style>
|
||||
|
||||
<Style
|
||||
x:Key="OnlyIconKeyVisualStyle"
|
||||
BasedOn="{StaticResource DefaultTextKeyVisualStyle}"
|
||||
TargetType="controls:KeyVisual">
|
||||
<Setter Property="MinHeight" Value="10" />
|
||||
<Setter Property="MinWidth" Value="10" />
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="Foreground" Value="{ThemeResource ButtonForeground}" />
|
||||
<Setter Property="BorderBrush" Value="Transparent" />
|
||||
<Setter Property="FontFamily" Value="{ThemeResource SymbolThemeFontFamily}" />
|
||||
<Setter Property="FontWeight" Value="Normal" />
|
||||
<Setter Property="Padding" Value="0,0,0,3" />
|
||||
<!--<Setter Property="FontSize" Value="9" />-->
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center" />
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
@@ -0,0 +1,302 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<UserControl
|
||||
x:Class="Microsoft.CmdPal.UI.Controls.ScrollContainer"
|
||||
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:local="using:Microsoft.CmdPal.UI.Controls"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d">
|
||||
<UserControl.Resources>
|
||||
<ResourceDictionary>
|
||||
<Style x:Key="ScrollButtonStyle" TargetType="Button">
|
||||
<Setter Property="Background" Value="{ThemeResource FlipViewNextPreviousButtonBackground}" />
|
||||
<Setter Property="BackgroundSizing" Value="InnerBorderEdge" />
|
||||
<Setter Property="Foreground" Value="{ThemeResource ButtonForeground}" />
|
||||
<Setter Property="BorderBrush" Value="{ThemeResource FlipViewNextPreviousButtonBorderBrush}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="Padding" Value="0" />
|
||||
<Setter Property="HorizontalAlignment" Value="Left" />
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
<Setter Property="FontFamily" Value="{ThemeResource ContentControlThemeFontFamily}" />
|
||||
<Setter Property="FontWeight" Value="Normal" />
|
||||
<Setter Property="FontSize" Value="{ThemeResource ControlContentThemeFontSize}" />
|
||||
<Setter Property="UseSystemFocusVisuals" Value="{StaticResource UseSystemFocusVisuals}" />
|
||||
<Setter Property="FocusVisualMargin" Value="-3" />
|
||||
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<ContentPresenter
|
||||
x:Name="ContentPresenter"
|
||||
Padding="{TemplateBinding Padding}"
|
||||
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||
AnimatedIcon.State="Normal"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
Background="{TemplateBinding Background}"
|
||||
BackgroundSizing="{TemplateBinding BackgroundSizing}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
Content="{TemplateBinding Content}"
|
||||
ContentTemplate="{TemplateBinding ContentTemplate}"
|
||||
ContentTransitions="{TemplateBinding ContentTransitions}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}">
|
||||
<ContentPresenter.BackgroundTransition>
|
||||
<BrushTransition Duration="0:0:0.083" />
|
||||
</ContentPresenter.BackgroundTransition>
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="PointerOver">
|
||||
<Storyboard>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource FlipViewNextPreviousButtonBackgroundPointerOver}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource FlipViewNextPreviousButtonBorderBrushPointerOver}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource FlipViewNextPreviousArrowForegroundPointerOver}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
<VisualState.Setters>
|
||||
<Setter Target="ContentPresenter.(AnimatedIcon.State)" Value="PointerOver" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="Pressed">
|
||||
<Storyboard>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource FlipViewNextPreviousButtonBackgroundPressed}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource FlipViewNextPreviousButtonBorderBrushPressed}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource FlipViewNextPreviousArrowForegroundPressed}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
<VisualState.Setters>
|
||||
<Setter Target="ContentPresenter.(AnimatedIcon.State)" Value="Pressed" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="Disabled">
|
||||
<Storyboard>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ButtonBackgroundDisabled}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ButtonBorderBrushDisabled}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ButtonForegroundDisabled}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
<VisualState.Setters>
|
||||
<!-- DisabledVisual Should be handled by the control, not the animated icon. -->
|
||||
<Setter Target="ContentPresenter.(AnimatedIcon.State)" Value="Normal" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
</ContentPresenter>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
</UserControl.Resources>
|
||||
<Grid
|
||||
x:Name="RootGrid"
|
||||
Background="{x:Bind Background, Mode=OneWay}"
|
||||
BorderBrush="{x:Bind BorderBrush, Mode=OneWay}"
|
||||
BorderThickness="{x:Bind BorderThickness, Mode=OneWay}"
|
||||
CornerRadius="{x:Bind CornerRadius, Mode=OneWay}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition x:Name="Row2" Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Action button - position controlled by visual states -->
|
||||
<ContentPresenter
|
||||
x:Name="ActionButtonPresenter"
|
||||
Grid.Row="0"
|
||||
Grid.RowSpan="3"
|
||||
Grid.Column="2"
|
||||
Margin="4,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
Content="{x:Bind ActionButton, Mode=OneWay}"
|
||||
Visibility="{x:Bind ActionButtonVisibility, Mode=OneWay}" />
|
||||
|
||||
<Grid
|
||||
x:Name="ScrollerContainer"
|
||||
Grid.Row="0"
|
||||
Grid.RowSpan="3"
|
||||
Grid.Column="1">
|
||||
<ScrollViewer
|
||||
x:Name="scroller"
|
||||
HorizontalScrollBarVisibility="Hidden"
|
||||
HorizontalScrollMode="Enabled"
|
||||
SizeChanged="Scroller_SizeChanged"
|
||||
VerticalScrollBarVisibility="Hidden"
|
||||
VerticalScrollMode="Disabled"
|
||||
ViewChanging="Scroller_ViewChanging">
|
||||
<Grid x:Name="ContentGrid">
|
||||
<ContentPresenter Content="{x:Bind Source, Mode=OneWay}" />
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
<Button
|
||||
x:Name="ScrollBackBtn"
|
||||
Margin="8,0,0,0"
|
||||
Padding="2,8,2,8"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
AutomationProperties.Name="Scroll left"
|
||||
Click="ScrollBackBtn_Click"
|
||||
Style="{StaticResource ScrollButtonStyle}"
|
||||
ToolTipService.ToolTip="Scroll left"
|
||||
Visibility="Collapsed">
|
||||
<FontIcon
|
||||
x:Name="ScrollBackIcon"
|
||||
FontSize="{ThemeResource FlipViewButtonFontSize}"
|
||||
Glyph="" />
|
||||
</Button>
|
||||
<Button
|
||||
x:Name="ScrollForwardBtn"
|
||||
Margin="0,0,8,0"
|
||||
Padding="2,8,2,8"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
AutomationProperties.Name="Scroll right"
|
||||
Click="ScrollForwardBtn_Click"
|
||||
Style="{StaticResource ScrollButtonStyle}"
|
||||
ToolTipService.ToolTip="Scroll right">
|
||||
<FontIcon
|
||||
x:Name="ScrollForwardIcon"
|
||||
FontSize="{ThemeResource FlipViewButtonFontSize}"
|
||||
Glyph="" />
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup x:Name="OrientationStates">
|
||||
<VisualState x:Name="HorizontalState">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="scroller.HorizontalScrollBarVisibility" Value="Hidden" />
|
||||
<Setter Target="scroller.HorizontalScrollMode" Value="Enabled" />
|
||||
<Setter Target="scroller.VerticalScrollBarVisibility" Value="Hidden" />
|
||||
<Setter Target="scroller.VerticalScrollMode" Value="Disabled" />
|
||||
<Setter Target="ScrollBackBtn.Padding" Value="4,12,4,12" />
|
||||
<Setter Target="ScrollBackBtn.Margin" Value="8,0,0,0" />
|
||||
<Setter Target="ScrollBackBtn.HorizontalAlignment" Value="Left" />
|
||||
<Setter Target="ScrollBackBtn.VerticalAlignment" Value="Center" />
|
||||
<Setter Target="ScrollBackBtn.(AutomationProperties.Name)" Value="Scroll left" />
|
||||
<Setter Target="ScrollBackBtn.(ToolTipService.ToolTip)" Value="Scroll left" />
|
||||
<Setter Target="ScrollBackIcon.Glyph" Value="" />
|
||||
<Setter Target="ScrollForwardBtn.Padding" Value="4,12,4,12" />
|
||||
<Setter Target="ScrollForwardBtn.Margin" Value="0,0,8,0" />
|
||||
<Setter Target="ScrollForwardBtn.HorizontalAlignment" Value="Right" />
|
||||
<Setter Target="ScrollForwardBtn.VerticalAlignment" Value="Center" />
|
||||
<Setter Target="ScrollForwardBtn.(AutomationProperties.Name)" Value="Scroll right" />
|
||||
<Setter Target="ScrollForwardBtn.(ToolTipService.ToolTip)" Value="Scroll right" />
|
||||
<Setter Target="ScrollForwardIcon.Glyph" Value="" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="VerticalState">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="scroller.HorizontalScrollBarVisibility" Value="Hidden" />
|
||||
<Setter Target="scroller.HorizontalScrollMode" Value="Disabled" />
|
||||
<Setter Target="scroller.VerticalScrollBarVisibility" Value="Hidden" />
|
||||
<Setter Target="scroller.VerticalScrollMode" Value="Enabled" />
|
||||
<Setter Target="ScrollBackBtn.Padding" Value="12,4,12,4" />
|
||||
<Setter Target="ScrollBackBtn.Margin" Value="0,8,0,0" />
|
||||
<Setter Target="ScrollBackBtn.HorizontalAlignment" Value="Center" />
|
||||
<Setter Target="ScrollBackBtn.VerticalAlignment" Value="Top" />
|
||||
<Setter Target="ScrollBackBtn.(AutomationProperties.Name)" Value="Scroll up" />
|
||||
<Setter Target="ScrollBackBtn.(ToolTipService.ToolTip)" Value="Scroll up" />
|
||||
<Setter Target="ScrollBackIcon.Glyph" Value="" />
|
||||
<Setter Target="ScrollForwardBtn.Padding" Value="12,4,12,4" />
|
||||
<Setter Target="ScrollForwardBtn.Margin" Value="0,0,0,8" />
|
||||
<Setter Target="ScrollForwardBtn.HorizontalAlignment" Value="Center" />
|
||||
<Setter Target="ScrollForwardBtn.VerticalAlignment" Value="Bottom" />
|
||||
<Setter Target="ScrollForwardBtn.(AutomationProperties.Name)" Value="Scroll down" />
|
||||
<Setter Target="ScrollForwardBtn.(ToolTipService.ToolTip)" Value="Scroll down" />
|
||||
<Setter Target="ScrollForwardIcon.Glyph" Value="" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
<VisualStateGroup x:Name="LayoutStates">
|
||||
<!-- Horizontal + Start: button on right -->
|
||||
<VisualState x:Name="HorizontalStartState">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="ScrollerContainer.(Grid.Row)" Value="0" />
|
||||
<Setter Target="ScrollerContainer.(Grid.RowSpan)" Value="3" />
|
||||
<Setter Target="ScrollerContainer.(Grid.Column)" Value="1" />
|
||||
<Setter Target="ScrollerContainer.(Grid.ColumnSpan)" Value="1" />
|
||||
<Setter Target="ActionButtonPresenter.(Grid.Row)" Value="0" />
|
||||
<Setter Target="ActionButtonPresenter.(Grid.RowSpan)" Value="3" />
|
||||
<Setter Target="ActionButtonPresenter.(Grid.Column)" Value="2" />
|
||||
<Setter Target="ActionButtonPresenter.(Grid.ColumnSpan)" Value="1" />
|
||||
<Setter Target="ActionButtonPresenter.Margin" Value="4,0,0,0" />
|
||||
<Setter Target="ActionButtonPresenter.HorizontalAlignment" Value="Stretch" />
|
||||
<Setter Target="ActionButtonPresenter.VerticalAlignment" Value="Center" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<!-- Horizontal + End: button on left -->
|
||||
<VisualState x:Name="HorizontalEndState">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="ScrollerContainer.(Grid.Row)" Value="0" />
|
||||
<Setter Target="ScrollerContainer.(Grid.RowSpan)" Value="3" />
|
||||
<Setter Target="ScrollerContainer.(Grid.Column)" Value="1" />
|
||||
<Setter Target="ScrollerContainer.(Grid.ColumnSpan)" Value="1" />
|
||||
<Setter Target="ActionButtonPresenter.(Grid.Row)" Value="0" />
|
||||
<Setter Target="ActionButtonPresenter.(Grid.RowSpan)" Value="3" />
|
||||
<Setter Target="ActionButtonPresenter.(Grid.Column)" Value="0" />
|
||||
<Setter Target="ActionButtonPresenter.(Grid.ColumnSpan)" Value="1" />
|
||||
<Setter Target="ActionButtonPresenter.Margin" Value="0,0,4,0" />
|
||||
<Setter Target="ActionButtonPresenter.HorizontalAlignment" Value="Stretch" />
|
||||
<Setter Target="ActionButtonPresenter.VerticalAlignment" Value="Center" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<!-- Vertical + Start: button on bottom -->
|
||||
<VisualState x:Name="VerticalStartState">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="ScrollerContainer.(Grid.Row)" Value="1" />
|
||||
<Setter Target="ScrollerContainer.(Grid.RowSpan)" Value="1" />
|
||||
<Setter Target="ScrollerContainer.(Grid.Column)" Value="0" />
|
||||
<Setter Target="ScrollerContainer.(Grid.ColumnSpan)" Value="3" />
|
||||
<Setter Target="ActionButtonPresenter.(Grid.Row)" Value="2" />
|
||||
<Setter Target="ActionButtonPresenter.(Grid.RowSpan)" Value="1" />
|
||||
<Setter Target="ActionButtonPresenter.(Grid.Column)" Value="0" />
|
||||
<Setter Target="ActionButtonPresenter.(Grid.ColumnSpan)" Value="3" />
|
||||
<Setter Target="ActionButtonPresenter.Margin" Value="0,4,0,0" />
|
||||
<Setter Target="ActionButtonPresenter.HorizontalAlignment" Value="Center" />
|
||||
<Setter Target="ActionButtonPresenter.VerticalAlignment" Value="Stretch" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<!-- Vertical + End: button on top -->
|
||||
<VisualState x:Name="VerticalEndState">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="ScrollerContainer.(Grid.Row)" Value="1" />
|
||||
<Setter Target="ScrollerContainer.(Grid.RowSpan)" Value="1" />
|
||||
<Setter Target="ScrollerContainer.(Grid.Column)" Value="0" />
|
||||
<Setter Target="ScrollerContainer.(Grid.ColumnSpan)" Value="3" />
|
||||
<Setter Target="ActionButtonPresenter.(Grid.Row)" Value="0" />
|
||||
<Setter Target="ActionButtonPresenter.(Grid.RowSpan)" Value="1" />
|
||||
<Setter Target="ActionButtonPresenter.(Grid.Column)" Value="0" />
|
||||
<Setter Target="ActionButtonPresenter.(Grid.ColumnSpan)" Value="3" />
|
||||
<Setter Target="ActionButtonPresenter.Margin" Value="0,0,0,4" />
|
||||
<Setter Target="ActionButtonPresenter.HorizontalAlignment" Value="Center" />
|
||||
<Setter Target="ActionButtonPresenter.VerticalAlignment" Value="Stretch" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,222 @@
|
||||
// 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.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices.WindowsRuntime;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Controls.Primitives;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Microsoft.UI.Xaml.Navigation;
|
||||
using Windows.Foundation;
|
||||
using Windows.Foundation.Collections;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Controls;
|
||||
|
||||
public sealed partial class ScrollContainer : UserControl
|
||||
{
|
||||
public enum ScrollContentAlignment
|
||||
{
|
||||
Start,
|
||||
End,
|
||||
}
|
||||
|
||||
public ScrollContainer()
|
||||
{
|
||||
InitializeComponent();
|
||||
Loaded += ScrollContainer_Loaded;
|
||||
}
|
||||
|
||||
private void ScrollContainer_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
UpdateOrientationState();
|
||||
UpdateLayoutState();
|
||||
}
|
||||
|
||||
public object Source
|
||||
{
|
||||
get => (object)GetValue(SourceProperty);
|
||||
set => SetValue(SourceProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty SourceProperty =
|
||||
DependencyProperty.Register(nameof(Source), typeof(object), typeof(ScrollContainer), new PropertyMetadata(null));
|
||||
|
||||
public Orientation Orientation
|
||||
{
|
||||
get => (Orientation)GetValue(OrientationProperty);
|
||||
set => SetValue(OrientationProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty OrientationProperty =
|
||||
DependencyProperty.Register(nameof(Orientation), typeof(Orientation), typeof(ScrollContainer), new PropertyMetadata(Orientation.Horizontal, OnOrientationChanged));
|
||||
|
||||
public ScrollContentAlignment ContentAlignment
|
||||
{
|
||||
get => (ScrollContentAlignment)GetValue(ContentAlignmentProperty);
|
||||
set => SetValue(ContentAlignmentProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty ContentAlignmentProperty =
|
||||
DependencyProperty.Register(nameof(ContentAlignment), typeof(ScrollContentAlignment), typeof(ScrollContainer), new PropertyMetadata(ScrollContentAlignment.Start, OnContentAlignmentChanged));
|
||||
|
||||
public object ActionButton
|
||||
{
|
||||
get => (object)GetValue(ActionButtonProperty);
|
||||
set => SetValue(ActionButtonProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty ActionButtonProperty =
|
||||
DependencyProperty.Register(nameof(ActionButton), typeof(object), typeof(ScrollContainer), new PropertyMetadata(null));
|
||||
|
||||
public Visibility ActionButtonVisibility
|
||||
{
|
||||
get => (Visibility)GetValue(ActionButtonVisibilityProperty);
|
||||
set => SetValue(ActionButtonVisibilityProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty ActionButtonVisibilityProperty =
|
||||
DependencyProperty.Register(nameof(ActionButtonVisibility), typeof(Visibility), typeof(ScrollContainer), new PropertyMetadata(Visibility.Collapsed));
|
||||
|
||||
private static void OnContentAlignmentChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is ScrollContainer control)
|
||||
{
|
||||
control.UpdateLayoutState();
|
||||
control.ScrollToAlignment();
|
||||
}
|
||||
}
|
||||
|
||||
private void ScrollToAlignment()
|
||||
{
|
||||
// Reset button visibility
|
||||
ScrollBackBtn.Visibility = Visibility.Collapsed;
|
||||
ScrollForwardBtn.Visibility = Visibility.Collapsed;
|
||||
|
||||
if (ContentAlignment == ScrollContentAlignment.End)
|
||||
{
|
||||
// Scroll to the end
|
||||
if (Orientation == Orientation.Horizontal)
|
||||
{
|
||||
scroller.ChangeView(scroller.ScrollableWidth, null, null, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
scroller.ChangeView(null, scroller.ScrollableHeight, null, true);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Scroll to the beginning
|
||||
scroller.ChangeView(0, 0, null, true);
|
||||
}
|
||||
|
||||
// Defer visibility update until after layout
|
||||
void OnLayoutUpdated(object? sender, object args)
|
||||
{
|
||||
scroller.LayoutUpdated -= OnLayoutUpdated;
|
||||
UpdateScrollButtonsVisibility();
|
||||
}
|
||||
|
||||
scroller.LayoutUpdated += OnLayoutUpdated;
|
||||
}
|
||||
|
||||
private static void OnOrientationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is ScrollContainer control)
|
||||
{
|
||||
control.UpdateOrientationState();
|
||||
control.UpdateLayoutState();
|
||||
control.ScrollToAlignment();
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateOrientationState()
|
||||
{
|
||||
var stateName = Orientation == Orientation.Horizontal ? "HorizontalState" : "VerticalState";
|
||||
VisualStateManager.GoToState(this, stateName, true);
|
||||
}
|
||||
|
||||
private void UpdateLayoutState()
|
||||
{
|
||||
var isHorizontal = Orientation == Orientation.Horizontal;
|
||||
var isStart = ContentAlignment == ScrollContentAlignment.Start;
|
||||
|
||||
var stateName = (isHorizontal, isStart) switch
|
||||
{
|
||||
(true, true) => "HorizontalStartState",
|
||||
(true, false) => "HorizontalEndState",
|
||||
(false, true) => "VerticalStartState",
|
||||
(false, false) => "VerticalEndState",
|
||||
};
|
||||
|
||||
VisualStateManager.GoToState(this, stateName, true);
|
||||
}
|
||||
|
||||
private void Scroller_ViewChanging(object sender, ScrollViewerViewChangingEventArgs e)
|
||||
{
|
||||
UpdateScrollButtonsVisibility(e.FinalView.HorizontalOffset, e.FinalView.VerticalOffset);
|
||||
}
|
||||
|
||||
private void ScrollBackBtn_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (Orientation == Orientation.Horizontal)
|
||||
{
|
||||
scroller.ChangeView(scroller.HorizontalOffset - scroller.ViewportWidth, null, null);
|
||||
}
|
||||
else
|
||||
{
|
||||
scroller.ChangeView(null, scroller.VerticalOffset - scroller.ViewportHeight, null);
|
||||
}
|
||||
|
||||
// Manually focus to ScrollForwardBtn since this button disappears after scrolling to the end.
|
||||
ScrollForwardBtn.Focus(FocusState.Programmatic);
|
||||
}
|
||||
|
||||
private void ScrollForwardBtn_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (Orientation == Orientation.Horizontal)
|
||||
{
|
||||
scroller.ChangeView(scroller.HorizontalOffset + scroller.ViewportWidth, null, null);
|
||||
}
|
||||
else
|
||||
{
|
||||
scroller.ChangeView(null, scroller.VerticalOffset + scroller.ViewportHeight, null);
|
||||
}
|
||||
|
||||
// Manually focus to ScrollBackBtn since this button disappears after scrolling to the end.
|
||||
ScrollBackBtn.Focus(FocusState.Programmatic);
|
||||
}
|
||||
|
||||
private void Scroller_SizeChanged(object sender, SizeChangedEventArgs e)
|
||||
{
|
||||
UpdateScrollButtonsVisibility();
|
||||
}
|
||||
|
||||
private void UpdateScrollButtonsVisibility(double? horizontalOffset = null, double? verticalOffset = null)
|
||||
{
|
||||
var hOffset = horizontalOffset ?? scroller.HorizontalOffset;
|
||||
var vOffset = verticalOffset ?? scroller.VerticalOffset;
|
||||
|
||||
if (Orientation == Orientation.Horizontal)
|
||||
{
|
||||
ScrollBackBtn.Visibility = hOffset > 1 ? Visibility.Visible : Visibility.Collapsed;
|
||||
ScrollForwardBtn.Visibility = scroller.ScrollableWidth > 0 && hOffset < scroller.ScrollableWidth - 1
|
||||
? Visibility.Visible
|
||||
: Visibility.Collapsed;
|
||||
}
|
||||
else
|
||||
{
|
||||
ScrollBackBtn.Visibility = vOffset > 1 ? Visibility.Visible : Visibility.Collapsed;
|
||||
ScrollForwardBtn.Visibility = scroller.ScrollableHeight > 0 && vOffset < scroller.ScrollableHeight - 1
|
||||
? Visibility.Visible
|
||||
: Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user