mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-01-06 12:27:01 +01:00
Compare commits
1 Commits
shawn/wina
...
leilzh/fix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bdb1d848c6 |
2
.github/actions/spell-check/expect.txt
vendored
2
.github/actions/spell-check/expect.txt
vendored
@@ -1854,8 +1854,6 @@ uitests
|
||||
UITo
|
||||
ULONGLONG
|
||||
ums
|
||||
UMax
|
||||
UMin
|
||||
uncompilable
|
||||
UNCPRIORITY
|
||||
UNDNAME
|
||||
|
||||
@@ -16,7 +16,32 @@ Param(
|
||||
[string]$sourceLink = "https://microsoft.pkgs.visualstudio.com/ProjectReunion/_packaging/Project.Reunion.nuget.internal/nuget/v3/index.json"
|
||||
)
|
||||
|
||||
function Update-NugetConfig {
|
||||
param (
|
||||
[string]$filePath = [System.IO.Path]::Combine($rootPath, "nuget.config")
|
||||
)
|
||||
|
||||
Write-Host "Updating nuget.config file"
|
||||
[xml]$xml = Get-Content -Path $filePath
|
||||
|
||||
# Add localpackages source into nuget.config
|
||||
$packageSourcesNode = $xml.configuration.packageSources
|
||||
$addNode = $xml.CreateElement("add")
|
||||
$addNode.SetAttribute("key", "localpackages")
|
||||
$addNode.SetAttribute("value", "localpackages")
|
||||
$packageSourcesNode.AppendChild($addNode) | Out-Null
|
||||
|
||||
# Remove <packageSourceMapping> tag and its content
|
||||
$packageSourceMappingNode = $xml.configuration.packageSourceMapping
|
||||
if ($packageSourceMappingNode) {
|
||||
$xml.configuration.RemoveChild($packageSourceMappingNode) | Out-Null
|
||||
}
|
||||
|
||||
# print nuget.config after modification
|
||||
$xml.OuterXml
|
||||
# Save the modified nuget.config file
|
||||
$xml.Save($filePath)
|
||||
}
|
||||
|
||||
function Read-FileWithEncoding {
|
||||
param (
|
||||
@@ -46,132 +71,6 @@ function Write-FileWithEncoding {
|
||||
$writer.Close()
|
||||
}
|
||||
|
||||
|
||||
function Add-NuGetSourceAndMapping {
|
||||
param (
|
||||
[xml]$Xml,
|
||||
[string]$Key,
|
||||
[string]$Value,
|
||||
[string[]]$Patterns
|
||||
)
|
||||
|
||||
# Ensure packageSources exists
|
||||
if (-not $Xml.configuration.packageSources) {
|
||||
$Xml.configuration.AppendChild($Xml.CreateElement("packageSources")) | Out-Null
|
||||
}
|
||||
$sources = $Xml.configuration.packageSources
|
||||
|
||||
# Add/Update Source
|
||||
$sourceNode = $sources.SelectSingleNode("add[@key='$Key']")
|
||||
if (-not $sourceNode) {
|
||||
$sourceNode = $Xml.CreateElement("add")
|
||||
$sourceNode.SetAttribute("key", $Key)
|
||||
$sources.AppendChild($sourceNode) | Out-Null
|
||||
}
|
||||
$sourceNode.SetAttribute("value", $Value)
|
||||
|
||||
# Ensure packageSourceMapping exists
|
||||
if (-not $Xml.configuration.packageSourceMapping) {
|
||||
$Xml.configuration.AppendChild($Xml.CreateElement("packageSourceMapping")) | Out-Null
|
||||
}
|
||||
$mapping = $Xml.configuration.packageSourceMapping
|
||||
|
||||
# Remove invalid packageSource nodes (missing key or empty key)
|
||||
$invalidNodes = $mapping.SelectNodes("packageSource[not(@key) or @key='']")
|
||||
if ($invalidNodes) {
|
||||
foreach ($node in $invalidNodes) {
|
||||
$mapping.RemoveChild($node) | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
# Add/Update Mapping Source
|
||||
$mappingSource = $mapping.SelectSingleNode("packageSource[@key='$Key']")
|
||||
if (-not $mappingSource) {
|
||||
$mappingSource = $Xml.CreateElement("packageSource")
|
||||
$mappingSource.SetAttribute("key", $Key)
|
||||
# Insert at top for priority
|
||||
if ($mapping.HasChildNodes) {
|
||||
$mapping.InsertBefore($mappingSource, $mapping.FirstChild) | Out-Null
|
||||
} else {
|
||||
$mapping.AppendChild($mappingSource) | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
# Double check and force attribute
|
||||
if (-not $mappingSource.HasAttribute("key")) {
|
||||
$mappingSource.SetAttribute("key", $Key)
|
||||
}
|
||||
|
||||
# Update Patterns
|
||||
# RemoveAll() removes all child nodes AND attributes, so we must re-set the key afterwards
|
||||
$mappingSource.RemoveAll()
|
||||
$mappingSource.SetAttribute("key", $Key)
|
||||
|
||||
foreach ($pattern in $Patterns) {
|
||||
$pkg = $Xml.CreateElement("package")
|
||||
$pkg.SetAttribute("pattern", $pattern)
|
||||
$mappingSource.AppendChild($pkg) | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
function Resolve-WinAppSdkSplitDependencies {
|
||||
Write-Host "Version $WinAppSDKVersion detected. Resolving split dependencies..."
|
||||
$installDir = Join-Path $rootPath "localpackages\output"
|
||||
New-Item -ItemType Directory -Path $installDir -Force | Out-Null
|
||||
|
||||
# Create a temporary nuget.config to avoid interference from the repo's config
|
||||
$tempConfig = Join-Path $env:TEMP "nuget_$(Get-Random).config"
|
||||
Set-Content -Path $tempConfig -Value "<?xml version='1.0' encoding='utf-8'?><configuration><packageSources><clear /><add key='TempSource' value='$sourceLink' /></packageSources></configuration>"
|
||||
|
||||
try {
|
||||
# Extract BuildTools version from Directory.Packages.props to ensure we have the required version
|
||||
$dirPackagesProps = Join-Path $rootPath "Directory.Packages.props"
|
||||
if (Test-Path $dirPackagesProps) {
|
||||
$propsContent = Get-Content $dirPackagesProps -Raw
|
||||
if ($propsContent -match '<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="([^"]+)"') {
|
||||
$buildToolsVersion = $Matches[1]
|
||||
Write-Host "Downloading Microsoft.Windows.SDK.BuildTools version $buildToolsVersion..."
|
||||
$nugetArgsBuildTools = "install Microsoft.Windows.SDK.BuildTools -Version $buildToolsVersion -ConfigFile $tempConfig -OutputDirectory $installDir -NonInteractive -NoCache"
|
||||
Invoke-Expression "nuget $nugetArgsBuildTools" | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
# Download package to inspect nuspec and keep it for the build
|
||||
$nugetArgs = "install Microsoft.WindowsAppSDK -Version $WinAppSDKVersion -ConfigFile $tempConfig -OutputDirectory $installDir -NonInteractive -NoCache"
|
||||
Invoke-Expression "nuget $nugetArgs" | Out-Null
|
||||
|
||||
# Parse dependencies from the installed folders
|
||||
# Folder structure is typically {PackageId}.{Version}
|
||||
$directories = Get-ChildItem -Path $installDir -Directory
|
||||
$allLocalPackages = @()
|
||||
foreach ($dir in $directories) {
|
||||
# Match any package pattern: PackageId.Version
|
||||
if ($dir.Name -match "^(.+?)\.(\d+\..*)$") {
|
||||
$pkgId = $Matches[1]
|
||||
$pkgVer = $Matches[2]
|
||||
$allLocalPackages += $pkgId
|
||||
|
||||
$packageVersions[$pkgId] = $pkgVer
|
||||
Write-Host "Found dependency: $pkgId = $pkgVer"
|
||||
}
|
||||
}
|
||||
|
||||
# Update repo's nuget.config to use localpackages
|
||||
$nugetConfig = Join-Path $rootPath "nuget.config"
|
||||
$configData = Read-FileWithEncoding -Path $nugetConfig
|
||||
[xml]$xml = $configData.Content
|
||||
|
||||
Add-NuGetSourceAndMapping -Xml $xml -Key "localpackages" -Value "localpackages\output" -Patterns $allLocalPackages
|
||||
|
||||
$xml.Save($nugetConfig)
|
||||
Write-Host "Updated nuget.config with localpackages mapping."
|
||||
} catch {
|
||||
Write-Warning "Failed to resolve dependencies: $_"
|
||||
} finally {
|
||||
Remove-Item $tempConfig -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
# Execute nuget list and capture the output
|
||||
if ($useExperimentalVersion) {
|
||||
# The nuget list for experimental versions will cost more time
|
||||
@@ -213,36 +112,56 @@ if ($latestVersion) {
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Resolve dependencies for 1.8+
|
||||
$packageVersions = @{ "Microsoft.WindowsAppSDK" = $WinAppSDKVersion }
|
||||
|
||||
Resolve-WinAppSdkSplitDependencies
|
||||
# Update packages.config files
|
||||
Get-ChildItem -Path $rootPath -Recurse packages.config | ForEach-Object {
|
||||
$file = Read-FileWithEncoding -Path $_.FullName
|
||||
$content = $file.Content
|
||||
if ($content -match 'package id="Microsoft.WindowsAppSDK"') {
|
||||
$newVersionString = 'package id="Microsoft.WindowsAppSDK" version="' + $WinAppSDKVersion + '"'
|
||||
$oldVersionString = 'package id="Microsoft.WindowsAppSDK" version="[-.0-9a-zA-Z]*"'
|
||||
$content = $content -replace $oldVersionString, $newVersionString
|
||||
Write-FileWithEncoding -Path $_.FullName -Content $content -Encoding $file.encoding
|
||||
Write-Host "Modified " $_.FullName
|
||||
}
|
||||
}
|
||||
|
||||
# Update Directory.Packages.props file
|
||||
Get-ChildItem -Path $rootPath -Recurse "Directory.Packages.props" | ForEach-Object {
|
||||
$file = Read-FileWithEncoding -Path $_.FullName
|
||||
$content = $file.Content
|
||||
$isModified = $false
|
||||
|
||||
foreach ($pkgId in $packageVersions.Keys) {
|
||||
$ver = $packageVersions[$pkgId]
|
||||
# Escape dots in package ID for regex
|
||||
$pkgIdRegex = $pkgId -replace '\.', '\.'
|
||||
|
||||
$newVersionString = "<PackageVersion Include=""$pkgId"" Version=""$ver"" />"
|
||||
$oldVersionString = "<PackageVersion Include=""$pkgIdRegex"" Version=""[-.0-9a-zA-Z]*"" />"
|
||||
|
||||
if ($content -match "<PackageVersion Include=""$pkgIdRegex""") {
|
||||
# Update existing package
|
||||
if ($content -notmatch [regex]::Escape($newVersionString)) {
|
||||
$content = $content -replace $oldVersionString, $newVersionString
|
||||
$isModified = $true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($isModified) {
|
||||
if ($content -match '<PackageVersion Include="Microsoft.WindowsAppSDK"') {
|
||||
$newVersionString = '<PackageVersion Include="Microsoft.WindowsAppSDK" Version="' + $WinAppSDKVersion + '" />'
|
||||
$oldVersionString = '<PackageVersion Include="Microsoft.WindowsAppSDK" Version="[-.0-9a-zA-Z]*" />'
|
||||
$content = $content -replace $oldVersionString, $newVersionString
|
||||
Write-FileWithEncoding -Path $_.FullName -Content $content -Encoding $file.encoding
|
||||
Write-Host "Modified " $_.FullName
|
||||
}
|
||||
}
|
||||
|
||||
# Update .vcxproj files
|
||||
Get-ChildItem -Path $rootPath -Recurse *.vcxproj | ForEach-Object {
|
||||
$file = Read-FileWithEncoding -Path $_.FullName
|
||||
$content = $file.Content
|
||||
if ($content -match '\\Microsoft.WindowsAppSDK.') {
|
||||
$newVersionString = '\Microsoft.WindowsAppSDK.' + $WinAppSDKVersion
|
||||
$oldVersionString = '\\Microsoft.WindowsAppSDK.(?=[-.0-9a-zA-Z]*\d)[-.0-9a-zA-Z]*' #positive lookahead for at least a digit
|
||||
$content = $content -replace $oldVersionString, $newVersionString
|
||||
Write-FileWithEncoding -Path $_.FullName -Content $content -Encoding $file.encoding
|
||||
Write-Host "Modified " $_.FullName
|
||||
}
|
||||
}
|
||||
|
||||
# Update .csproj files
|
||||
Get-ChildItem -Path $rootPath -Recurse *.csproj | ForEach-Object {
|
||||
$file = Read-FileWithEncoding -Path $_.FullName
|
||||
$content = $file.Content
|
||||
if ($content -match 'PackageReference Include="Microsoft.WindowsAppSDK"') {
|
||||
$newVersionString = 'PackageReference Include="Microsoft.WindowsAppSDK" Version="'+ $WinAppSDKVersion + '"'
|
||||
$oldVersionString = 'PackageReference Include="Microsoft.WindowsAppSDK" Version="[-.0-9a-zA-Z]*"'
|
||||
$content = $content -replace $oldVersionString, $newVersionString
|
||||
Write-FileWithEncoding -Path $_.FullName -Content $content -Encoding $file.encoding
|
||||
Write-Host "Modified " $_.FullName
|
||||
}
|
||||
}
|
||||
|
||||
Update-NugetConfig
|
||||
|
||||
@@ -19,7 +19,7 @@ parameters:
|
||||
- name: enableMsBuildCaching
|
||||
type: boolean
|
||||
displayName: "Enable MSBuild Caching"
|
||||
default: false
|
||||
default: true
|
||||
- name: runTests
|
||||
type: boolean
|
||||
displayName: "Run Tests"
|
||||
@@ -33,7 +33,7 @@ parameters:
|
||||
default: true
|
||||
- name: winAppSDKVersionNumber
|
||||
type: string
|
||||
default: 1.8
|
||||
default: 1.7
|
||||
- name: useExperimentalVersion
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
@@ -19,19 +19,48 @@ steps:
|
||||
-useExperimentalVersion $${{ parameters.useExperimentalVersion }}
|
||||
-rootPath "$(build.sourcesdirectory)"
|
||||
|
||||
# - task: NuGetCommand@2
|
||||
# displayName: 'Restore NuGet packages (slnx)'
|
||||
# inputs:
|
||||
# command: 'restore'
|
||||
# feedsToUse: 'config'
|
||||
# nugetConfigPath: '$(build.sourcesdirectory)\nuget.config'
|
||||
# restoreSolution: '$(build.sourcesdirectory)\**\*.slnx'
|
||||
# includeNuGetOrg: false
|
||||
- script: echo $(WinAppSDKVersion)
|
||||
displayName: 'Display WinAppSDK Version Found'
|
||||
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: 'Restore NuGet packages (dotnet)'
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: 'Download WindowsAppSDK'
|
||||
inputs:
|
||||
buildType: 'specific'
|
||||
project: '55e8140e-57ac-4e5f-8f9c-c7c15b51929d'
|
||||
definition: '104083'
|
||||
buildVersionToDownload: 'latestFromBranch'
|
||||
branchName: 'refs/heads/release/${{ parameters.versionNumber }}-stable'
|
||||
artifactName: 'WindowsAppSDK_Nuget_And_MSIX'
|
||||
targetPath: '$(Build.SourcesDirectory)\localpackages'
|
||||
|
||||
- script: dir $(Build.SourcesDirectory)\localpackages\NugetPackages
|
||||
displayName: 'List downloaded packages'
|
||||
|
||||
- task: NuGetCommand@2
|
||||
displayName: 'Install WindowsAppSDK'
|
||||
inputs:
|
||||
command: 'custom'
|
||||
arguments: >
|
||||
install "Microsoft.WindowsAppSDK"
|
||||
-Source "$(Build.SourcesDirectory)\localpackages\NugetPackages"
|
||||
-Version "$(WinAppSDKVersion)"
|
||||
-OutputDirectory "$(Build.SourcesDirectory)\localpackages\output"
|
||||
-FallbackSource "https://microsoft.pkgs.visualstudio.com/ProjectReunion/_packaging/Project.Reunion.nuget.internal/nuget/v3/index.json"
|
||||
|
||||
- task: NuGetCommand@2
|
||||
displayName: 'Restore NuGet packages'
|
||||
inputs:
|
||||
command: 'restore'
|
||||
projects: '$(build.sourcesdirectory)\**\*.slnx'
|
||||
feedsToUse: 'config'
|
||||
nugetConfigPath: '$(build.sourcesdirectory)\nuget.config'
|
||||
restoreSolution: '$(build.sourcesdirectory)\**\*.sln'
|
||||
includeNuGetOrg: false
|
||||
|
||||
- task: NuGetCommand@2
|
||||
displayName: 'Restore NuGet packages (slnx)'
|
||||
inputs:
|
||||
command: 'restore'
|
||||
feedsToUse: 'config'
|
||||
nugetConfigPath: '$(build.sourcesdirectory)\nuget.config'
|
||||
restoreSolution: '$(build.sourcesdirectory)\**\*.slnx'
|
||||
includeNuGetOrg: false
|
||||
|
||||
@@ -27,8 +27,7 @@ $versionExceptions = @(
|
||||
"WyHash.dll",
|
||||
"Microsoft.Recognizers.Text.DataTypes.TimexExpression.dll",
|
||||
"ObjectModelCsProjection.dll",
|
||||
"RendererCsProjection.dll",
|
||||
"Microsoft.ML.OnnxRuntime.dll") -join '|';
|
||||
"RendererCsProjection.dll") -join '|';
|
||||
$nullVersionExceptions = @(
|
||||
"SkiaSharp.Views.WinUI.Native.dll",
|
||||
"libSkiaSharp.dll",
|
||||
|
||||
@@ -8,20 +8,4 @@
|
||||
<PropertyGroup Label="ManifestToolOverride">
|
||||
<ManifestTool Condition="Exists('$(WindowsSdkDir)bin\x64\mt.exe')">$(WindowsSdkDir)bin\x64\mt.exe</ManifestTool>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Auto-restore NuGet for native vcxproj (PackageReference) when building inside VS -->
|
||||
<Target Name="EnsureNuGetRestoreForVcxproj" BeforeTargets="PrepareForBuild" Condition="
|
||||
'$(BuildingInsideVisualStudio)' == 'true'
|
||||
and '$(DesignTimeBuild)' != 'true'
|
||||
and '$(RestoreInProgress)' != 'true'
|
||||
and '$(MSBuildProjectExtension)' == '.vcxproj'
|
||||
and '$(RestoreProjectStyle)' == 'PackageReference'
|
||||
and '$(MSBuildProjectExtensionsPath)' != ''
|
||||
and !Exists('$(MSBuildProjectExtensionsPath)project.assets.json')
|
||||
">
|
||||
|
||||
<Message Importance="normal" Text="NuGet assets missing for $(MSBuildProjectName); running Restore...; IntDir=$(IntDir); BaseIntermediateOutputPath=$(BaseIntermediateOutputPath)" />
|
||||
|
||||
<MSBuild Projects="$(MSBuildProjectFullPath)" Targets="Restore" Properties="RestoreInProgress=true" BuildInParallel="false" />
|
||||
</Target>
|
||||
</Project>
|
||||
@@ -124,7 +124,7 @@
|
||||
<Custom Action="SetBundleInstallLocation" After="InstallFiles" Condition="NOT Installed OR WIX_UPGRADE_DETECTED" />
|
||||
<Custom Action="ApplyModulesRegistryChangeSets" After="InstallFiles" Condition="NOT Installed" />
|
||||
<Custom Action="InstallCmdPalPackage" After="InstallFiles" Condition="NOT Installed" />
|
||||
<Custom Action="InstallPackageIdentityMSIX" After="InstallFiles" Condition="NOT Installed AND WINDOWSBUILDNUMBER >= 22000" />
|
||||
<Custom Action="InstallPackageIdentityMSIX" After="InstallFiles" Condition="NOT Installed" />
|
||||
<Custom Action="override Wix4CloseApplications_$(sys.BUILDARCHSHORT)" Before="RemoveFiles" />
|
||||
<Custom Action="RemovePowerToysSchTasks" After="RemoveFiles" />
|
||||
<!-- TODO: Use to activate embedded MSIX -->
|
||||
|
||||
@@ -144,7 +144,7 @@ public sealed class AIServiceBatchIntegrationTests
|
||||
switch (format)
|
||||
{
|
||||
case PasteFormats.CustomTextTransformation:
|
||||
var transformResult = await services.CustomActionTransformService.TransformAsync(batchTestInput.Prompt, batchTestInput.Clipboard, null, CancellationToken.None, progress);
|
||||
var transformResult = await services.CustomActionTransformService.TransformTextAsync(batchTestInput.Prompt, batchTestInput.Clipboard, CancellationToken.None, progress);
|
||||
return DataPackageHelpers.CreateFromText(transformResult.Content ?? string.Empty);
|
||||
|
||||
case PasteFormats.KernelQuery:
|
||||
|
||||
@@ -198,14 +198,20 @@ namespace AdvancedPaste.Pages
|
||||
}
|
||||
}
|
||||
|
||||
private void ClipboardHistory_ItemInvoked(ItemsView sender, ItemsViewItemInvokedEventArgs args)
|
||||
private async void ClipboardHistory_ItemInvoked(ItemsView sender, ItemsViewItemInvokedEventArgs args)
|
||||
{
|
||||
if (args.InvokedItem is ClipboardItem item && item.Item is not null)
|
||||
if (args.InvokedItem is ClipboardItem item)
|
||||
{
|
||||
PowerToysTelemetry.Log.WriteEvent(new Telemetry.AdvancedPasteClipboardItemClicked());
|
||||
|
||||
// Use SetHistoryItemAsContent to set the clipboard content without creating a new history entry
|
||||
Clipboard.SetHistoryItemAsContent(item.Item);
|
||||
if (!string.IsNullOrEmpty(item.Content))
|
||||
{
|
||||
ClipboardHelper.SetTextContent(item.Content);
|
||||
}
|
||||
else if (item.Image is not null)
|
||||
{
|
||||
RandomAccessStreamReference image = await item.Item.Content.GetBitmapAsync();
|
||||
ClipboardHelper.SetImageContent(image);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,24 +225,6 @@ internal static class DataPackageHelpers
|
||||
internal static async Task<string> GetHtmlContentAsync(this DataPackageView dataPackageView) =>
|
||||
dataPackageView.Contains(StandardDataFormats.Html) ? await dataPackageView.GetHtmlFormatAsync() : string.Empty;
|
||||
|
||||
internal static async Task<byte[]> GetImageAsPngBytesAsync(this DataPackageView dataPackageView)
|
||||
{
|
||||
var bitmap = await dataPackageView.GetImageContentAsync();
|
||||
if (bitmap == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
using var pngStream = new InMemoryRandomAccessStream();
|
||||
var encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, pngStream);
|
||||
encoder.SetSoftwareBitmap(bitmap);
|
||||
await encoder.FlushAsync();
|
||||
|
||||
using var memoryStream = new MemoryStream();
|
||||
await pngStream.AsStreamForRead().CopyToAsync(memoryStream);
|
||||
return memoryStream.ToArray();
|
||||
}
|
||||
|
||||
internal static async Task<SoftwareBitmap> GetImageContentAsync(this DataPackageView dataPackageView)
|
||||
{
|
||||
using var stream = await dataPackageView.GetImageStreamAsync();
|
||||
|
||||
@@ -166,8 +166,5 @@ namespace AdvancedPaste.Helpers
|
||||
|
||||
[DllImport("Shlwapi.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
internal static extern HResult AssocQueryString(AssocF flags, AssocStr str, string pszAssoc, string pszExtra, [Out] StringBuilder pszOut, [In][Out] ref uint pcchOut);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
internal static extern uint GetClipboardSequenceNumber();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ public enum PasteFormats
|
||||
CanPreview = true,
|
||||
SupportedClipboardFormats = ClipboardFormat.Image,
|
||||
IPCKey = AdvancedPasteAdditionalActions.PropertyNames.ImageToText,
|
||||
KernelFunctionDescription = "Takes an image from the clipboard and extracts text using OCR. This function is intended only for explicit text extraction or OCR requests.")]
|
||||
KernelFunctionDescription = "Takes an image in the clipboard and extracts all text from it using OCR.")]
|
||||
ImageToText,
|
||||
|
||||
[PasteFormatMetadata(
|
||||
@@ -118,8 +118,8 @@ public enum PasteFormats
|
||||
IconGlyph = "\uE945",
|
||||
RequiresAIService = true,
|
||||
CanPreview = true,
|
||||
SupportedClipboardFormats = ClipboardFormat.Text | ClipboardFormat.Image,
|
||||
KernelFunctionDescription = "Takes user instructions and applies them to the current clipboard content (text or image). Use this function for image analysis, description, or transformation tasks beyond simple OCR.",
|
||||
SupportedClipboardFormats = ClipboardFormat.Text,
|
||||
KernelFunctionDescription = "Takes input instructions and transforms clipboard text (not TXT files) with these input instructions, putting the result back on the clipboard. This uses AI to accomplish the task.",
|
||||
RequiresPrompt = true)]
|
||||
CustomTextTransformation,
|
||||
}
|
||||
|
||||
@@ -40,15 +40,15 @@ namespace AdvancedPaste.Services.CustomActions
|
||||
this.userSettings = userSettings;
|
||||
}
|
||||
|
||||
public async Task<CustomActionTransformResult> TransformAsync(string prompt, string inputText, byte[] imageBytes, CancellationToken cancellationToken, IProgress<double> progress)
|
||||
public async Task<CustomActionTransformResult> TransformTextAsync(string prompt, string inputText, CancellationToken cancellationToken, IProgress<double> progress)
|
||||
{
|
||||
var pasteConfig = userSettings?.PasteAIConfiguration;
|
||||
var providerConfig = BuildProviderConfig(pasteConfig);
|
||||
|
||||
return await TransformAsync(prompt, inputText, imageBytes, providerConfig, cancellationToken, progress);
|
||||
return await TransformAsync(prompt, inputText, providerConfig, cancellationToken, progress);
|
||||
}
|
||||
|
||||
private async Task<CustomActionTransformResult> TransformAsync(string prompt, string inputText, byte[] imageBytes, PasteAIConfig providerConfig, CancellationToken cancellationToken, IProgress<double> progress)
|
||||
private async Task<CustomActionTransformResult> TransformAsync(string prompt, string inputText, PasteAIConfig providerConfig, CancellationToken cancellationToken, IProgress<double> progress)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(providerConfig);
|
||||
|
||||
@@ -57,9 +57,9 @@ namespace AdvancedPaste.Services.CustomActions
|
||||
return new CustomActionTransformResult(string.Empty, AIServiceUsage.None);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(inputText) && imageBytes is null)
|
||||
if (string.IsNullOrWhiteSpace(inputText))
|
||||
{
|
||||
Logger.LogWarning("Clipboard has no usable data");
|
||||
Logger.LogWarning("Clipboard has no usable text data");
|
||||
return new CustomActionTransformResult(string.Empty, AIServiceUsage.None);
|
||||
}
|
||||
|
||||
@@ -80,8 +80,6 @@ namespace AdvancedPaste.Services.CustomActions
|
||||
{
|
||||
Prompt = prompt,
|
||||
InputText = inputText,
|
||||
ImageBytes = imageBytes,
|
||||
ImageMimeType = imageBytes != null ? "image/png" : null,
|
||||
SystemPrompt = systemPrompt,
|
||||
};
|
||||
|
||||
|
||||
@@ -12,6 +12,6 @@ namespace AdvancedPaste.Services.CustomActions
|
||||
{
|
||||
public interface ICustomActionTransformService
|
||||
{
|
||||
Task<CustomActionTransformResult> TransformAsync(string prompt, string inputText, byte[] imageBytes, CancellationToken cancellationToken, IProgress<double> progress);
|
||||
Task<CustomActionTransformResult> TransformTextAsync(string prompt, string inputText, CancellationToken cancellationToken, IProgress<double> progress);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,10 +12,6 @@ namespace AdvancedPaste.Services.CustomActions
|
||||
|
||||
public string InputText { get; init; }
|
||||
|
||||
public byte[] ImageBytes { get; init; }
|
||||
|
||||
public string ImageMimeType { get; init; }
|
||||
|
||||
public string SystemPrompt { get; init; }
|
||||
|
||||
public AIServiceUsage Usage { get; set; } = AIServiceUsage.None;
|
||||
|
||||
@@ -64,13 +64,21 @@ namespace AdvancedPaste.Services.CustomActions
|
||||
|
||||
var prompt = request.Prompt;
|
||||
var inputText = request.InputText;
|
||||
var imageBytes = request.ImageBytes;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(prompt) || (string.IsNullOrWhiteSpace(inputText) && imageBytes is null))
|
||||
if (string.IsNullOrWhiteSpace(prompt) || string.IsNullOrWhiteSpace(inputText))
|
||||
{
|
||||
throw new ArgumentException("Prompt and input content must be provided", nameof(request));
|
||||
throw new ArgumentException("Prompt and input text must be provided", nameof(request));
|
||||
}
|
||||
|
||||
var userMessageContent = $"""
|
||||
User instructions:
|
||||
{prompt}
|
||||
|
||||
Clipboard Content:
|
||||
{inputText}
|
||||
|
||||
Output:
|
||||
""";
|
||||
|
||||
var executionSettings = CreateExecutionSettings();
|
||||
var kernel = CreateKernel();
|
||||
var modelId = _config.Model;
|
||||
@@ -94,32 +102,7 @@ namespace AdvancedPaste.Services.CustomActions
|
||||
|
||||
var chatHistory = new ChatHistory();
|
||||
chatHistory.AddSystemMessage(systemPrompt);
|
||||
|
||||
if (imageBytes != null)
|
||||
{
|
||||
var collection = new ChatMessageContentItemCollection();
|
||||
if (!string.IsNullOrWhiteSpace(inputText))
|
||||
{
|
||||
collection.Add(new TextContent($"Clipboard Content:\n{inputText}"));
|
||||
}
|
||||
|
||||
collection.Add(new ImageContent(imageBytes, request.ImageMimeType ?? "image/png"));
|
||||
collection.Add(new TextContent($"User instructions:\n{prompt}\n\nOutput:"));
|
||||
chatHistory.AddUserMessage(collection);
|
||||
}
|
||||
else
|
||||
{
|
||||
var userMessageContent = $"""
|
||||
User instructions:
|
||||
{prompt}
|
||||
|
||||
Clipboard Content:
|
||||
{inputText}
|
||||
|
||||
Output:
|
||||
""";
|
||||
chatHistory.AddUserMessage(userMessageContent);
|
||||
}
|
||||
chatHistory.AddUserMessage(userMessageContent);
|
||||
|
||||
var response = await chatService.GetChatMessageContentAsync(chatHistory, executionSettings, kernel, cancellationToken);
|
||||
chatHistory.Add(response);
|
||||
|
||||
@@ -67,36 +67,12 @@ public abstract class KernelServiceBase(
|
||||
|
||||
LogResult(cacheUsed, isSavedQuery, kernel.GetOrAddActionChain(), usage);
|
||||
|
||||
var outputPackage = kernel.GetDataPackage();
|
||||
var hasUsableData = await outputPackage.GetView().HasUsableDataAsync();
|
||||
|
||||
if (kernel.GetLastError() is Exception ex)
|
||||
{
|
||||
// If we have an error, but the AI provided a final text response, we can ignore the error (likely a tool failure that the AI handled).
|
||||
// However, if we have usable data (e.g. from a successful tool call before the error?), we might want to keep it?
|
||||
// In the case of ImageToText failure, outputPackage is empty (new DataPackage), hasUsableData is false.
|
||||
// So we check if there is a valid response in the chat history.
|
||||
var lastMessage = chatHistory.LastOrDefault();
|
||||
bool hasAssistantResponse = lastMessage != null && lastMessage.Role == AuthorRole.Assistant && !string.IsNullOrEmpty(lastMessage.Content);
|
||||
|
||||
if (!hasAssistantResponse && !hasUsableData)
|
||||
{
|
||||
throw ex;
|
||||
}
|
||||
|
||||
// If we have a response or data, we log the error but proceed.
|
||||
Logger.LogWarning($"Kernel operation encountered an error but proceeded with available response/data: {ex.Message}");
|
||||
throw ex;
|
||||
}
|
||||
|
||||
if (!hasUsableData)
|
||||
{
|
||||
var lastMessage = chatHistory.LastOrDefault();
|
||||
if (lastMessage != null && lastMessage.Role == AuthorRole.Assistant && !string.IsNullOrEmpty(lastMessage.Content))
|
||||
{
|
||||
outputPackage = DataPackageHelpers.CreateFromText(lastMessage.Content);
|
||||
kernel.SetDataPackage(outputPackage);
|
||||
}
|
||||
}
|
||||
var outputPackage = kernel.GetDataPackage();
|
||||
|
||||
if (!(await outputPackage.GetView().HasUsableDataAsync()))
|
||||
{
|
||||
@@ -172,21 +148,7 @@ public abstract class KernelServiceBase(
|
||||
var systemPrompt = string.IsNullOrWhiteSpace(runtimeConfig.SystemPrompt) ? DefaultSystemPrompt : runtimeConfig.SystemPrompt;
|
||||
chatHistory.AddSystemMessage(systemPrompt);
|
||||
chatHistory.AddSystemMessage($"Available clipboard formats: {await kernel.GetDataFormatsAsync()}");
|
||||
|
||||
var imageBytes = await kernel.GetDataPackageView().GetImageAsPngBytesAsync();
|
||||
if (imageBytes != null)
|
||||
{
|
||||
var collection = new ChatMessageContentItemCollection
|
||||
{
|
||||
new TextContent(prompt),
|
||||
new ImageContent(imageBytes, "image/png"),
|
||||
};
|
||||
chatHistory.AddUserMessage(collection);
|
||||
}
|
||||
else
|
||||
{
|
||||
chatHistory.AddUserMessage(prompt);
|
||||
}
|
||||
chatHistory.AddUserMessage(prompt);
|
||||
|
||||
if (ShouldModerateAdvancedAI())
|
||||
{
|
||||
@@ -340,16 +302,8 @@ public abstract class KernelServiceBase(
|
||||
new ActionChainItem(PasteFormats.CustomTextTransformation, Arguments: new() { { PromptParameterName, fixedPrompt } }),
|
||||
async dataPackageView =>
|
||||
{
|
||||
var imageBytes = await dataPackageView.GetImageAsPngBytesAsync();
|
||||
var input = await dataPackageView.GetTextOrHtmlTextAsync();
|
||||
|
||||
if (string.IsNullOrEmpty(input) && imageBytes == null)
|
||||
{
|
||||
// If we have no text and no image, try to get text via OCR or throw if nothing exists
|
||||
input = await dataPackageView.GetClipboardTextOrThrowAsync(kernel.GetCancellationToken());
|
||||
}
|
||||
|
||||
var result = await _customActionTransformService.TransformAsync(fixedPrompt, input, imageBytes, kernel.GetCancellationToken(), kernel.GetProgress());
|
||||
var input = await dataPackageView.GetClipboardTextOrThrowAsync(kernel.GetCancellationToken());
|
||||
var result = await _customActionTransformService.TransformTextAsync(fixedPrompt, input, kernel.GetCancellationToken(), kernel.GetProgress());
|
||||
return DataPackageHelpers.CreateFromText(result?.Content ?? string.Empty);
|
||||
});
|
||||
|
||||
@@ -359,22 +313,15 @@ public abstract class KernelServiceBase(
|
||||
new ActionChainItem(format, Arguments: new() { { PromptParameterName, prompt } }),
|
||||
async dataPackageView =>
|
||||
{
|
||||
var imageBytes = await dataPackageView.GetImageAsPngBytesAsync();
|
||||
var input = await dataPackageView.GetTextOrHtmlTextAsync();
|
||||
|
||||
if (string.IsNullOrEmpty(input) && imageBytes == null)
|
||||
{
|
||||
input = await dataPackageView.GetClipboardTextOrThrowAsync(kernel.GetCancellationToken());
|
||||
}
|
||||
|
||||
string output = await GetPromptBasedOutput(format, prompt, input, imageBytes, kernel.GetCancellationToken(), kernel.GetProgress());
|
||||
var input = await dataPackageView.GetClipboardTextOrThrowAsync(kernel.GetCancellationToken());
|
||||
string output = await GetPromptBasedOutput(format, prompt, input, kernel.GetCancellationToken(), kernel.GetProgress());
|
||||
return DataPackageHelpers.CreateFromText(output);
|
||||
});
|
||||
|
||||
private async Task<string> GetPromptBasedOutput(PasteFormats format, string prompt, string input, byte[] imageBytes, CancellationToken cancellationToken, IProgress<double> progress) =>
|
||||
private async Task<string> GetPromptBasedOutput(PasteFormats format, string prompt, string input, CancellationToken cancellationToken, IProgress<double> progress) =>
|
||||
format switch
|
||||
{
|
||||
PasteFormats.CustomTextTransformation => (await _customActionTransformService.TransformAsync(prompt, input, imageBytes, cancellationToken, progress))?.Content ?? string.Empty,
|
||||
PasteFormats.CustomTextTransformation => (await _customActionTransformService.TransformTextAsync(prompt, input, cancellationToken, progress))?.Content ?? string.Empty,
|
||||
_ => throw new ArgumentException($"Unsupported format {format} for prompt transform", nameof(format)),
|
||||
};
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ public sealed class PasteFormatExecutor(IKernelService kernelService, ICustomAct
|
||||
pasteFormat.Format switch
|
||||
{
|
||||
PasteFormats.KernelQuery => await _kernelService.TransformClipboardAsync(pasteFormat.Prompt, clipboardData, pasteFormat.IsSavedQuery, cancellationToken, progress),
|
||||
PasteFormats.CustomTextTransformation => DataPackageHelpers.CreateFromText((await _customActionTransformService.TransformAsync(pasteFormat.Prompt, await clipboardData.GetTextOrHtmlTextAsync(), await clipboardData.GetImageAsPngBytesAsync(), cancellationToken, progress))?.Content ?? string.Empty),
|
||||
PasteFormats.CustomTextTransformation => DataPackageHelpers.CreateFromText((await _customActionTransformService.TransformTextAsync(pasteFormat.Prompt, await clipboardData.GetClipboardTextOrThrowAsync(cancellationToken), cancellationToken, progress))?.Content ?? string.Empty),
|
||||
_ => await TransformHelpers.TransformAsync(format, clipboardData, cancellationToken, progress),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -45,7 +45,6 @@ namespace AdvancedPaste.ViewModels
|
||||
private CancellationTokenSource _pasteActionCancellationTokenSource;
|
||||
|
||||
private string _currentClipboardHistoryId;
|
||||
private uint _lastClipboardSequenceNumber;
|
||||
private DateTimeOffset? _currentClipboardTimestamp;
|
||||
private ClipboardFormat _lastClipboardFormats = ClipboardFormat.None;
|
||||
private bool _clipboardHistoryUnavailableLogged;
|
||||
@@ -456,7 +455,6 @@ namespace AdvancedPaste.ViewModels
|
||||
{
|
||||
ResetClipboardPreview();
|
||||
_currentClipboardHistoryId = null;
|
||||
_lastClipboardSequenceNumber = 0;
|
||||
_currentClipboardTimestamp = null;
|
||||
_lastClipboardFormats = ClipboardFormat.None;
|
||||
return;
|
||||
@@ -479,13 +477,6 @@ namespace AdvancedPaste.ViewModels
|
||||
{
|
||||
bool clipboardChanged = formatsChanged;
|
||||
|
||||
var currentSequenceNumber = NativeMethods.GetClipboardSequenceNumber();
|
||||
if (_lastClipboardSequenceNumber != currentSequenceNumber)
|
||||
{
|
||||
clipboardChanged = true;
|
||||
_lastClipboardSequenceNumber = currentSequenceNumber;
|
||||
}
|
||||
|
||||
if (Clipboard.IsHistoryEnabled())
|
||||
{
|
||||
try
|
||||
|
||||
@@ -312,39 +312,13 @@ private:
|
||||
return false;
|
||||
}
|
||||
|
||||
void read_settings(PowerToysSettings::PowerToyValues& settings)
|
||||
void read_settings(PowerToysSettings::PowerToyValues& settings)
|
||||
{
|
||||
const auto settingsObject = settings.get_raw_json();
|
||||
|
||||
// Migrate Paste As Plain text shortcut
|
||||
Hotkey old_paste_as_plain_hotkey;
|
||||
bool old_data_migrated = migrate_data_and_remove_data_file(old_paste_as_plain_hotkey);
|
||||
|
||||
if (settingsObject.GetView().Size())
|
||||
{
|
||||
const auto propertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES);
|
||||
|
||||
m_is_advanced_ai_enabled = has_advanced_ai_provider(propertiesObject);
|
||||
|
||||
if (propertiesObject.HasKey(JSON_KEY_IS_AI_ENABLED))
|
||||
{
|
||||
m_is_ai_enabled = propertiesObject.GetNamedObject(JSON_KEY_IS_AI_ENABLED).GetNamedBoolean(JSON_KEY_VALUE, false);
|
||||
}
|
||||
else if (propertiesObject.HasKey(JSON_KEY_IS_OPEN_AI_ENABLED))
|
||||
{
|
||||
m_is_ai_enabled = propertiesObject.GetNamedObject(JSON_KEY_IS_OPEN_AI_ENABLED).GetNamedBoolean(JSON_KEY_VALUE, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_is_ai_enabled = false;
|
||||
}
|
||||
|
||||
if (propertiesObject.HasKey(JSON_KEY_SHOW_CUSTOM_PREVIEW))
|
||||
{
|
||||
m_preview_custom_format_output = propertiesObject.GetNamedObject(JSON_KEY_SHOW_CUSTOM_PREVIEW).GetNamedBoolean(JSON_KEY_VALUE);
|
||||
}
|
||||
}
|
||||
|
||||
if (old_data_migrated)
|
||||
{
|
||||
m_paste_as_plain_hotkey = old_paste_as_plain_hotkey;
|
||||
@@ -431,6 +405,31 @@ private:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (settingsObject.GetView().Size())
|
||||
{
|
||||
const auto propertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES);
|
||||
|
||||
m_is_advanced_ai_enabled = has_advanced_ai_provider(propertiesObject);
|
||||
|
||||
if (propertiesObject.HasKey(JSON_KEY_IS_AI_ENABLED))
|
||||
{
|
||||
m_is_ai_enabled = propertiesObject.GetNamedObject(JSON_KEY_IS_AI_ENABLED).GetNamedBoolean(JSON_KEY_VALUE, false);
|
||||
}
|
||||
else if (propertiesObject.HasKey(JSON_KEY_IS_OPEN_AI_ENABLED))
|
||||
{
|
||||
m_is_ai_enabled = propertiesObject.GetNamedObject(JSON_KEY_IS_OPEN_AI_ENABLED).GetNamedBoolean(JSON_KEY_VALUE, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_is_ai_enabled = false;
|
||||
}
|
||||
|
||||
if (propertiesObject.HasKey(JSON_KEY_SHOW_CUSTOM_PREVIEW))
|
||||
{
|
||||
m_preview_custom_format_output = propertiesObject.GetNamedObject(JSON_KEY_SHOW_CUSTOM_PREVIEW).GetNamedBoolean(JSON_KEY_VALUE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load the settings file.
|
||||
|
||||
@@ -8,7 +8,6 @@ using Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Models;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Windows.ApplicationModel.DataTransfer;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.ViewModels;
|
||||
|
||||
@@ -17,8 +16,6 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
{
|
||||
public ExtensionObject<ICommandItem> Model => _commandItemModel;
|
||||
|
||||
private ExtensionObject<IExtendedAttributesProvider>? ExtendedAttributesProvider { get; set; }
|
||||
|
||||
private readonly ExtensionObject<ICommandItem> _commandItemModel = new(null);
|
||||
private CommandContextItemViewModel? _defaultCommandContextItemViewModel;
|
||||
|
||||
@@ -68,8 +65,6 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
|
||||
public bool ShouldBeVisible => !string.IsNullOrEmpty(Name);
|
||||
|
||||
public DataPackageView? DataPackage { get; private set; }
|
||||
|
||||
public List<IContextItemViewModel> AllCommands
|
||||
{
|
||||
get
|
||||
@@ -162,13 +157,6 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
// will never be able to load Hotkeys & aliases
|
||||
UpdateProperty(nameof(IsInitialized));
|
||||
|
||||
if (model is IExtendedAttributesProvider extendedAttributesProvider)
|
||||
{
|
||||
ExtendedAttributesProvider = new ExtensionObject<IExtendedAttributesProvider>(extendedAttributesProvider);
|
||||
var properties = extendedAttributesProvider.GetProperties();
|
||||
UpdateDataPackage(properties);
|
||||
}
|
||||
|
||||
Initialized |= InitializedState.Initialized;
|
||||
}
|
||||
|
||||
@@ -391,9 +379,6 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
UpdateProperty(nameof(SecondaryCommandName));
|
||||
UpdateProperty(nameof(HasMoreCommands));
|
||||
|
||||
break;
|
||||
case nameof(DataPackage):
|
||||
UpdateDataPackage(ExtendedAttributesProvider?.Unsafe?.GetProperties());
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -446,16 +431,6 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
UpdateProperty(nameof(Icon));
|
||||
}
|
||||
|
||||
private void UpdateDataPackage(IDictionary<string, object?>? properties)
|
||||
{
|
||||
DataPackage =
|
||||
properties?.TryGetValue(WellKnownExtensionAttributes.DataPackage, out var dataPackageView) == true &&
|
||||
dataPackageView is DataPackageView view
|
||||
? view
|
||||
: null;
|
||||
UpdateProperty(nameof(DataPackage));
|
||||
}
|
||||
|
||||
protected override void UnsafeCleanup()
|
||||
{
|
||||
base.UnsafeCleanup();
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Models;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Windows.Storage.Streams;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.ViewModels;
|
||||
@@ -58,7 +57,7 @@ public partial class IconDataViewModel : ObservableObject, IIconData
|
||||
// because each call to GetProperties() is a cross process hop, and if you
|
||||
// marshal-by-value the property set, then you don't want to throw it away and
|
||||
// re-marshal it for every property. MAKE SURE YOU CACHE IT.
|
||||
if (props?.TryGetValue(WellKnownExtensionAttributes.FontFamily, out var family) ?? false)
|
||||
if (props?.TryGetValue("FontFamily", out var family) ?? false)
|
||||
{
|
||||
FontFamily = family as string;
|
||||
}
|
||||
|
||||
@@ -24,8 +24,6 @@ public partial class ListItemViewModel : CommandItemViewModel
|
||||
|
||||
public string Section { get; private set; } = string.Empty;
|
||||
|
||||
public bool IsSectionOrSeparator { get; private set; }
|
||||
|
||||
public DetailsViewModel? Details { get; private set; }
|
||||
|
||||
[MemberNotNullWhen(true, nameof(Details))]
|
||||
@@ -84,18 +82,14 @@ public partial class ListItemViewModel : CommandItemViewModel
|
||||
}
|
||||
|
||||
UpdateTags(li.Tags);
|
||||
|
||||
Section = li.Section ?? string.Empty;
|
||||
IsSectionOrSeparator = IsSeparator(li);
|
||||
UpdateProperty(nameof(Section), nameof(IsSectionOrSeparator));
|
||||
|
||||
UpdateProperty(nameof(Section));
|
||||
|
||||
UpdateAccessibleName();
|
||||
}
|
||||
|
||||
private bool IsSeparator(IListItem item)
|
||||
{
|
||||
return item.Command is null;
|
||||
}
|
||||
|
||||
public override void SlowInitializeProperties()
|
||||
{
|
||||
base.SlowInitializeProperties();
|
||||
@@ -110,7 +104,8 @@ public partial class ListItemViewModel : CommandItemViewModel
|
||||
{
|
||||
Details = new(extensionDetails, PageContext);
|
||||
Details.InitializeProperties();
|
||||
UpdateProperty(nameof(Details), nameof(HasDetails));
|
||||
UpdateProperty(nameof(Details));
|
||||
UpdateProperty(nameof(HasDetails));
|
||||
}
|
||||
|
||||
AddShowDetailsCommands();
|
||||
@@ -140,18 +135,14 @@ public partial class ListItemViewModel : CommandItemViewModel
|
||||
break;
|
||||
case nameof(model.Section):
|
||||
Section = model.Section ?? string.Empty;
|
||||
IsSectionOrSeparator = IsSeparator(model);
|
||||
UpdateProperty(nameof(Section), nameof(IsSectionOrSeparator));
|
||||
UpdateProperty(nameof(Section));
|
||||
break;
|
||||
case nameof(model.Command):
|
||||
IsSectionOrSeparator = IsSeparator(model);
|
||||
UpdateProperty(nameof(IsSectionOrSeparator));
|
||||
break;
|
||||
case nameof(Details):
|
||||
case nameof(model.Details):
|
||||
var extensionDetails = model.Details;
|
||||
Details = extensionDetails is not null ? new(extensionDetails, PageContext) : null;
|
||||
Details?.InitializeProperties();
|
||||
UpdateProperty(nameof(Details), nameof(HasDetails));
|
||||
UpdateProperty(nameof(Details));
|
||||
UpdateProperty(nameof(HasDetails));
|
||||
UpdateShowDetailsCommand();
|
||||
break;
|
||||
case nameof(model.MoreCommands):
|
||||
@@ -203,7 +194,8 @@ public partial class ListItemViewModel : CommandItemViewModel
|
||||
MoreCommands.Add(showDetailsContextItemViewModel);
|
||||
}
|
||||
|
||||
UpdateProperty(nameof(MoreCommands), nameof(AllCommands));
|
||||
UpdateProperty(nameof(MoreCommands));
|
||||
UpdateProperty(nameof(AllCommands));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,7 +227,8 @@ public partial class ListItemViewModel : CommandItemViewModel
|
||||
showDetailsContextItemViewModel.SlowInitializeProperties();
|
||||
MoreCommands.Add(showDetailsContextItemViewModel);
|
||||
|
||||
UpdateProperty(nameof(MoreCommands), nameof(AllCommands));
|
||||
UpdateProperty(nameof(MoreCommands));
|
||||
UpdateProperty(nameof(AllCommands));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,14 +3,13 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.ViewModels;
|
||||
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
|
||||
public partial class SeparatorViewModel() :
|
||||
CommandItem,
|
||||
IContextItemViewModel,
|
||||
IFilterItemViewModel,
|
||||
ISeparatorContextItem,
|
||||
|
||||
@@ -18,7 +18,7 @@ using WyHash;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IExtendedAttributesProvider
|
||||
public sealed partial class TopLevelViewModel : ObservableObject, IListItem
|
||||
{
|
||||
private readonly SettingsModel _settings;
|
||||
private readonly ProviderSettings _providerSettings;
|
||||
@@ -232,13 +232,6 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
|
||||
{
|
||||
UpdateInitialIcon();
|
||||
}
|
||||
else if (e.PropertyName == nameof(CommandItem.DataPackage))
|
||||
{
|
||||
DoOnUiThread(() =>
|
||||
{
|
||||
OnPropertyChanged(nameof(CommandItem.DataPackage));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -401,12 +394,4 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
|
||||
{
|
||||
return $"{nameof(TopLevelViewModel)}: {Id} ({Title}) - display: {DisplayTitle} - fallback: {IsFallback} - enabled: {IsEnabled}";
|
||||
}
|
||||
|
||||
public IDictionary<string, object?> GetProperties()
|
||||
{
|
||||
return new Dictionary<string, object?>
|
||||
{
|
||||
[WellKnownExtensionAttributes.DataPackage] = _commandItemViewModel?.DataPackage,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -368,69 +368,32 @@ internal sealed partial class BlurImageControl : Control
|
||||
{
|
||||
try
|
||||
{
|
||||
if (imageSource is not Microsoft.UI.Xaml.Media.Imaging.BitmapImage bitmapImage)
|
||||
if (imageSource is Microsoft.UI.Xaml.Media.Imaging.BitmapImage bitmapImage)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_imageBrush ??= _compositor?.CreateSurfaceBrush();
|
||||
if (_imageBrush is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_imageBrush ??= _compositor?.CreateSurfaceBrush();
|
||||
if (_imageBrush is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var loadedSurface = LoadedImageSurface.StartLoadFromUri(bitmapImage.UriSource);
|
||||
loadedSurface.LoadCompleted += (_, _) =>
|
||||
{
|
||||
if (_imageBrush is not null)
|
||||
{
|
||||
_imageBrush.Surface = loadedSurface;
|
||||
_imageBrush.Stretch = ConvertStretch(ImageStretch);
|
||||
_imageBrush.BitmapInterpolationMode = CompositionBitmapInterpolationMode.Linear;
|
||||
}
|
||||
};
|
||||
|
||||
Logger.LogDebug($"Starting load of BlurImageControl from '{bitmapImage.UriSource}'");
|
||||
var loadedSurface = LoadedImageSurface.StartLoadFromUri(bitmapImage.UriSource);
|
||||
loadedSurface.LoadCompleted += OnLoadedSurfaceOnLoadCompleted;
|
||||
SetLoadedSurfaceToBrush(loadedSurface);
|
||||
_effectBrush?.SetSourceParameter(ImageSourceParameterName, _imageBrush);
|
||||
_effectBrush?.SetSourceParameter(ImageSourceParameterName, _imageBrush);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to load image for BlurImageControl: {0}", ex);
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
void OnLoadedSurfaceOnLoadCompleted(LoadedImageSurface loadedSurface, LoadedImageSourceLoadCompletedEventArgs e)
|
||||
{
|
||||
switch (e.Status)
|
||||
{
|
||||
case LoadedImageSourceLoadStatus.Success:
|
||||
Logger.LogDebug($"BlurImageControl loaded successfully: has _imageBrush? {_imageBrush != null}");
|
||||
|
||||
try
|
||||
{
|
||||
SetLoadedSurfaceToBrush(loadedSurface);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to set surface in BlurImageControl", ex);
|
||||
throw;
|
||||
}
|
||||
|
||||
break;
|
||||
case LoadedImageSourceLoadStatus.NetworkError:
|
||||
case LoadedImageSourceLoadStatus.InvalidFormat:
|
||||
case LoadedImageSourceLoadStatus.Other:
|
||||
default:
|
||||
Logger.LogError($"Failed to load image for BlurImageControl: Load status {e.Status}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SetLoadedSurfaceToBrush(LoadedImageSurface loadedSurface)
|
||||
{
|
||||
var surfaceBrush = _imageBrush;
|
||||
if (surfaceBrush is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
surfaceBrush.Surface = loadedSurface;
|
||||
surfaceBrush.Stretch = ConvertStretch(ImageStretch);
|
||||
surfaceBrush.BitmapInterpolationMode = CompositionBitmapInterpolationMode.Linear;
|
||||
}
|
||||
|
||||
private static CompositionStretch ConvertStretch(Stretch stretch)
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Controls;
|
||||
|
||||
internal sealed class UVBounds
|
||||
{
|
||||
public double UMin { get; }
|
||||
|
||||
public double UMax { get; }
|
||||
|
||||
public double VMin { get; }
|
||||
|
||||
public double VMax { get; }
|
||||
|
||||
public UVBounds(Orientation orientation, Rect rect)
|
||||
{
|
||||
if (orientation == Orientation.Horizontal)
|
||||
{
|
||||
UMin = rect.Left;
|
||||
UMax = rect.Right;
|
||||
VMin = rect.Top;
|
||||
VMax = rect.Bottom;
|
||||
}
|
||||
else
|
||||
{
|
||||
UMin = rect.Top;
|
||||
UMax = rect.Bottom;
|
||||
VMin = rect.Left;
|
||||
VMax = rect.Right;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Diagnostics;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Controls;
|
||||
|
||||
[DebuggerDisplay("U = {U} V = {V}")]
|
||||
internal struct UvMeasure
|
||||
{
|
||||
internal double U { get; set; }
|
||||
|
||||
internal double V { get; set; }
|
||||
|
||||
internal static UvMeasure Zero => default(UvMeasure);
|
||||
|
||||
public UvMeasure(Orientation orientation, Size size)
|
||||
: this(orientation, size.Width, size.Height)
|
||||
{
|
||||
}
|
||||
|
||||
public UvMeasure(Orientation orientation, double width, double height)
|
||||
{
|
||||
if (orientation == Orientation.Horizontal)
|
||||
{
|
||||
U = width;
|
||||
V = height;
|
||||
}
|
||||
else
|
||||
{
|
||||
U = height;
|
||||
V = width;
|
||||
}
|
||||
}
|
||||
|
||||
public UvMeasure Add(double u, double v)
|
||||
{
|
||||
UvMeasure result = default(UvMeasure);
|
||||
result.U = U + u;
|
||||
result.V = V + v;
|
||||
return result;
|
||||
}
|
||||
|
||||
public UvMeasure Add(UvMeasure measure)
|
||||
{
|
||||
return Add(measure.U, measure.V);
|
||||
}
|
||||
|
||||
public Size ToSize(Orientation orientation)
|
||||
{
|
||||
if (orientation != Orientation.Horizontal)
|
||||
{
|
||||
return new Size(V, U);
|
||||
}
|
||||
|
||||
return new Size(U, V);
|
||||
}
|
||||
|
||||
public Point GetPoint(Orientation orientation)
|
||||
{
|
||||
return orientation is Orientation.Horizontal ? new Point(U, V) : new Point(V, U);
|
||||
}
|
||||
|
||||
public Size GetSize(Orientation orientation)
|
||||
{
|
||||
return orientation is Orientation.Horizontal ? new Size(U, V) : new Size(V, U);
|
||||
}
|
||||
|
||||
public static bool operator ==(UvMeasure measure1, UvMeasure measure2)
|
||||
{
|
||||
return measure1.U == measure2.U && measure1.V == measure2.V;
|
||||
}
|
||||
|
||||
public static bool operator !=(UvMeasure measure1, UvMeasure measure2)
|
||||
{
|
||||
return !(measure1 == measure2);
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is UvMeasure measure && this == measure;
|
||||
}
|
||||
|
||||
public bool Equals(UvMeasure value)
|
||||
{
|
||||
return this == value;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return base.GetHashCode();
|
||||
}
|
||||
}
|
||||
@@ -1,416 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using CommunityToolkit.WinUI.Controls;
|
||||
using Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// Arranges elements by wrapping them to fit the available space.
|
||||
/// When <see cref="Orientation"/> is set to Orientation.Horizontal, element are arranged in rows until the available width is reached and then to a new row.
|
||||
/// When <see cref="Orientation"/> is set to Orientation.Vertical, element are arranged in columns until the available height is reached.
|
||||
/// </summary>
|
||||
public sealed partial class WrapPanel : Panel
|
||||
{
|
||||
private struct UvRect
|
||||
{
|
||||
public UvMeasure Position { get; set; }
|
||||
|
||||
public UvMeasure Size { get; set; }
|
||||
|
||||
public Rect ToRect(Orientation orientation)
|
||||
{
|
||||
return orientation switch
|
||||
{
|
||||
Orientation.Vertical => new Rect(Position.V, Position.U, Size.V, Size.U),
|
||||
Orientation.Horizontal => new Rect(Position.U, Position.V, Size.U, Size.V),
|
||||
_ => ThrowArgumentException(),
|
||||
};
|
||||
}
|
||||
|
||||
private static Rect ThrowArgumentException()
|
||||
{
|
||||
throw new ArgumentException("The input orientation is not valid.");
|
||||
}
|
||||
}
|
||||
|
||||
private struct Row
|
||||
{
|
||||
public List<UvRect> ChildrenRects { get; }
|
||||
|
||||
public UvMeasure Size { get; set; }
|
||||
|
||||
public UvRect Rect
|
||||
{
|
||||
get
|
||||
{
|
||||
UvRect result;
|
||||
if (ChildrenRects.Count <= 0)
|
||||
{
|
||||
result = default(UvRect);
|
||||
result.Position = UvMeasure.Zero;
|
||||
result.Size = Size;
|
||||
return result;
|
||||
}
|
||||
|
||||
result = default(UvRect);
|
||||
result.Position = ChildrenRects.First().Position;
|
||||
result.Size = Size;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public Row(List<UvRect> childrenRects, UvMeasure size)
|
||||
{
|
||||
ChildrenRects = childrenRects;
|
||||
Size = size;
|
||||
}
|
||||
|
||||
public void Add(UvMeasure position, UvMeasure size)
|
||||
{
|
||||
ChildrenRects.Add(new UvRect
|
||||
{
|
||||
Position = position,
|
||||
Size = size,
|
||||
});
|
||||
|
||||
Size = new UvMeasure
|
||||
{
|
||||
U = position.U + size.U,
|
||||
V = Math.Max(Size.V, size.V),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a uniform Horizontal distance (in pixels) between items when <see cref="Orientation"/> is set to Horizontal,
|
||||
/// or between columns of items when <see cref="Orientation"/> is set to Vertical.
|
||||
/// </summary>
|
||||
public double HorizontalSpacing
|
||||
{
|
||||
get { return (double)GetValue(HorizontalSpacingProperty); }
|
||||
set { SetValue(HorizontalSpacingProperty, value); }
|
||||
}
|
||||
|
||||
private bool IsSectionItem(UIElement element) => element is FrameworkElement fe && fe.DataContext is ListItemViewModel item && item.IsSectionOrSeparator;
|
||||
|
||||
/// <summary>
|
||||
/// Identifies the <see cref="HorizontalSpacing"/> dependency property.
|
||||
/// </summary>
|
||||
public static readonly DependencyProperty HorizontalSpacingProperty =
|
||||
DependencyProperty.Register(
|
||||
nameof(HorizontalSpacing),
|
||||
typeof(double),
|
||||
typeof(WrapPanel),
|
||||
new PropertyMetadata(0d, LayoutPropertyChanged));
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a uniform Vertical distance (in pixels) between items when <see cref="Orientation"/> is set to Vertical,
|
||||
/// or between rows of items when <see cref="Orientation"/> is set to Horizontal.
|
||||
/// </summary>
|
||||
public double VerticalSpacing
|
||||
{
|
||||
get { return (double)GetValue(VerticalSpacingProperty); }
|
||||
set { SetValue(VerticalSpacingProperty, value); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Identifies the <see cref="VerticalSpacing"/> dependency property.
|
||||
/// </summary>
|
||||
public static readonly DependencyProperty VerticalSpacingProperty =
|
||||
DependencyProperty.Register(
|
||||
nameof(VerticalSpacing),
|
||||
typeof(double),
|
||||
typeof(WrapPanel),
|
||||
new PropertyMetadata(0d, LayoutPropertyChanged));
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the orientation of the WrapPanel.
|
||||
/// Horizontal means that child controls will be added horizontally until the width of the panel is reached, then a new row is added to add new child controls.
|
||||
/// Vertical means that children will be added vertically until the height of the panel is reached, then a new column is added.
|
||||
/// </summary>
|
||||
public Orientation Orientation
|
||||
{
|
||||
get { return (Orientation)GetValue(OrientationProperty); }
|
||||
set { SetValue(OrientationProperty, value); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Identifies the <see cref="Orientation"/> dependency property.
|
||||
/// </summary>
|
||||
public static readonly DependencyProperty OrientationProperty =
|
||||
DependencyProperty.Register(
|
||||
nameof(Orientation),
|
||||
typeof(Orientation),
|
||||
typeof(WrapPanel),
|
||||
new PropertyMetadata(Orientation.Horizontal, LayoutPropertyChanged));
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the distance between the border and its child object.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// The dimensions of the space between the border and its child as a Thickness value.
|
||||
/// Thickness is a structure that stores dimension values using pixel measures.
|
||||
/// </returns>
|
||||
public Thickness Padding
|
||||
{
|
||||
get { return (Thickness)GetValue(PaddingProperty); }
|
||||
set { SetValue(PaddingProperty, value); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Identifies the Padding dependency property.
|
||||
/// </summary>
|
||||
/// <returns>The identifier for the <see cref="Padding"/> dependency property.</returns>
|
||||
public static readonly DependencyProperty PaddingProperty =
|
||||
DependencyProperty.Register(
|
||||
nameof(Padding),
|
||||
typeof(Thickness),
|
||||
typeof(WrapPanel),
|
||||
new PropertyMetadata(default(Thickness), LayoutPropertyChanged));
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating how to arrange child items
|
||||
/// </summary>
|
||||
public StretchChild StretchChild
|
||||
{
|
||||
get { return (StretchChild)GetValue(StretchChildProperty); }
|
||||
set { SetValue(StretchChildProperty, value); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Identifies the <see cref="StretchChild"/> dependency property.
|
||||
/// </summary>
|
||||
/// <returns>The identifier for the <see cref="StretchChild"/> dependency property.</returns>
|
||||
public static readonly DependencyProperty StretchChildProperty =
|
||||
DependencyProperty.Register(
|
||||
nameof(StretchChild),
|
||||
typeof(StretchChild),
|
||||
typeof(WrapPanel),
|
||||
new PropertyMetadata(StretchChild.None, LayoutPropertyChanged));
|
||||
|
||||
/// <summary>
|
||||
/// Identifies the IsFullLine attached dependency property.
|
||||
/// If true, the child element will occupy the entire width of the panel and force a line break before and after itself.
|
||||
/// </summary>
|
||||
public static readonly DependencyProperty IsFullLineProperty =
|
||||
DependencyProperty.RegisterAttached(
|
||||
"IsFullLine",
|
||||
typeof(bool),
|
||||
typeof(WrapPanel),
|
||||
new PropertyMetadata(false, OnIsFullLineChanged));
|
||||
|
||||
public static bool GetIsFullLine(DependencyObject obj)
|
||||
{
|
||||
return (bool)obj.GetValue(IsFullLineProperty);
|
||||
}
|
||||
|
||||
public static void SetIsFullLine(DependencyObject obj, bool value)
|
||||
{
|
||||
obj.SetValue(IsFullLineProperty, value);
|
||||
}
|
||||
|
||||
private static void OnIsFullLineChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (FindVisualParentWrapPanel(d) is WrapPanel wp)
|
||||
{
|
||||
wp.InvalidateMeasure();
|
||||
}
|
||||
}
|
||||
|
||||
private static WrapPanel? FindVisualParentWrapPanel(DependencyObject child)
|
||||
{
|
||||
var parent = Microsoft.UI.Xaml.Media.VisualTreeHelper.GetParent(child);
|
||||
|
||||
while (parent != null)
|
||||
{
|
||||
if (parent is WrapPanel wrapPanel)
|
||||
{
|
||||
return wrapPanel;
|
||||
}
|
||||
|
||||
parent = Microsoft.UI.Xaml.Media.VisualTreeHelper.GetParent(parent);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void LayoutPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is WrapPanel wp)
|
||||
{
|
||||
wp.InvalidateMeasure();
|
||||
wp.InvalidateArrange();
|
||||
}
|
||||
}
|
||||
|
||||
private readonly List<Row> _rows = new List<Row>();
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Size MeasureOverride(Size availableSize)
|
||||
{
|
||||
var childAvailableSize = new Size(
|
||||
availableSize.Width - Padding.Left - Padding.Right,
|
||||
availableSize.Height - Padding.Top - Padding.Bottom);
|
||||
foreach (var child in Children)
|
||||
{
|
||||
child.Measure(childAvailableSize);
|
||||
}
|
||||
|
||||
var requiredSize = UpdateRows(availableSize);
|
||||
return requiredSize;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Size ArrangeOverride(Size finalSize)
|
||||
{
|
||||
if ((Orientation == Orientation.Horizontal && finalSize.Width < DesiredSize.Width) ||
|
||||
(Orientation == Orientation.Vertical && finalSize.Height < DesiredSize.Height))
|
||||
{
|
||||
// We haven't received our desired size. We need to refresh the rows.
|
||||
UpdateRows(finalSize);
|
||||
}
|
||||
|
||||
if (_rows.Count > 0)
|
||||
{
|
||||
// Now that we have all the data, we do the actual arrange pass
|
||||
var childIndex = 0;
|
||||
foreach (var row in _rows)
|
||||
{
|
||||
foreach (var rect in row.ChildrenRects)
|
||||
{
|
||||
var child = Children[childIndex++];
|
||||
while (child.Visibility == Visibility.Collapsed)
|
||||
{
|
||||
// Collapsed children are not added into the rows,
|
||||
// we skip them.
|
||||
child = Children[childIndex++];
|
||||
}
|
||||
|
||||
var arrangeRect = new UvRect
|
||||
{
|
||||
Position = rect.Position,
|
||||
Size = new UvMeasure { U = rect.Size.U, V = row.Size.V },
|
||||
};
|
||||
|
||||
var finalRect = arrangeRect.ToRect(Orientation);
|
||||
child.Arrange(finalRect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return finalSize;
|
||||
}
|
||||
|
||||
private Size UpdateRows(Size availableSize)
|
||||
{
|
||||
_rows.Clear();
|
||||
|
||||
var paddingStart = new UvMeasure(Orientation, Padding.Left, Padding.Top);
|
||||
var paddingEnd = new UvMeasure(Orientation, Padding.Right, Padding.Bottom);
|
||||
|
||||
if (Children.Count == 0)
|
||||
{
|
||||
return paddingStart.Add(paddingEnd).ToSize(Orientation);
|
||||
}
|
||||
|
||||
var parentMeasure = new UvMeasure(Orientation, availableSize.Width, availableSize.Height);
|
||||
var spacingMeasure = new UvMeasure(Orientation, HorizontalSpacing, VerticalSpacing);
|
||||
var position = new UvMeasure(Orientation, Padding.Left, Padding.Top);
|
||||
|
||||
var currentRow = new Row(new List<UvRect>(), default);
|
||||
var finalMeasure = new UvMeasure(Orientation, width: 0.0, height: 0.0);
|
||||
|
||||
void CommitRow()
|
||||
{
|
||||
// Only adds if the row has a content
|
||||
if (currentRow.ChildrenRects.Count > 0)
|
||||
{
|
||||
_rows.Add(currentRow);
|
||||
|
||||
position.V += currentRow.Size.V + spacingMeasure.V;
|
||||
}
|
||||
|
||||
position.U = paddingStart.U;
|
||||
|
||||
currentRow = new Row(new List<UvRect>(), default);
|
||||
}
|
||||
|
||||
void Arrange(UIElement child, bool isLast = false)
|
||||
{
|
||||
if (child.Visibility == Visibility.Collapsed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var isFullLine = IsSectionItem(child);
|
||||
var desiredMeasure = new UvMeasure(Orientation, child.DesiredSize);
|
||||
|
||||
if (isFullLine)
|
||||
{
|
||||
if (currentRow.ChildrenRects.Count > 0)
|
||||
{
|
||||
CommitRow();
|
||||
}
|
||||
|
||||
// Forces the width to fill all the available space
|
||||
// (Total width - Padding Left - Padding Right)
|
||||
desiredMeasure.U = parentMeasure.U - paddingStart.U - paddingEnd.U;
|
||||
|
||||
// Adds the Section Header to the row
|
||||
currentRow.Add(position, desiredMeasure);
|
||||
|
||||
// Updates the global measures
|
||||
position.U += desiredMeasure.U + spacingMeasure.U;
|
||||
finalMeasure.U = Math.Max(finalMeasure.U, position.U);
|
||||
|
||||
CommitRow();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Checks if the item can fit in the row
|
||||
if ((desiredMeasure.U + position.U + paddingEnd.U) > parentMeasure.U)
|
||||
{
|
||||
CommitRow();
|
||||
}
|
||||
|
||||
if (isLast)
|
||||
{
|
||||
desiredMeasure.U = parentMeasure.U - position.U;
|
||||
}
|
||||
|
||||
currentRow.Add(position, desiredMeasure);
|
||||
|
||||
position.U += desiredMeasure.U + spacingMeasure.U;
|
||||
finalMeasure.U = Math.Max(finalMeasure.U, position.U);
|
||||
}
|
||||
}
|
||||
|
||||
var lastIndex = Children.Count - 1;
|
||||
for (var i = 0; i < lastIndex; i++)
|
||||
{
|
||||
Arrange(Children[i]);
|
||||
}
|
||||
|
||||
Arrange(Children[lastIndex], StretchChild == StretchChild.Last);
|
||||
|
||||
if (currentRow.ChildrenRects.Count > 0)
|
||||
{
|
||||
_rows.Add(currentRow);
|
||||
}
|
||||
|
||||
if (_rows.Count == 0)
|
||||
{
|
||||
return paddingStart.Add(paddingEnd).ToSize(Orientation);
|
||||
}
|
||||
|
||||
var lastRowRect = _rows.Last().Rect;
|
||||
finalMeasure.V = lastRowRect.Position.V + lastRowRect.Size.V;
|
||||
return finalMeasure.Add(paddingEnd).ToSize(Orientation);
|
||||
}
|
||||
}
|
||||
@@ -18,23 +18,8 @@ internal sealed partial class GridItemTemplateSelector : DataTemplateSelector
|
||||
|
||||
public DataTemplate? Gallery { get; set; }
|
||||
|
||||
public DataTemplate? Section { get; set; }
|
||||
|
||||
public DataTemplate? Separator { get; set; }
|
||||
|
||||
protected override DataTemplate? SelectTemplateCore(object item, DependencyObject dependencyObject)
|
||||
{
|
||||
if (item is ListItemViewModel element && element.IsSectionOrSeparator)
|
||||
{
|
||||
if (dependencyObject is UIElement li)
|
||||
{
|
||||
li.IsTabStop = false;
|
||||
li.IsHitTestVisible = false;
|
||||
}
|
||||
|
||||
return string.IsNullOrWhiteSpace(element.Section) ? Separator : Section;
|
||||
}
|
||||
|
||||
return GridProperties switch
|
||||
{
|
||||
SmallGridPropertiesViewModel => Small,
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace Microsoft.CmdPal.UI;
|
||||
|
||||
public sealed partial class ListItemTemplateSelector : DataTemplateSelector
|
||||
{
|
||||
public DataTemplate? ListItem { get; set; }
|
||||
|
||||
public DataTemplate? Separator { get; set; }
|
||||
|
||||
public DataTemplate? Section { get; set; }
|
||||
|
||||
protected override DataTemplate? SelectTemplateCore(object item, DependencyObject container)
|
||||
{
|
||||
DataTemplate? dataTemplate = ListItem;
|
||||
|
||||
if (container is ListViewItem listItem)
|
||||
{
|
||||
if (item is ListItemViewModel element)
|
||||
{
|
||||
if (container is ListViewItem li && element.IsSectionOrSeparator)
|
||||
{
|
||||
li.IsEnabled = false;
|
||||
li.AllowFocusWhenDisabled = false;
|
||||
li.AllowFocusOnInteraction = false;
|
||||
li.IsHitTestVisible = false;
|
||||
dataTemplate = string.IsNullOrWhiteSpace(element.Section) ? Separator : Section;
|
||||
}
|
||||
else
|
||||
{
|
||||
listItem.IsEnabled = true;
|
||||
listItem.AllowFocusWhenDisabled = true;
|
||||
listItem.AllowFocusOnInteraction = true;
|
||||
listItem.IsHitTestVisible = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return dataTemplate;
|
||||
}
|
||||
}
|
||||
@@ -28,8 +28,6 @@
|
||||
<CornerRadius x:Key="MediumGridViewItemCornerRadius">8</CornerRadius>
|
||||
|
||||
<Style x:Key="IconGridViewItemStyle" TargetType="GridViewItem">
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="GridViewItem">
|
||||
@@ -92,8 +90,6 @@
|
||||
</Style>
|
||||
|
||||
<Style x:Key="GalleryGridViewItemStyle" TargetType="GridViewItem">
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="GridViewItem">
|
||||
@@ -172,17 +168,8 @@
|
||||
Gallery="{StaticResource GalleryGridItemViewModelTemplate}"
|
||||
GridProperties="{x:Bind ViewModel.GridProperties, Mode=OneWay}"
|
||||
Medium="{StaticResource MediumGridItemViewModelTemplate}"
|
||||
Section="{StaticResource ListSectionViewModelTemplate}"
|
||||
Separator="{StaticResource ListSeparatorViewModelTemplate}"
|
||||
Small="{StaticResource SmallGridItemViewModelTemplate}" />
|
||||
|
||||
<cmdpalUI:ListItemTemplateSelector
|
||||
x:Key="ListItemTemplateSelector"
|
||||
x:DataType="coreViewModels:ListItemViewModel"
|
||||
ListItem="{StaticResource ListItemViewModelTemplate}"
|
||||
Section="{StaticResource ListSectionViewModelTemplate}"
|
||||
Separator="{StaticResource ListSeparatorViewModelTemplate}" />
|
||||
|
||||
<cmdpalUI:GridItemContainerStyleSelector
|
||||
x:Key="GridItemContainerStyleSelector"
|
||||
Gallery="{StaticResource GalleryGridViewItemStyle}"
|
||||
@@ -254,46 +241,12 @@
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="ListSeparatorViewModelTemplate" x:DataType="coreViewModels:ListItemViewModel">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Rectangle
|
||||
Grid.Column="1"
|
||||
Height="1"
|
||||
Margin="0,2,0,2"
|
||||
Fill="{ThemeResource DividerStrokeColorDefaultBrush}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="ListSectionViewModelTemplate" x:DataType="coreViewModels:ListItemViewModel">
|
||||
<Grid
|
||||
Margin="0"
|
||||
VerticalAlignment="Center"
|
||||
cpcontrols:WrapPanel.IsFullLine="True"
|
||||
ColumnSpacing="8"
|
||||
IsTabStop="False"
|
||||
IsTapEnabled="True">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock
|
||||
Grid.Column="0"
|
||||
Foreground="{ThemeResource TextFillColorDisabled}"
|
||||
Style="{ThemeResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind Section}" />
|
||||
<Rectangle
|
||||
Grid.Column="1"
|
||||
Height="1"
|
||||
Fill="{ThemeResource DividerStrokeColorDefaultBrush}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
<!-- Grid item templates for visual grid representation -->
|
||||
<DataTemplate x:Key="SmallGridItemViewModelTemplate" x:DataType="coreViewModels:ListItemViewModel">
|
||||
<StackPanel
|
||||
Width="60"
|
||||
Height="60"
|
||||
Padding="8,16"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
AutomationProperties.Name="{x:Bind Title, Mode=OneWay}"
|
||||
@@ -312,6 +265,7 @@
|
||||
Foreground="{ThemeResource TextFillColorPrimary}"
|
||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
|
||||
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
|
||||
@@ -439,16 +393,13 @@
|
||||
<ListView
|
||||
x:Name="ItemsList"
|
||||
Padding="0,2,0,0"
|
||||
CanDragItems="True"
|
||||
ContextCanceled="Items_OnContextCanceled"
|
||||
ContextRequested="Items_OnContextRequested"
|
||||
DoubleTapped="Items_DoubleTapped"
|
||||
DragItemsCompleted="Items_DragItemsCompleted"
|
||||
DragItemsStarting="Items_DragItemsStarting"
|
||||
IsDoubleTapEnabled="True"
|
||||
IsItemClickEnabled="True"
|
||||
ItemClick="Items_ItemClick"
|
||||
ItemTemplateSelector="{StaticResource ListItemTemplateSelector}"
|
||||
ItemTemplate="{StaticResource ListItemViewModelTemplate}"
|
||||
ItemsSource="{x:Bind ViewModel.FilteredItems, Mode=OneWay}"
|
||||
RightTapped="Items_RightTapped"
|
||||
SelectionChanged="Items_SelectionChanged">
|
||||
@@ -460,13 +411,10 @@
|
||||
<controls:Case Value="True">
|
||||
<GridView
|
||||
x:Name="ItemsGrid"
|
||||
Padding="16,0"
|
||||
CanDragItems="True"
|
||||
Padding="8"
|
||||
ContextCanceled="Items_OnContextCanceled"
|
||||
ContextRequested="Items_OnContextRequested"
|
||||
DoubleTapped="Items_DoubleTapped"
|
||||
DragItemsCompleted="Items_DragItemsCompleted"
|
||||
DragItemsStarting="Items_DragItemsStarting"
|
||||
IsDoubleTapEnabled="True"
|
||||
IsItemClickEnabled="True"
|
||||
ItemClick="Items_ItemClick"
|
||||
@@ -475,14 +423,10 @@
|
||||
ItemsSource="{x:Bind ViewModel.FilteredItems, Mode=OneWay}"
|
||||
RightTapped="Items_RightTapped"
|
||||
SelectionChanged="Items_SelectionChanged">
|
||||
<GridView.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<cpcontrols:WrapPanel HorizontalSpacing="8" Orientation="Horizontal" />
|
||||
</ItemsPanelTemplate>
|
||||
</GridView.ItemsPanel>
|
||||
<GridView.ItemContainerTransitions>
|
||||
<TransitionCollection />
|
||||
</GridView.ItemContainerTransitions>
|
||||
<GridView.ItemContainerStyle />
|
||||
</GridView>
|
||||
</controls:Case>
|
||||
</controls:SwitchPresenter>
|
||||
|
||||
@@ -18,7 +18,6 @@ using Microsoft.UI.Xaml.Controls.Primitives;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Microsoft.UI.Xaml.Navigation;
|
||||
using Windows.ApplicationModel.DataTransfer;
|
||||
using Windows.Foundation;
|
||||
using Windows.System;
|
||||
|
||||
@@ -77,18 +76,12 @@ public sealed partial class ListPage : Page,
|
||||
|
||||
ViewModel = listViewModel;
|
||||
|
||||
if (e.NavigationMode == NavigationMode.Back)
|
||||
if (e.NavigationMode == NavigationMode.Back
|
||||
|| (e.NavigationMode == NavigationMode.New && ItemView.Items.Count > 0))
|
||||
{
|
||||
// Must dispatch the selection to run at a lower priority; otherwise, GetFirstSelectableIndex
|
||||
// may return an incorrect index because item containers are not yet rendered.
|
||||
_ = DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Low, () =>
|
||||
{
|
||||
var firstUsefulIndex = GetFirstSelectableIndex();
|
||||
if (firstUsefulIndex != -1)
|
||||
{
|
||||
ItemView.SelectedIndex = firstUsefulIndex;
|
||||
}
|
||||
});
|
||||
// Upon navigating _back_ to this page, immediately select the
|
||||
// first item in the list
|
||||
ItemView.SelectedIndex = 0;
|
||||
}
|
||||
|
||||
// RegisterAll isn't AOT compatible
|
||||
@@ -135,29 +128,6 @@ public sealed partial class ListPage : Page,
|
||||
GC.Collect();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the index of the first item in the list that is not a separator.
|
||||
/// Returns -1 if the list is empty or only contains separators.
|
||||
/// </summary>
|
||||
private int GetFirstSelectableIndex()
|
||||
{
|
||||
var items = ItemView.Items;
|
||||
if (items is null || items.Count == 0)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (var i = 0; i < items.Count; i++)
|
||||
{
|
||||
if (!IsSeparator(items[i]))
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "VS is too aggressive at pruning methods bound in XAML")]
|
||||
private void Items_ItemClick(object sender, ItemClickEventArgs e)
|
||||
{
|
||||
@@ -213,33 +183,19 @@ public sealed partial class ListPage : Page,
|
||||
// here, then in Page_ItemsUpdated trying to select that cached item if
|
||||
// it's in the list (otherwise, clear the cache), but that seems
|
||||
// aggressively BODGY for something that mostly just works today.
|
||||
if (ItemView.SelectedItem is not null && !IsSeparator(ItemView.SelectedItem))
|
||||
if (ItemView.SelectedItem is not null)
|
||||
{
|
||||
var items = ItemView.Items;
|
||||
var firstUsefulIndex = GetFirstSelectableIndex();
|
||||
var shouldScroll = false;
|
||||
|
||||
if (e.RemovedItems.Count > 0)
|
||||
{
|
||||
shouldScroll = true;
|
||||
}
|
||||
else if (ItemView.SelectedIndex > firstUsefulIndex)
|
||||
{
|
||||
shouldScroll = true;
|
||||
}
|
||||
|
||||
if (shouldScroll)
|
||||
{
|
||||
ItemView.ScrollIntoView(ItemView.SelectedItem);
|
||||
}
|
||||
ItemView.ScrollIntoView(ItemView.SelectedItem);
|
||||
|
||||
// Automation notification for screen readers
|
||||
var listViewPeer = Microsoft.UI.Xaml.Automation.Peers.ListViewAutomationPeer.CreatePeerForElement(ItemView);
|
||||
if (listViewPeer is not null && li is not null)
|
||||
{
|
||||
var notificationText = li.Title;
|
||||
|
||||
UIHelper.AnnounceActionForAccessibility(
|
||||
ItemsList,
|
||||
li.Title,
|
||||
notificationText,
|
||||
"CommandPaletteSelectedItemChanged");
|
||||
}
|
||||
}
|
||||
@@ -315,7 +271,14 @@ public sealed partial class ListPage : Page,
|
||||
else
|
||||
{
|
||||
// For list views, use simple linear navigation
|
||||
NavigateDown();
|
||||
if (ItemView.SelectedIndex < ItemView.Items.Count - 1)
|
||||
{
|
||||
ItemView.SelectedIndex++;
|
||||
}
|
||||
else
|
||||
{
|
||||
ItemView.SelectedIndex = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,7 +291,15 @@ public sealed partial class ListPage : Page,
|
||||
}
|
||||
else
|
||||
{
|
||||
NavigateUp();
|
||||
// For list views, use simple linear navigation
|
||||
if (ItemView.SelectedIndex > 0)
|
||||
{
|
||||
ItemView.SelectedIndex--;
|
||||
}
|
||||
else
|
||||
{
|
||||
ItemView.SelectedIndex = ItemView.Items.Count - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -395,10 +366,7 @@ public sealed partial class ListPage : Page,
|
||||
if (indexes.Value.CurrentIndex != indexes.Value.TargetIndex)
|
||||
{
|
||||
ItemView.SelectedIndex = indexes.Value.TargetIndex;
|
||||
if (ItemView.SelectedItem is not null)
|
||||
{
|
||||
ItemView.ScrollIntoView(ItemView.SelectedItem);
|
||||
}
|
||||
ItemView.ScrollIntoView(ItemView.SelectedItem);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -413,10 +381,7 @@ public sealed partial class ListPage : Page,
|
||||
if (indexes.Value.CurrentIndex != indexes.Value.TargetIndex)
|
||||
{
|
||||
ItemView.SelectedIndex = indexes.Value.TargetIndex;
|
||||
if (ItemView.SelectedItem is not null)
|
||||
{
|
||||
ItemView.ScrollIntoView(ItemView.SelectedItem);
|
||||
}
|
||||
ItemView.ScrollIntoView(ItemView.SelectedItem);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -559,65 +524,17 @@ public sealed partial class ListPage : Page,
|
||||
// ItemView_SelectionChanged again to give us another chance to change
|
||||
// the selection from null -> something. Better to just update the
|
||||
// selection once, at the end of all the updating.
|
||||
// The selection logic must be deferred to the DispatcherQueue
|
||||
// to ensure the UI has processed the updated ItemsSource binding,
|
||||
// preventing ItemView.Items from appearing empty/null immediately after update.
|
||||
_ = DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Low, () =>
|
||||
if (ItemView.SelectedItem is null)
|
||||
{
|
||||
var items = ItemView.Items;
|
||||
ItemView.SelectedIndex = 0;
|
||||
}
|
||||
|
||||
// If the list is null or empty, clears the selection and return
|
||||
if (items is null || items.Count == 0)
|
||||
{
|
||||
ItemView.SelectedIndex = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
// Finds the first item that is not a separator
|
||||
var firstUsefulIndex = GetFirstSelectableIndex();
|
||||
|
||||
// If there is only separators in the list, don't select anything.
|
||||
if (firstUsefulIndex == -1)
|
||||
{
|
||||
ItemView.SelectedIndex = -1;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var shouldUpdateSelection = false;
|
||||
|
||||
// If it's a top level list update we force the reset to the top useful item
|
||||
if (!sender.IsNested)
|
||||
{
|
||||
shouldUpdateSelection = true;
|
||||
}
|
||||
|
||||
// No current selection or current selection is null
|
||||
else if (ItemView.SelectedItem is null)
|
||||
{
|
||||
shouldUpdateSelection = true;
|
||||
}
|
||||
|
||||
// The current selected item is a separator
|
||||
else if (IsSeparator(ItemView.SelectedItem))
|
||||
{
|
||||
shouldUpdateSelection = true;
|
||||
}
|
||||
|
||||
// The selected item does not exist in the new list
|
||||
else if (!items.Contains(ItemView.SelectedItem))
|
||||
{
|
||||
shouldUpdateSelection = true;
|
||||
}
|
||||
|
||||
if (shouldUpdateSelection)
|
||||
{
|
||||
if (firstUsefulIndex != -1)
|
||||
{
|
||||
ItemView.SelectedIndex = firstUsefulIndex;
|
||||
}
|
||||
}
|
||||
});
|
||||
// Always reset the selected item when the top-level list page changes
|
||||
// its items
|
||||
if (!sender.IsNested)
|
||||
{
|
||||
ItemView.SelectedIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private void ViewModel_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
@@ -687,11 +604,6 @@ public sealed partial class ListPage : Page,
|
||||
continue;
|
||||
}
|
||||
|
||||
if (IsSeparator(ItemView.Items[i]))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ItemView.ContainerFromIndex(i) is FrameworkElement c && c.ActualWidth > 0 && c.ActualHeight > 0)
|
||||
{
|
||||
var p = c.TransformToVisual(ItemView).TransformPoint(new Point(0, 0));
|
||||
@@ -852,185 +764,6 @@ public sealed partial class ListPage : Page,
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Code stealed from <see cref="Controls.ContextMenu.NavigateUp"/>
|
||||
/// </summary>
|
||||
private void NavigateUp()
|
||||
{
|
||||
var newIndex = ItemView.SelectedIndex;
|
||||
|
||||
if (ItemView.SelectedIndex > 0)
|
||||
{
|
||||
newIndex--;
|
||||
|
||||
while (
|
||||
newIndex >= 0 &&
|
||||
IsSeparator(ItemView.Items[newIndex]) &&
|
||||
newIndex != ItemView.SelectedIndex)
|
||||
{
|
||||
newIndex--;
|
||||
}
|
||||
|
||||
if (newIndex < 0)
|
||||
{
|
||||
newIndex = ItemView.Items.Count - 1;
|
||||
|
||||
while (
|
||||
newIndex >= 0 &&
|
||||
IsSeparator(ItemView.Items[newIndex]) &&
|
||||
newIndex != ItemView.SelectedIndex)
|
||||
{
|
||||
newIndex--;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
newIndex = ItemView.Items.Count - 1;
|
||||
}
|
||||
|
||||
ItemView.SelectedIndex = newIndex;
|
||||
}
|
||||
|
||||
private void Items_DragItemsStarting(object sender, DragItemsStartingEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (e.Items.FirstOrDefault() is not ListItemViewModel item || item.DataPackage is null)
|
||||
{
|
||||
e.Cancel = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// copy properties
|
||||
foreach (var (key, value) in item.DataPackage.Properties)
|
||||
{
|
||||
try
|
||||
{
|
||||
e.Data.Properties[key] = value;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// noop - skip any properties that fail
|
||||
}
|
||||
}
|
||||
|
||||
// setup e.Data formats as deferred renderers to read from the item's DataPackage
|
||||
foreach (var format in item.DataPackage.AvailableFormats)
|
||||
{
|
||||
try
|
||||
{
|
||||
e.Data.SetDataProvider(format, request => DelayRenderer(request, item, format));
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// noop - skip any formats that fail
|
||||
}
|
||||
}
|
||||
|
||||
WeakReferenceMessenger.Default.Send(new DragStartedMessage());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send(new DragCompletedMessage());
|
||||
Logger.LogError("Failed to start dragging an item", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static void DelayRenderer(DataProviderRequest request, ListItemViewModel item, string format)
|
||||
{
|
||||
var deferral = request.GetDeferral();
|
||||
try
|
||||
{
|
||||
item.DataPackage?.GetDataAsync(format)
|
||||
.AsTask()
|
||||
.ContinueWith(dataTask =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (dataTask.IsCompletedSuccessfully)
|
||||
{
|
||||
request.SetData(dataTask.Result);
|
||||
}
|
||||
else if (dataTask.IsFaulted && dataTask.Exception is not null)
|
||||
{
|
||||
Logger.LogError($"Failed to get data for format '{format}' during drag-and-drop", dataTask.Exception);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
deferral.Complete();
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to set data for format '{format}' during drag-and-drop", ex);
|
||||
deferral.Complete();
|
||||
}
|
||||
}
|
||||
|
||||
private void Items_DragItemsCompleted(ListViewBase sender, DragItemsCompletedEventArgs args)
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send(new DragCompletedMessage());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Code stealed from <see cref="Controls.ContextMenu.NavigateDown"/>
|
||||
/// </summary>
|
||||
private void NavigateDown()
|
||||
{
|
||||
var newIndex = ItemView.SelectedIndex;
|
||||
|
||||
if (ItemView.SelectedIndex == ItemView.Items.Count - 1)
|
||||
{
|
||||
newIndex = 0;
|
||||
while (
|
||||
newIndex < ItemView.Items.Count &&
|
||||
IsSeparator(ItemView.Items[newIndex]))
|
||||
{
|
||||
newIndex++;
|
||||
}
|
||||
|
||||
if (newIndex >= ItemView.Items.Count)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
newIndex++;
|
||||
|
||||
while (
|
||||
newIndex < ItemView.Items.Count &&
|
||||
IsSeparator(ItemView.Items[newIndex]) &&
|
||||
newIndex != ItemView.SelectedIndex)
|
||||
{
|
||||
newIndex++;
|
||||
}
|
||||
|
||||
if (newIndex >= ItemView.Items.Count)
|
||||
{
|
||||
newIndex = 0;
|
||||
|
||||
while (
|
||||
newIndex < ItemView.Items.Count &&
|
||||
IsSeparator(ItemView.Items[newIndex]) &&
|
||||
newIndex != ItemView.SelectedIndex)
|
||||
{
|
||||
newIndex++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ItemView.SelectedIndex = newIndex;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Code stealed from <see cref="Controls.ContextMenu.IsSeparator(object)"/>
|
||||
/// </summary>
|
||||
private bool IsSeparator(object? item) => item is ListItemViewModel li && li.IsSectionOrSeparator;
|
||||
|
||||
private enum InputSource
|
||||
{
|
||||
None,
|
||||
|
||||
@@ -52,8 +52,6 @@ public sealed partial class MainWindow : WindowEx,
|
||||
IRecipient<ShowWindowMessage>,
|
||||
IRecipient<HideWindowMessage>,
|
||||
IRecipient<QuitMessage>,
|
||||
IRecipient<DragStartedMessage>,
|
||||
IRecipient<DragCompletedMessage>,
|
||||
IDisposable
|
||||
{
|
||||
private const int DefaultWidth = 800;
|
||||
@@ -81,8 +79,6 @@ public sealed partial class MainWindow : WindowEx,
|
||||
|
||||
private WindowPosition _currentWindowPosition = new();
|
||||
|
||||
private bool _preventHideWhenDeactivated;
|
||||
|
||||
private MainWindowViewModel ViewModel { get; }
|
||||
|
||||
public MainWindow()
|
||||
@@ -123,8 +119,6 @@ public sealed partial class MainWindow : WindowEx,
|
||||
WeakReferenceMessenger.Default.Register<QuitMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<ShowWindowMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<HideWindowMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<DragStartedMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<DragCompletedMessage>(this);
|
||||
|
||||
// Hide our titlebar.
|
||||
// We need to both ExtendsContentIntoTitleBar, then set the height to Collapsed
|
||||
@@ -757,12 +751,6 @@ public sealed partial class MainWindow : WindowEx,
|
||||
return;
|
||||
}
|
||||
|
||||
// We're doing something that requires us to lose focus, but we don't want to hide the window
|
||||
if (_preventHideWhenDeactivated)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// This will DWM cloak our window:
|
||||
HideWindow();
|
||||
|
||||
@@ -1039,44 +1027,4 @@ public sealed partial class MainWindow : WindowEx,
|
||||
_windowThemeSynchronizer.Dispose();
|
||||
DisposeAcrylic();
|
||||
}
|
||||
|
||||
public void Receive(DragStartedMessage message)
|
||||
{
|
||||
_preventHideWhenDeactivated = true;
|
||||
}
|
||||
|
||||
public void Receive(DragCompletedMessage message)
|
||||
{
|
||||
_preventHideWhenDeactivated = false;
|
||||
Task.Delay(200).ContinueWith(_ =>
|
||||
{
|
||||
DispatcherQueue.TryEnqueue(StealForeground);
|
||||
});
|
||||
}
|
||||
|
||||
private unsafe void StealForeground()
|
||||
{
|
||||
var foregroundWindow = PInvoke.GetForegroundWindow();
|
||||
if (foregroundWindow == _hwnd)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// This is bad, evil, and I'll have to forgo today's dinner dessert to punish myself
|
||||
// for writing this. But there's no way to make this work without it.
|
||||
// If the window is not reactivated, the UX breaks down: a deactivated window has to
|
||||
// be activated and then deactivated again to hide.
|
||||
var currentThreadId = PInvoke.GetCurrentThreadId();
|
||||
var foregroundThreadId = PInvoke.GetWindowThreadProcessId(foregroundWindow, null);
|
||||
if (foregroundThreadId != currentThreadId)
|
||||
{
|
||||
PInvoke.AttachThreadInput(currentThreadId, foregroundThreadId, true);
|
||||
PInvoke.SetForegroundWindow(_hwnd);
|
||||
PInvoke.AttachThreadInput(currentThreadId, foregroundThreadId, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
PInvoke.SetForegroundWindow(_hwnd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Messages;
|
||||
|
||||
public record DragCompletedMessage;
|
||||
@@ -1,7 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Messages;
|
||||
|
||||
public record DragStartedMessage;
|
||||
@@ -63,7 +63,4 @@ CreateWindowEx
|
||||
WNDCLASSEXW
|
||||
RegisterClassEx
|
||||
GetStockObject
|
||||
GetModuleHandle
|
||||
|
||||
GetWindowThreadProcessId
|
||||
AttachThreadInput
|
||||
GetModuleHandle
|
||||
@@ -12,8 +12,6 @@ using Microsoft.CmdPal.Ext.ClipboardHistory.Helpers;
|
||||
using Microsoft.CmdPal.Ext.ClipboardHistory.Helpers.Analyzers;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Windows.ApplicationModel.DataTransfer;
|
||||
using WinRT;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.ClipboardHistory.Models;
|
||||
|
||||
@@ -64,8 +62,6 @@ internal sealed partial class ClipboardListItem : ListItem
|
||||
RequestedShortcut = KeyChords.DeleteEntry,
|
||||
};
|
||||
|
||||
DataPackageView = _item.Item.Content;
|
||||
|
||||
if (item.IsImage)
|
||||
{
|
||||
Title = "Image";
|
||||
|
||||
@@ -2,17 +2,14 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.CmdPal.Core.Common.Commands;
|
||||
using Microsoft.CmdPal.Ext.Indexer.Helpers;
|
||||
using Microsoft.CmdPal.Ext.Indexer.Pages;
|
||||
using Microsoft.CmdPal.Ext.Indexer.Properties;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Windows.Foundation.Metadata;
|
||||
using FileAttributes = System.IO.FileAttributes;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Indexer.Data;
|
||||
|
||||
@@ -39,8 +36,6 @@ internal sealed partial class IndexerListItem : ListItem
|
||||
Title = indexerItem.FileName;
|
||||
Subtitle = indexerItem.FullPath;
|
||||
|
||||
DataPackage = DataPackageHelper.CreateDataPackageForPath(this, FilePath);
|
||||
|
||||
var commands = FileCommands(indexerItem.FullPath, browseByDefault);
|
||||
if (commands.Any())
|
||||
{
|
||||
|
||||
@@ -7,7 +7,6 @@ using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using Microsoft.CmdPal.Ext.Indexer.Data;
|
||||
using Microsoft.CmdPal.Ext.Indexer.Helpers;
|
||||
using Microsoft.CmdPal.Ext.Indexer.Properties;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Windows.Storage.Streams;
|
||||
@@ -43,7 +42,6 @@ internal sealed partial class FallbackOpenFileItem : FallbackCommandItem, System
|
||||
Subtitle = string.Empty;
|
||||
Icon = null;
|
||||
MoreCommands = null;
|
||||
DataPackage = null;
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -55,7 +53,6 @@ internal sealed partial class FallbackOpenFileItem : FallbackCommandItem, System
|
||||
Subtitle = string.Empty;
|
||||
Icon = null;
|
||||
MoreCommands = null;
|
||||
DataPackage = null;
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -70,7 +67,6 @@ internal sealed partial class FallbackOpenFileItem : FallbackCommandItem, System
|
||||
Subtitle = item.FileName;
|
||||
Title = item.FullPath;
|
||||
Icon = listItemForUs.Icon;
|
||||
DataPackage = DataPackageHelper.CreateDataPackageForPath(listItemForUs, item.FullPath);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -96,15 +92,13 @@ internal sealed partial class FallbackOpenFileItem : FallbackCommandItem, System
|
||||
_searchEngine.Query(query, _queryCookie);
|
||||
var results = _searchEngine.FetchItems(0, 20, _queryCookie, out var _);
|
||||
|
||||
if (results.Count == 0 || (results[0] is not IndexerListItem indexerListItem))
|
||||
if (results.Count == 0 || ((results[0] as IndexerListItem) is null))
|
||||
{
|
||||
// Exit 2: We searched for the file, and found nothing. Oh well.
|
||||
// Hide ourselves.
|
||||
Title = string.Empty;
|
||||
Subtitle = string.Empty;
|
||||
Command = new NoOpCommand();
|
||||
MoreCommands = null;
|
||||
DataPackage = null;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -112,12 +106,11 @@ internal sealed partial class FallbackOpenFileItem : FallbackCommandItem, System
|
||||
{
|
||||
// Exit 3: We searched for the file, and found exactly one thing. Awesome!
|
||||
// Return it.
|
||||
Title = indexerListItem.Title;
|
||||
Subtitle = indexerListItem.Subtitle;
|
||||
Icon = indexerListItem.Icon;
|
||||
Command = indexerListItem.Command;
|
||||
MoreCommands = indexerListItem.MoreCommands;
|
||||
DataPackage = DataPackageHelper.CreateDataPackageForPath(indexerListItem, indexerListItem.FilePath);
|
||||
Title = results[0].Title;
|
||||
Subtitle = results[0].Subtitle;
|
||||
Icon = results[0].Icon;
|
||||
Command = results[0].Command;
|
||||
MoreCommands = results[0].MoreCommands;
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -128,8 +121,6 @@ internal sealed partial class FallbackOpenFileItem : FallbackCommandItem, System
|
||||
Title = string.Format(CultureInfo.CurrentCulture, fallbackItemSearchPageTitleCompositeFormat, query);
|
||||
Icon = Icons.FileExplorerIcon;
|
||||
Command = indexerPage;
|
||||
MoreCommands = null;
|
||||
DataPackage = null;
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -140,7 +131,6 @@ internal sealed partial class FallbackOpenFileItem : FallbackCommandItem, System
|
||||
Icon = null;
|
||||
Command = new NoOpCommand();
|
||||
MoreCommands = null;
|
||||
DataPackage = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Windows.ApplicationModel.DataTransfer;
|
||||
using Windows.Storage;
|
||||
using File = System.IO.File;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Indexer.Helpers;
|
||||
|
||||
internal static class DataPackageHelper
|
||||
{
|
||||
public static DataPackage CreateDataPackageForPath(ICommandItem listItem, string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var dataPackage = new DataPackage();
|
||||
dataPackage.SetText(path);
|
||||
_ = dataPackage.TrySetStorageItemsAsync(path);
|
||||
dataPackage.Properties.Title = listItem.Title;
|
||||
dataPackage.Properties.Description = listItem.Subtitle;
|
||||
dataPackage.RequestedOperation = DataPackageOperation.Copy;
|
||||
return dataPackage;
|
||||
}
|
||||
|
||||
public static async Task<bool> TrySetStorageItemsAsync(this DataPackage dataPackage, string filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(filePath))
|
||||
{
|
||||
var file = await StorageFile.GetFileFromPathAsync(filePath);
|
||||
dataPackage.SetStorageItems([file]);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Directory.Exists(filePath))
|
||||
{
|
||||
var folder = await StorageFolder.GetFolderFromPathAsync(filePath);
|
||||
dataPackage.SetStorageItems([folder]);
|
||||
return true;
|
||||
}
|
||||
|
||||
// nothing there
|
||||
return false;
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
// Access denied – skip or report, but don't crash
|
||||
return false;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.CmdPal.Core.Common.Commands;
|
||||
using Microsoft.CmdPal.Ext.Indexer.Data;
|
||||
using Microsoft.CmdPal.Ext.Indexer.Helpers;
|
||||
using Microsoft.CmdPal.Ext.Indexer.Properties;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Windows.Foundation;
|
||||
@@ -29,9 +28,6 @@ internal sealed partial class ExploreListItem : ListItem
|
||||
|
||||
Title = indexerItem.FileName;
|
||||
Subtitle = indexerItem.FullPath;
|
||||
|
||||
DataPackage = DataPackageHelper.CreateDataPackageForPath(this, FilePath);
|
||||
|
||||
List<CommandContextItem> context = [];
|
||||
if (indexerItem.IsDirectory())
|
||||
{
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace SamplePagesExtension.Pages.SectionsPages;
|
||||
|
||||
internal sealed partial class SampleListPageWithSections : ListPage
|
||||
{
|
||||
public SampleListPageWithSections()
|
||||
{
|
||||
Icon = new IconInfo("\uE7C5");
|
||||
Name = "Sample Gallery List Page";
|
||||
}
|
||||
|
||||
public SampleListPageWithSections(IGridProperties gridProperties)
|
||||
{
|
||||
Icon = new IconInfo("\uE7C5");
|
||||
Name = "Sample Gallery List Page";
|
||||
GridProperties = gridProperties;
|
||||
}
|
||||
|
||||
public override IListItem[] GetItems()
|
||||
{
|
||||
var sectionList = new Section("This is a section list", [
|
||||
new ListItem(new NoOpCommand())
|
||||
{
|
||||
Title = "Sample Title",
|
||||
Subtitle = "I don't do anything",
|
||||
Icon = IconHelpers.FromRelativePath("Assets/Images/RedRectangle.png"),
|
||||
},
|
||||
]);
|
||||
var anotherSectionList = new Section("This is another section list", [
|
||||
new ListItem(new NoOpCommand())
|
||||
{
|
||||
Title = "Another Title",
|
||||
Subtitle = "I don't do anything",
|
||||
Icon = IconHelpers.FromRelativePath("Assets/Images/Space.png"),
|
||||
},
|
||||
new ListItem(new NoOpCommand())
|
||||
{
|
||||
Title = "More Titles",
|
||||
Subtitle = "I don't do anything",
|
||||
Icon = IconHelpers.FromRelativePath("Assets/Images/Swirls.png"),
|
||||
},
|
||||
new ListItem(new NoOpCommand())
|
||||
{
|
||||
Title = "Stop With The Titles",
|
||||
Subtitle = "I don't do anything",
|
||||
Icon = IconHelpers.FromRelativePath("Assets/Images/Win-Digital.png"),
|
||||
},
|
||||
]);
|
||||
|
||||
var yesTheresAnother = new Section("There's another", [
|
||||
new ListItem(new NoOpCommand())
|
||||
{
|
||||
Title = "Sample Title",
|
||||
Subtitle = "I don't do anything",
|
||||
Icon = IconHelpers.FromRelativePath("Assets/Images/RedRectangle.png"),
|
||||
},
|
||||
new ListItem(new NoOpCommand())
|
||||
{
|
||||
Title = "Another Title",
|
||||
Subtitle = "I don't do anything",
|
||||
Icon = IconHelpers.FromRelativePath("Assets/Images/Swirls.png"),
|
||||
},
|
||||
new ListItem(new NoOpCommand())
|
||||
{
|
||||
Title = "More Titles",
|
||||
Subtitle = "I don't do anything",
|
||||
Icon = IconHelpers.FromRelativePath("Assets/Images/Win-Digital.png"),
|
||||
},
|
||||
new ListItem(new NoOpCommand())
|
||||
{
|
||||
Title = "Stop With The Titles",
|
||||
Subtitle = "I don't do anything",
|
||||
Icon = IconHelpers.FromRelativePath("Assets/Images/RedRectangle.png"),
|
||||
},
|
||||
new ListItem(new NoOpCommand())
|
||||
{
|
||||
Title = "Another Title",
|
||||
Subtitle = "I don't do anything",
|
||||
Icon = IconHelpers.FromRelativePath("Assets/Images/Space.png"),
|
||||
},
|
||||
new ListItem(new NoOpCommand())
|
||||
{
|
||||
Title = "More Titles",
|
||||
Subtitle = "I don't do anything",
|
||||
Icon = IconHelpers.FromRelativePath("Assets/Images/Swirls.png"),
|
||||
},
|
||||
new ListItem(new NoOpCommand())
|
||||
{
|
||||
Title = "Stop With The Titles",
|
||||
Subtitle = "I don't do anything",
|
||||
Icon = IconHelpers.FromRelativePath("Assets/Images/Win-Digital.png"),
|
||||
},
|
||||
]);
|
||||
|
||||
return [
|
||||
..sectionList,
|
||||
..anotherSectionList,
|
||||
new Separator(),
|
||||
new ListItem(new NoOpCommand())
|
||||
{
|
||||
Title = "Separators also work",
|
||||
Subtitle = "But I still don't do anything",
|
||||
Icon = IconHelpers.FromRelativePath("Assets/Images/Win-Digital.png"),
|
||||
},
|
||||
..yesTheresAnother
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using SamplePagesExtension.Pages.SectionsPages;
|
||||
|
||||
namespace SamplePagesExtension.Pages;
|
||||
|
||||
internal sealed partial class SectionsIndexPage : ListPage
|
||||
{
|
||||
public SectionsIndexPage()
|
||||
{
|
||||
Name = "Sections Index Page";
|
||||
Icon = new IconInfo("\uF168");
|
||||
}
|
||||
|
||||
public override IListItem[] GetItems()
|
||||
{
|
||||
return [
|
||||
new ListItem(new SampleListPageWithSections())
|
||||
{
|
||||
Title = "A list page with sections",
|
||||
},
|
||||
new ListItem(new SampleListPageWithSections(new SmallGridLayout()))
|
||||
{
|
||||
Title = "A small grid page with sections",
|
||||
},
|
||||
new ListItem(new SampleListPageWithSections(new MediumGridLayout()))
|
||||
{
|
||||
Title = "A medium grid page with sections",
|
||||
},
|
||||
new ListItem(new SampleListPageWithSections(new GalleryGridLayout()))
|
||||
{
|
||||
Title = "A Gallery grid page with sections",
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,254 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Windows.ApplicationModel.DataTransfer;
|
||||
using Windows.Storage.Streams;
|
||||
|
||||
namespace SamplePagesExtension;
|
||||
|
||||
internal sealed partial class SampleDataTransferPage : ListPage
|
||||
{
|
||||
private readonly IListItem[] _items;
|
||||
|
||||
public SampleDataTransferPage()
|
||||
{
|
||||
var dataPackageWithText = CreateDataPackageWithText();
|
||||
var dataPackageWithDelayedText = CreateDataPackageWithDelayedText();
|
||||
var dataPackageWithImage = CreateDataPackageWithImage();
|
||||
|
||||
_items =
|
||||
[
|
||||
new ListItem(new NoOpCommand())
|
||||
{
|
||||
Title = "Draggable item with a plain text",
|
||||
Subtitle = "A sample page demonstrating how to drag and drop data",
|
||||
DataPackage = dataPackageWithText,
|
||||
},
|
||||
new ListItem(new NoOpCommand())
|
||||
{
|
||||
Title = "Draggable item with a lazily rendered plain text",
|
||||
Subtitle = "A sample page demonstrating how to drag and drop data with delayed rendering",
|
||||
DataPackage = dataPackageWithDelayedText,
|
||||
},
|
||||
new ListItem(new NoOpCommand())
|
||||
{
|
||||
Title = "Draggable item with an image",
|
||||
Subtitle = "This item has an image - package contains both file and a bitmap",
|
||||
Icon = IconHelpers.FromRelativePath("Assets/Images/Swirls.png"),
|
||||
DataPackage = dataPackageWithImage,
|
||||
},
|
||||
new ListItem(new SampleDataTransferOnGridPage())
|
||||
{
|
||||
Title = "Drag & drop grid",
|
||||
Subtitle = "A sample page demonstrating a grid list of items",
|
||||
Icon = new IconInfo("\uF0E2"),
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
private static DataPackage CreateDataPackageWithText()
|
||||
{
|
||||
var dataPackageWithText = new DataPackage
|
||||
{
|
||||
Properties =
|
||||
{
|
||||
Title = "Item with data package with text",
|
||||
Description = "This item has associated text with it",
|
||||
},
|
||||
RequestedOperation = DataPackageOperation.Copy,
|
||||
};
|
||||
dataPackageWithText.SetText("Text data in the Data Package");
|
||||
return dataPackageWithText;
|
||||
}
|
||||
|
||||
private static DataPackage CreateDataPackageWithDelayedText()
|
||||
{
|
||||
var dataPackageWithDelayedText = new DataPackage
|
||||
{
|
||||
Properties =
|
||||
{
|
||||
Title = "Item with delayed render data in the data package",
|
||||
Description = "This items has an item associated with it that is evaluated when requested for the first time",
|
||||
},
|
||||
RequestedOperation = DataPackageOperation.Copy,
|
||||
};
|
||||
dataPackageWithDelayedText.SetDataProvider(StandardDataFormats.Text, request =>
|
||||
{
|
||||
var d = request.GetDeferral();
|
||||
try
|
||||
{
|
||||
request.SetData(DateTime.Now.ToString("G", CultureInfo.CurrentCulture));
|
||||
}
|
||||
finally
|
||||
{
|
||||
d.Complete();
|
||||
}
|
||||
});
|
||||
return dataPackageWithDelayedText;
|
||||
}
|
||||
|
||||
private static DataPackage CreateDataPackageWithImage()
|
||||
{
|
||||
var dataPackageWithImage = new DataPackage
|
||||
{
|
||||
Properties =
|
||||
{
|
||||
Title = "Item with delayed render image in the data package",
|
||||
Description = "This items has an image associated with it that is evaluated when requested for the first time",
|
||||
},
|
||||
RequestedOperation = DataPackageOperation.Copy,
|
||||
};
|
||||
dataPackageWithImage.SetDataProvider(StandardDataFormats.Bitmap, async void (request) =>
|
||||
{
|
||||
var deferral = request.GetDeferral();
|
||||
try
|
||||
{
|
||||
var file = await Windows.Storage.StorageFile.GetFileFromApplicationUriAsync(new Uri("ms-appx:///Assets/Images/Swirls.png"));
|
||||
var stream = await file.OpenAsync(Windows.Storage.FileAccessMode.Read);
|
||||
var streamRef = RandomAccessStreamReference.CreateFromStream(stream);
|
||||
request.SetData(streamRef);
|
||||
}
|
||||
finally
|
||||
{
|
||||
deferral.Complete();
|
||||
}
|
||||
});
|
||||
dataPackageWithImage.SetDataProvider(StandardDataFormats.StorageItems, async void (request) =>
|
||||
{
|
||||
var deferral = request.GetDeferral();
|
||||
try
|
||||
{
|
||||
var file = await Windows.Storage.StorageFile.GetFileFromApplicationUriAsync(new Uri("ms-appx:///Assets/Images/Swirls.png"));
|
||||
var items = new[] { file };
|
||||
request.SetData(items);
|
||||
}
|
||||
finally
|
||||
{
|
||||
deferral.Complete();
|
||||
}
|
||||
});
|
||||
return dataPackageWithImage;
|
||||
}
|
||||
|
||||
public override IListItem[] GetItems() => _items;
|
||||
}
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Samples")]
|
||||
internal sealed partial class SampleDataTransferOnGridPage : ListPage
|
||||
{
|
||||
public SampleDataTransferOnGridPage()
|
||||
{
|
||||
GridProperties = new GalleryGridLayout
|
||||
{
|
||||
ShowTitle = true,
|
||||
ShowSubtitle = true,
|
||||
};
|
||||
}
|
||||
|
||||
public override IListItem[] GetItems()
|
||||
{
|
||||
return [
|
||||
new ListItem(new NoOpCommand())
|
||||
{
|
||||
Title = "Red Rectangle",
|
||||
Subtitle = "Drag me",
|
||||
Icon = IconHelpers.FromRelativePath("Assets/Images/RedRectangle.png"),
|
||||
DataPackage = CreateDataPackageForImage("Assets/Images/RedRectangle.png"),
|
||||
},
|
||||
new ListItem(new NoOpCommand())
|
||||
{
|
||||
Title = "Swirls",
|
||||
Subtitle = "Drop me",
|
||||
Icon = IconHelpers.FromRelativePath("Assets/Images/Swirls.png"),
|
||||
DataPackage = CreateDataPackageForImage("Assets/Images/Swirls.png"),
|
||||
},
|
||||
new ListItem(new NoOpCommand())
|
||||
{
|
||||
Title = "Windows Digital",
|
||||
Subtitle = "Drag me",
|
||||
Icon = IconHelpers.FromRelativePath("Assets/Images/Win-Digital.png"),
|
||||
DataPackage = CreateDataPackageForImage("Assets/Images/Win-Digital.png"),
|
||||
},
|
||||
new ListItem(new NoOpCommand())
|
||||
{
|
||||
Title = "Red Rectangle",
|
||||
Subtitle = "Drop me",
|
||||
Icon = IconHelpers.FromRelativePath("Assets/Images/RedRectangle.png"),
|
||||
DataPackage = CreateDataPackageForImage("Assets/Images/RedRectangle.png"),
|
||||
},
|
||||
new ListItem(new NoOpCommand())
|
||||
{
|
||||
Title = "Space",
|
||||
Subtitle = "Drag me",
|
||||
Icon = IconHelpers.FromRelativePath("Assets/Images/Space.png"),
|
||||
DataPackage = CreateDataPackageForImage("Assets/Images/Space.png"),
|
||||
},
|
||||
new ListItem(new NoOpCommand())
|
||||
{
|
||||
Title = "Swirls",
|
||||
Subtitle = "Drop me",
|
||||
Icon = IconHelpers.FromRelativePath("Assets/Images/Swirls.png"),
|
||||
DataPackage = CreateDataPackageForImage("Assets/Images/Swirls.png"),
|
||||
},
|
||||
new ListItem(new NoOpCommand())
|
||||
{
|
||||
Title = "Windows Digital",
|
||||
Subtitle = "Drag me",
|
||||
Icon = IconHelpers.FromRelativePath("Assets/Images/Win-Digital.png"),
|
||||
DataPackage = CreateDataPackageForImage("Assets/Images/Win-Digital.png"),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
private static DataPackage CreateDataPackageForImage(string relativePath)
|
||||
{
|
||||
var dataPackageWithImage = new DataPackage
|
||||
{
|
||||
Properties =
|
||||
{
|
||||
Title = "Image",
|
||||
Description = "This item has an image associated with it.",
|
||||
},
|
||||
RequestedOperation = DataPackageOperation.Copy,
|
||||
};
|
||||
|
||||
var imageUri = new Uri($"ms-appx:///{relativePath}");
|
||||
|
||||
dataPackageWithImage.SetDataProvider(StandardDataFormats.Bitmap, async (request) =>
|
||||
{
|
||||
var deferral = request.GetDeferral();
|
||||
try
|
||||
{
|
||||
var file = await Windows.Storage.StorageFile.GetFileFromApplicationUriAsync(imageUri);
|
||||
var stream = await file.OpenAsync(Windows.Storage.FileAccessMode.Read);
|
||||
var streamRef = RandomAccessStreamReference.CreateFromStream(stream);
|
||||
request.SetData(streamRef);
|
||||
}
|
||||
finally
|
||||
{
|
||||
deferral.Complete();
|
||||
}
|
||||
});
|
||||
|
||||
dataPackageWithImage.SetDataProvider(StandardDataFormats.StorageItems, async (request) =>
|
||||
{
|
||||
var deferral = request.GetDeferral();
|
||||
try
|
||||
{
|
||||
var file = await Windows.Storage.StorageFile.GetFileFromApplicationUriAsync(imageUri);
|
||||
var items = new[] { file };
|
||||
request.SetData(items);
|
||||
}
|
||||
finally
|
||||
{
|
||||
deferral.Complete();
|
||||
}
|
||||
});
|
||||
return dataPackageWithImage;
|
||||
}
|
||||
}
|
||||
@@ -24,11 +24,6 @@ public partial class SamplesListPage : ListPage
|
||||
Title = "List Page With Details",
|
||||
Subtitle = "A list of items, each with additional details to display",
|
||||
},
|
||||
new ListItem(new SectionsIndexPage())
|
||||
{
|
||||
Title = "List Pages With Sections",
|
||||
Subtitle = "A list of items, with sections header",
|
||||
},
|
||||
new ListItem(new SampleUpdatingItemsPage())
|
||||
{
|
||||
Title = "List page with items that change",
|
||||
@@ -106,13 +101,6 @@ public partial class SamplesListPage : ListPage
|
||||
Subtitle = "A demo of the settings helpers",
|
||||
},
|
||||
|
||||
// Data package samples
|
||||
new ListItem(new SampleDataTransferPage())
|
||||
{
|
||||
Title = "Clipboard and Drag-and-Drop Demo",
|
||||
Subtitle = "Demonstrates clipboard integration and drag-and-drop functionality",
|
||||
},
|
||||
|
||||
// Evil edge cases
|
||||
// Anything weird that might break the palette - put that in here.
|
||||
new ListItem(new EvilSamplesPage())
|
||||
|
||||
@@ -2,23 +2,14 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Windows.ApplicationModel.DataTransfer;
|
||||
using Windows.Foundation.Collections;
|
||||
using WinRT;
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class CommandItem : BaseObservable, ICommandItem, IExtendedAttributesProvider
|
||||
public partial class CommandItem : BaseObservable, ICommandItem
|
||||
{
|
||||
private readonly PropertySet _extendedAttributes = new();
|
||||
|
||||
private ICommand? _command;
|
||||
private WeakEventListener<CommandItem, object, IPropChangedEventArgs>? _commandListener;
|
||||
private string _title = string.Empty;
|
||||
|
||||
private DataPackage? _dataPackage;
|
||||
private DataPackageView? _dataPackageView;
|
||||
|
||||
public virtual IIconInfo? Icon
|
||||
{
|
||||
get => field;
|
||||
@@ -100,32 +91,6 @@ public partial class CommandItem : BaseObservable, ICommandItem, IExtendedAttrib
|
||||
|
||||
= [];
|
||||
|
||||
public DataPackage? DataPackage
|
||||
{
|
||||
get => _dataPackage;
|
||||
set
|
||||
{
|
||||
_dataPackage = value;
|
||||
_dataPackageView = null;
|
||||
_extendedAttributes[WellKnownExtensionAttributes.DataPackage] = value?.AsAgile().Get()?.GetView()!;
|
||||
OnPropertyChanged(nameof(DataPackage));
|
||||
OnPropertyChanged(nameof(DataPackageView));
|
||||
}
|
||||
}
|
||||
|
||||
public DataPackageView? DataPackageView
|
||||
{
|
||||
get => _dataPackageView;
|
||||
set
|
||||
{
|
||||
_dataPackage = null;
|
||||
_dataPackageView = value;
|
||||
_extendedAttributes[WellKnownExtensionAttributes.DataPackage] = value?.AsAgile().Get()!;
|
||||
OnPropertyChanged(nameof(DataPackage));
|
||||
OnPropertyChanged(nameof(DataPackageView));
|
||||
}
|
||||
}
|
||||
|
||||
public CommandItem()
|
||||
: this(new NoOpCommand())
|
||||
{
|
||||
@@ -167,9 +132,4 @@ public partial class CommandItem : BaseObservable, ICommandItem, IExtendedAttrib
|
||||
Title = title;
|
||||
Subtitle = subtitle;
|
||||
}
|
||||
|
||||
public IDictionary<string, object> GetProperties()
|
||||
{
|
||||
return _extendedAttributes;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,6 @@ public partial class FontIconData : IconData, IExtendedAttributesProvider
|
||||
|
||||
public IDictionary<string, object>? GetProperties() => new ValueSet()
|
||||
{
|
||||
{ WellKnownExtensionAttributes.FontFamily, FontFamily },
|
||||
{ "FontFamily", FontFamily },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
<Import Project="..\..\..\..\Common.Dotnet.AotCompatibility.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<RepoRoot>$(MSBuildThisFileDirectory)..\..\..\..\..\</RepoRoot>
|
||||
|
||||
<WindowsSdkPackageVersion>10.0.26100.57</WindowsSdkPackageVersion>
|
||||
<OutputPath>$(RepoRoot)$(Platform)\$(Configuration)\Microsoft.CommandPalette.Extensions.Toolkit</OutputPath>
|
||||
|
||||
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\Microsoft.CommandPalette.Extensions.Toolkit</OutputPath>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
@@ -20,7 +21,7 @@
|
||||
<PropertyGroup Condition="'$(CIBuild)'=='true'">
|
||||
<SignAssembly>true</SignAssembly>
|
||||
<DelaySign>true</DelaySign>
|
||||
<AssemblyOriginatorKeyFile>$(RepoRoot).pipelines\272MSSharedLibSN2048.snk</AssemblyOriginatorKeyFile>
|
||||
<AssemblyOriginatorKeyFile>$(MSBuildThisFileDirectory)..\..\..\..\..\.pipelines\272MSSharedLibSN2048.snk</AssemblyOriginatorKeyFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
@@ -46,19 +47,11 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Microsoft.CommandPalette.Extensions\Microsoft.CommandPalette.Extensions.vcxproj">
|
||||
<ReferenceOutputAssembly>False</ReferenceOutputAssembly>
|
||||
<BuildProject>True</BuildProject>
|
||||
</ProjectReference>
|
||||
<CsWinRTInputs Include="$(RepoRoot)$(Platform)\$(Configuration)\Microsoft.CommandPalette.Extensions\Microsoft.CommandPalette.Extensions.winmd" />
|
||||
<!-- Native implementation DLL -->
|
||||
<None Include="$(RepoRoot)$(Platform)\$(Configuration)\Microsoft.CommandPalette.Extensions\Microsoft.CommandPalette.Extensions.dll">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ProjectReference Include="..\Microsoft.CommandPalette.Extensions\Microsoft.CommandPalette.Extensions.vcxproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="$(RepoRoot)$(Platform)\$(Configuration)\Microsoft.CommandPalette.Extensions\Microsoft.CommandPalette.Extensions.winmd" Link="Microsoft.CommandPalette.Extensions.winmd" CopyToOutputDirectory="PreserveNewest" />
|
||||
<Content Include="$(SolutionDir)$(Platform)\$(Configuration)\Microsoft.CommandPalette.Extensions\Microsoft.CommandPalette.Extensions.winmd" Link="Microsoft.CommandPalette.Extensions.winmd" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections;
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public sealed partial class Section : IEnumerable<IListItem>
|
||||
{
|
||||
public IListItem[] Items { get; set; } = [];
|
||||
|
||||
public string SectionTitle { get; set; } = string.Empty;
|
||||
|
||||
private Separator CreateSectionListItem()
|
||||
{
|
||||
return new Separator(SectionTitle);
|
||||
}
|
||||
|
||||
public Section(string sectionName, IListItem[] items)
|
||||
{
|
||||
SectionTitle = sectionName;
|
||||
var listItems = items.ToList();
|
||||
|
||||
if (listItems.Count > 0)
|
||||
{
|
||||
listItems.Insert(0, CreateSectionListItem());
|
||||
Items = [.. listItems];
|
||||
}
|
||||
}
|
||||
|
||||
public Section()
|
||||
{
|
||||
}
|
||||
|
||||
public IEnumerator<IListItem> GetEnumerator() => Items.ToList().GetEnumerator();
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
}
|
||||
@@ -4,40 +4,6 @@
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class Separator : IListItem, ISeparatorContextItem, ISeparatorFilterItem
|
||||
public partial class Separator : ISeparatorContextItem, ISeparatorFilterItem
|
||||
{
|
||||
public Separator(string? title = "")
|
||||
: base()
|
||||
{
|
||||
Section = title ?? string.Empty;
|
||||
Command = null;
|
||||
}
|
||||
|
||||
public IDetails? Details => null;
|
||||
|
||||
public string? Section { get; private set; }
|
||||
|
||||
public ITag[]? Tags => null;
|
||||
|
||||
public string? TextToSuggest => null;
|
||||
|
||||
public ICommand? Command { get; private set; }
|
||||
|
||||
public IIconInfo? Icon => null;
|
||||
|
||||
public IContextItem[]? MoreCommands => null;
|
||||
|
||||
public string? Subtitle => null;
|
||||
|
||||
public string? Title
|
||||
{
|
||||
get => Section;
|
||||
set => Section = value;
|
||||
}
|
||||
|
||||
public event Windows.Foundation.TypedEventHandler<object, IPropChangedEventArgs>? PropChanged
|
||||
{
|
||||
add { }
|
||||
remove { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public static class WellKnownExtensionAttributes
|
||||
{
|
||||
public const string DataPackage = "Microsoft.CommandPalette.DataPackage";
|
||||
|
||||
public const string FontFamily = "FontFamily";
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup Label="NuGet">
|
||||
<!-- Tell NuGet this is PackageReference style -->
|
||||
<RestoreProjectStyle>PackageReference</RestoreProjectStyle>
|
||||
<!-- Tell NuGet we're a native project -->
|
||||
<NuGetTargetMoniker>native,Version=v0.0</NuGetTargetMoniker>
|
||||
<!-- Tell NuGet we target Windows (use your existing WindowsTargetPlatformVersion) -->
|
||||
<NuGetTargetPlatformIdentifier>Windows</NuGetTargetPlatformIdentifier>
|
||||
<NuGetTargetPlatformVersion>$(WindowsTargetPlatformVersion)</NuGetTargetPlatformVersion>
|
||||
<PropertyGroup>
|
||||
<PathToRoot>..\..\..\..\..\</PathToRoot>
|
||||
<WasdkNuget>$(PathToRoot)packages\Microsoft.WindowsAppSDK.1.8.250907003</WasdkNuget>
|
||||
<CppWinRTNuget>$(PathToRoot)packages\Microsoft.Windows.CppWinRT.2.0.240111.5</CppWinRTNuget>
|
||||
<WindowsSdkBuildToolsNuget>$(PathToRoot)packages\Microsoft.Windows.SDK.BuildTools.10.0.26100.6901</WindowsSdkBuildToolsNuget>
|
||||
<WebView2Nuget>$(PathToRoot)packages\Microsoft.Web.WebView2.1.0.2903.40</WebView2Nuget>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(WasdkNuget)\build\native\Microsoft.WindowsAppSDK.props" Condition="Exists('$(WasdkNuget)\build\native\Microsoft.WindowsAppSDK.props')" />
|
||||
<Import Project="$(CppWinRTNuget)\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('$(CppWinRTNuget)\build\native\Microsoft.Windows.CppWinRT.props')" />
|
||||
<Import Project="$(WindowsSdkBuildToolsNuget)\build\Microsoft.Windows.SDK.BuildTools.props" Condition="Exists('$(WindowsSdkBuildToolsNuget)\build\Microsoft.Windows.SDK.BuildTools.props')" />
|
||||
<PropertyGroup Label="Globals">
|
||||
<RepoRoot>$(MSBuildThisFileDirectory)..\..\..\..\..\</RepoRoot>
|
||||
<CppWinRTOptimized>true</CppWinRTOptimized>
|
||||
<CppWinRTRootNamespaceAutoMerge>true</CppWinRTRootNamespaceAutoMerge>
|
||||
<CppWinRTGenerateWindowsMetadata>true</CppWinRTGenerateWindowsMetadata>
|
||||
@@ -25,13 +25,7 @@
|
||||
<ApplicationTypeRevision>10.0</ApplicationTypeRevision>
|
||||
<WindowsTargetPlatformMinVersion>10.0.19041.0</WindowsTargetPlatformMinVersion>
|
||||
<WindowsTargetPlatformVersion>10.0.26100.0</WindowsTargetPlatformVersion>
|
||||
<WindowsAppSDKVerifyTransitiveDependencies>false</WindowsAppSDKVerifyTransitiveDependencies>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" GeneratePathProperty="true" />
|
||||
<PackageReference Include="Microsoft.Windows.CppWinRT" GeneratePathProperty="true" />
|
||||
<PackageReference Include="Microsoft.Windows.ImplementationLibrary" GeneratePathProperty="true" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Debug|ARM64">
|
||||
@@ -51,6 +45,10 @@
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\Microsoft.CommandPalette.Extensions\</OutDir>
|
||||
<IntDir>obj\$(Platform)\$(Configuration)\</IntDir>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Label="Configuration">
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
@@ -155,6 +153,7 @@
|
||||
<Midl Include="Microsoft.CommandPalette.Extensions.idl" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
<None Include="Microsoft.CommandPalette.Extensions.def" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
@@ -162,9 +161,23 @@
|
||||
<DeploymentContent>false</DeploymentContent>
|
||||
</Text>
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<OutDir>$(RepoRoot)$(Platform)\$(Configuration)\Microsoft.CommandPalette.Extensions\</OutDir>
|
||||
<IntDir>obj\$(Platform)\$(Configuration)\</IntDir>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<ImportGroup Label="ExtensionTargets">
|
||||
<Import Project="$(WindowsSdkBuildToolsNuget)\build\Microsoft.Windows.SDK.BuildTools.targets" Condition="Exists('$(WindowsSdkBuildToolsNuget)\build\Microsoft.Windows.SDK.BuildTools.targets')" />
|
||||
<Import Project="$(CppWinRTNuget)\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('$(CppWinRTNuget)\build\native\Microsoft.Windows.CppWinRT.targets')" />
|
||||
<Import Project="$(WasdkNuget)\build\native\Microsoft.WindowsAppSDK.targets" Condition="Exists('$(WasdkNuget)\build\native\Microsoft.WindowsAppSDK.targets')" />
|
||||
<Import Project="$(WebView2Nuget)\build\native\Microsoft.Web.WebView2.targets" Condition="Exists('$(WebView2Nuget)\build\native\Microsoft.Web.WebView2.targets')" />
|
||||
</ImportGroup>
|
||||
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
|
||||
<PropertyGroup>
|
||||
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
|
||||
</PropertyGroup>
|
||||
<Error Condition="!Exists('$(WindowsSdkBuildToolsNuget)\build\Microsoft.Windows.SDK.BuildTools.props')" Text="$([System.String]::Format('$(ErrorText)', '$(WindowsSdkBuildToolsNuget)\build\Microsoft.Windows.SDK.BuildTools.props'))" />
|
||||
<Error Condition="!Exists('$(WindowsSdkBuildToolsNuget)\build\Microsoft.Windows.SDK.BuildTools.targets')" Text="$([System.String]::Format('$(ErrorText)', '$(WindowsSdkBuildToolsNuget)\build\Microsoft.Windows.SDK.BuildTools.targets'))" />
|
||||
<Error Condition="!Exists('$(CppWinRTNuget)\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '$(CppWinRTNuget)\build\native\Microsoft.Windows.CppWinRT.props'))" />
|
||||
<Error Condition="!Exists('$(CppWinRTNuget)\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '$(CppWinRTNuget)\build\native\Microsoft.Windows.CppWinRT.targets'))" />
|
||||
<Error Condition="!Exists('$(WasdkNuget)\build\native\Microsoft.WindowsAppSDK.props')" Text="$([System.String]::Format('$(ErrorText)', '$(WasdkNuget)\build\native\Microsoft.WindowsAppSDK.props'))" />
|
||||
<Error Condition="!Exists('$(WasdkNuget)\build\native\Microsoft.WindowsAppSDK.targets')" Text="$([System.String]::Format('$(ErrorText)', '$(WasdkNuget)\build\native\Microsoft.WindowsAppSDK.targets'))" />
|
||||
<Error Condition="!Exists('$(WebView2Nuget)\build\native\Microsoft.Web.WebView2.targets')" Text="$([System.String]::Format('$(ErrorText)', '$(WebView2Nuget)\build\native\Microsoft.Web.WebView2.targets'))" />
|
||||
</Target>
|
||||
</Project>
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Microsoft.Web.WebView2" version="1.0.2903.40" targetFramework="native" />
|
||||
<package id="Microsoft.Windows.CppWinRT" version="2.0.240111.5" targetFramework="native" />
|
||||
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.231216.1" targetFramework="native" />
|
||||
<package id="Microsoft.WindowsAppSDK" version="1.8.250907003" targetFramework="native" />
|
||||
<package id="Microsoft.WindowsAppSDK.Base" version="1.8.250831001" targetFramework="native" />
|
||||
<package id="Microsoft.WindowsAppSDK.Foundation" version="1.8.250906002" targetFramework="native" />
|
||||
<package id="Microsoft.WindowsAppSDK.WinUI" version="1.8.250906003" targetFramework="native" />
|
||||
<package id="Microsoft.WindowsAppSDK.Runtime" version="1.8.250907003" targetFramework="native" />
|
||||
<package id="Microsoft.WindowsAppSDK.DWrite" version="1.8.25090401" targetFramework="native" />
|
||||
<package id="Microsoft.WindowsAppSDK.InteractiveExperiences" version="1.8.250906004" targetFramework="native" />
|
||||
<package id="Microsoft.WindowsAppSDK.Widgets" version="1.8.250904007" targetFramework="native" />
|
||||
<package id="Microsoft.WindowsAppSDK.AI" version="1.8.37" targetFramework="native" />
|
||||
<package id="Microsoft.Windows.SDK.BuildTools" version="10.0.26100.6901" targetFramework="native" />
|
||||
<package id="Microsoft.Windows.SDK.BuildTools.MSIX" version="1.7.20250829.1" targetFramework="native" />
|
||||
</packages>
|
||||
@@ -6,9 +6,9 @@
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Windows;
|
||||
|
||||
using ImageResizer.Models;
|
||||
using ImageResizer.Properties;
|
||||
using ImageResizer.Utilities;
|
||||
@@ -20,32 +20,8 @@ namespace ImageResizer
|
||||
{
|
||||
public partial class App : Application, IDisposable
|
||||
{
|
||||
private const string LogSubFolder = "\\ImageResizer\\Logs";
|
||||
|
||||
/// <summary>
|
||||
/// Gets cached AI availability state, checked at app startup.
|
||||
/// Can be updated after model download completes or background initialization.
|
||||
/// </summary>
|
||||
public static AiAvailabilityState AiAvailabilityState { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Event fired when AI initialization completes in background.
|
||||
/// Allows UI to refresh state when initialization finishes.
|
||||
/// </summary>
|
||||
public static event EventHandler<AiAvailabilityState> AiInitializationCompleted;
|
||||
|
||||
static App()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Initialize logger early (mirroring PowerOCR pattern)
|
||||
Logger.InitializeLogger(LogSubFolder);
|
||||
}
|
||||
catch
|
||||
{
|
||||
/* swallow logger init issues silently */
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
string appLanguage = LanguageHelper.LoadLanguage();
|
||||
@@ -54,9 +30,9 @@ namespace ImageResizer
|
||||
System.Threading.Thread.CurrentThread.CurrentUICulture = new CultureInfo(appLanguage);
|
||||
}
|
||||
}
|
||||
catch (CultureNotFoundException ex)
|
||||
catch (CultureNotFoundException)
|
||||
{
|
||||
Logger.LogError("CultureNotFoundException: " + ex.Message);
|
||||
// error
|
||||
}
|
||||
|
||||
Console.InputEncoding = Encoding.Unicode;
|
||||
@@ -67,59 +43,15 @@ namespace ImageResizer
|
||||
// Fix for .net 3.1.19 making Image Resizer not adapt to DPI changes.
|
||||
NativeMethods.SetProcessDPIAware();
|
||||
|
||||
// Check for AI detection mode (called by Runner in background)
|
||||
if (e?.Args?.Length > 0 && e.Args[0] == "--detect-ai")
|
||||
{
|
||||
RunAiDetectionMode();
|
||||
return;
|
||||
}
|
||||
|
||||
if (PowerToys.GPOWrapperProjection.GPOWrapper.GetConfiguredImageResizerEnabledValue() == PowerToys.GPOWrapperProjection.GpoRuleConfigured.Disabled)
|
||||
{
|
||||
/* TODO: Add logs to ImageResizer.
|
||||
* Logger.LogWarning("Tried to start with a GPO policy setting the utility to always be disabled. Please contact your systems administrator.");
|
||||
*/
|
||||
Logger.LogWarning("GPO policy disables ImageResizer. Exiting.");
|
||||
Environment.Exit(0); // Current.Exit won't work until there's a window opened.
|
||||
return;
|
||||
}
|
||||
|
||||
// AI Super Resolution is not supported on Windows 10 - skip cache check entirely
|
||||
if (OSVersionHelper.IsWindows10())
|
||||
{
|
||||
AiAvailabilityState = AiAvailabilityState.NotSupported;
|
||||
ResizeBatch.SetAiSuperResolutionService(Services.NoOpAiSuperResolutionService.Instance);
|
||||
Logger.LogInfo("AI Super Resolution not supported on Windows 10");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Load AI availability from cache (written by Runner's background detection)
|
||||
var cachedState = Services.AiAvailabilityCacheService.LoadCache();
|
||||
|
||||
if (cachedState.HasValue)
|
||||
{
|
||||
AiAvailabilityState = cachedState.Value;
|
||||
Logger.LogInfo($"AI state loaded from cache: {AiAvailabilityState}");
|
||||
}
|
||||
else
|
||||
{
|
||||
// No valid cache - default to NotSupported (Runner will detect and cache for next startup)
|
||||
AiAvailabilityState = AiAvailabilityState.NotSupported;
|
||||
Logger.LogInfo("No AI cache found, defaulting to NotSupported");
|
||||
}
|
||||
|
||||
// If AI is potentially available, start background initialization (non-blocking)
|
||||
if (AiAvailabilityState == AiAvailabilityState.Ready)
|
||||
{
|
||||
_ = InitializeAiServiceAsync(); // Fire and forget - don't block UI
|
||||
}
|
||||
else
|
||||
{
|
||||
// AI not available - set NoOp service immediately
|
||||
ResizeBatch.SetAiSuperResolutionService(Services.NoOpAiSuperResolutionService.Instance);
|
||||
}
|
||||
}
|
||||
|
||||
var batch = ResizeBatch.FromCommandLine(Console.In, e?.Args);
|
||||
|
||||
// TODO: Add command-line parameters that can be used in lieu of the input page (issue #14)
|
||||
@@ -130,121 +62,9 @@ namespace ImageResizer
|
||||
WindowHelpers.BringToForeground(new System.Windows.Interop.WindowInteropHelper(mainWindow).Handle);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AI detection mode: perform detection, write to cache, and exit.
|
||||
/// Called by Runner in background to avoid blocking ImageResizer UI startup.
|
||||
/// </summary>
|
||||
private void RunAiDetectionMode()
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.LogInfo("Running AI detection mode...");
|
||||
|
||||
// AI Super Resolution is not supported on Windows 10
|
||||
if (OSVersionHelper.IsWindows10())
|
||||
{
|
||||
Logger.LogInfo("AI detection skipped: Windows 10 does not support AI Super Resolution");
|
||||
Services.AiAvailabilityCacheService.SaveCache(AiAvailabilityState.NotSupported);
|
||||
Environment.Exit(0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Perform detection (reuse existing logic)
|
||||
var state = CheckAiAvailability();
|
||||
|
||||
// Write result to cache file
|
||||
Services.AiAvailabilityCacheService.SaveCache(state);
|
||||
|
||||
Logger.LogInfo($"AI detection complete: {state}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"AI detection failed: {ex.Message}");
|
||||
Services.AiAvailabilityCacheService.SaveCache(AiAvailabilityState.NotSupported);
|
||||
}
|
||||
|
||||
// Exit silently without showing UI
|
||||
Environment.Exit(0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check AI Super Resolution availability on this system.
|
||||
/// Performs architecture check and model availability check.
|
||||
/// </summary>
|
||||
private static AiAvailabilityState CheckAiAvailability()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Check Windows AI service model ready state
|
||||
// it's so slow, why?
|
||||
var readyState = Services.WinAiSuperResolutionService.GetModelReadyState();
|
||||
|
||||
// Map AI service state to our availability state
|
||||
switch (readyState)
|
||||
{
|
||||
case Microsoft.Windows.AI.AIFeatureReadyState.Ready:
|
||||
return AiAvailabilityState.Ready;
|
||||
|
||||
case Microsoft.Windows.AI.AIFeatureReadyState.NotReady:
|
||||
return AiAvailabilityState.ModelNotReady;
|
||||
|
||||
case Microsoft.Windows.AI.AIFeatureReadyState.DisabledByUser:
|
||||
case Microsoft.Windows.AI.AIFeatureReadyState.NotSupportedOnCurrentSystem:
|
||||
default:
|
||||
return AiAvailabilityState.NotSupported;
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return AiAvailabilityState.NotSupported;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize AI Super Resolution service asynchronously in background.
|
||||
/// Runs without blocking UI startup - state change event notifies completion.
|
||||
/// </summary>
|
||||
private static async System.Threading.Tasks.Task InitializeAiServiceAsync()
|
||||
{
|
||||
AiAvailabilityState finalState;
|
||||
|
||||
try
|
||||
{
|
||||
// Create and initialize AI service using async factory
|
||||
var aiService = await Services.WinAiSuperResolutionService.CreateAsync();
|
||||
|
||||
if (aiService != null)
|
||||
{
|
||||
ResizeBatch.SetAiSuperResolutionService(aiService);
|
||||
Logger.LogInfo("AI Super Resolution service initialized successfully.");
|
||||
finalState = AiAvailabilityState.Ready;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Initialization failed - use default NoOp service
|
||||
ResizeBatch.SetAiSuperResolutionService(Services.NoOpAiSuperResolutionService.Instance);
|
||||
Logger.LogWarning("AI Super Resolution service initialization failed. Using default service.");
|
||||
finalState = AiAvailabilityState.NotSupported;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Log error and use default NoOp service
|
||||
ResizeBatch.SetAiSuperResolutionService(Services.NoOpAiSuperResolutionService.Instance);
|
||||
Logger.LogError($"Exception during AI service initialization: {ex.Message}");
|
||||
finalState = AiAvailabilityState.NotSupported;
|
||||
}
|
||||
|
||||
// Update cached state and notify listeners
|
||||
AiAvailabilityState = finalState;
|
||||
AiInitializationCompleted?.Invoke(null, finalState);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Dispose AI Super Resolution service
|
||||
ResizeBatch.DisposeAiSuperResolutionService();
|
||||
|
||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
<GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
|
||||
<UseWPF>true</UseWPF>
|
||||
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
@@ -19,20 +18,19 @@
|
||||
<RootNamespace>ImageResizer</RootNamespace>
|
||||
<AssemblyName>PowerToys.ImageResizer</AssemblyName>
|
||||
<ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<ApplicationIcon>Resources\ImageResizer.ico</ApplicationIcon>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- <PropertyGroup>
|
||||
<ApplicationManifest>ImageResizerUI.dev.manifest</ApplicationManifest>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(CIBuild)'=='true'">
|
||||
<ApplicationManifest>ImageResizerUI.prod.manifest</ApplicationManifest>
|
||||
</PropertyGroup>
|
||||
</PropertyGroup> -->
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Update="Properties\Resources.resx">
|
||||
@@ -48,8 +46,6 @@
|
||||
<Resource Include="Resources\ImageResizer.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" />
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK.AI" />
|
||||
<PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" />
|
||||
<PackageReference Include="System.IO.Abstractions" />
|
||||
<PackageReference Include="WPF-UI" />
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
using ImageResizer.Properties;
|
||||
|
||||
namespace ImageResizer.Models
|
||||
{
|
||||
public class AiSize : ResizeSize
|
||||
{
|
||||
private static readonly CompositeFormat ScaleFormat = CompositeFormat.Parse(Resources.Input_AiScaleFormat);
|
||||
private int _scale = 2;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the formatted scale display string (e.g., "2×").
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public string ScaleDisplay => string.Format(CultureInfo.CurrentCulture, ScaleFormat, _scale);
|
||||
|
||||
[JsonPropertyName("scale")]
|
||||
public int Scale
|
||||
{
|
||||
get => _scale;
|
||||
set => Set(ref _scale, value);
|
||||
}
|
||||
|
||||
[JsonConstructor]
|
||||
public AiSize(int scale)
|
||||
{
|
||||
Scale = scale;
|
||||
}
|
||||
|
||||
public AiSize()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,30 +15,17 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using ImageResizer.Properties;
|
||||
using ImageResizer.Services;
|
||||
|
||||
namespace ImageResizer.Models
|
||||
{
|
||||
public class ResizeBatch
|
||||
{
|
||||
private readonly IFileSystem _fileSystem = new FileSystem();
|
||||
private static IAISuperResolutionService _aiSuperResolutionService;
|
||||
|
||||
public string DestinationDirectory { get; set; }
|
||||
|
||||
public ICollection<string> Files { get; } = new List<string>();
|
||||
|
||||
public static void SetAiSuperResolutionService(IAISuperResolutionService service)
|
||||
{
|
||||
_aiSuperResolutionService = service;
|
||||
}
|
||||
|
||||
public static void DisposeAiSuperResolutionService()
|
||||
{
|
||||
_aiSuperResolutionService?.Dispose();
|
||||
_aiSuperResolutionService = null;
|
||||
}
|
||||
|
||||
public static ResizeBatch FromCommandLine(TextReader standardInput, string[] args)
|
||||
{
|
||||
var batch = new ResizeBatch();
|
||||
@@ -135,9 +122,6 @@ namespace ImageResizer.Models
|
||||
}
|
||||
|
||||
protected virtual void Execute(string file, Settings settings)
|
||||
{
|
||||
var aiService = _aiSuperResolutionService ?? NoOpAiSuperResolutionService.Instance;
|
||||
new ResizeOperation(file, DestinationDirectory, settings, aiService).Execute();
|
||||
}
|
||||
=> new ResizeOperation(file, DestinationDirectory, settings).Execute();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,14 +10,12 @@ using System.Globalization;
|
||||
using System.IO;
|
||||
using System.IO.Abstractions;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
|
||||
using ImageResizer.Extensions;
|
||||
using ImageResizer.Properties;
|
||||
using ImageResizer.Services;
|
||||
using ImageResizer.Utilities;
|
||||
using Microsoft.VisualBasic.FileIO;
|
||||
|
||||
@@ -32,10 +30,6 @@ namespace ImageResizer.Models
|
||||
private readonly string _file;
|
||||
private readonly string _destinationDirectory;
|
||||
private readonly Settings _settings;
|
||||
private readonly IAISuperResolutionService _aiSuperResolutionService;
|
||||
|
||||
// Cache CompositeFormat for AI error message formatting (CA1863)
|
||||
private static readonly CompositeFormat _aiErrorFormat = CompositeFormat.Parse(Resources.Error_AiProcessingFailed);
|
||||
|
||||
// Filenames to avoid according to https://learn.microsoft.com/windows/win32/fileio/naming-a-file#file-and-directory-names
|
||||
private static readonly string[] _avoidFilenames =
|
||||
@@ -45,12 +39,11 @@ namespace ImageResizer.Models
|
||||
"LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
|
||||
};
|
||||
|
||||
public ResizeOperation(string file, string destinationDirectory, Settings settings, IAISuperResolutionService aiSuperResolutionService = null)
|
||||
public ResizeOperation(string file, string destinationDirectory, Settings settings)
|
||||
{
|
||||
_file = file;
|
||||
_destinationDirectory = destinationDirectory;
|
||||
_settings = settings;
|
||||
_aiSuperResolutionService = aiSuperResolutionService ?? NoOpAiSuperResolutionService.Instance;
|
||||
}
|
||||
|
||||
public void Execute()
|
||||
@@ -174,11 +167,6 @@ namespace ImageResizer.Models
|
||||
|
||||
private BitmapSource Transform(BitmapSource source)
|
||||
{
|
||||
if (_settings.SelectedSize is AiSize)
|
||||
{
|
||||
return TransformWithAi(source);
|
||||
}
|
||||
|
||||
int originalWidth = source.PixelWidth;
|
||||
int originalHeight = source.PixelHeight;
|
||||
|
||||
@@ -269,31 +257,6 @@ namespace ImageResizer.Models
|
||||
return scaledBitmap;
|
||||
}
|
||||
|
||||
private BitmapSource TransformWithAi(BitmapSource source)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = _aiSuperResolutionService.ApplySuperResolution(
|
||||
source,
|
||||
_settings.AiSize.Scale,
|
||||
_file);
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
throw new InvalidOperationException(Properties.Resources.Error_AiConversionFailed);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Wrap the exception with a localized message
|
||||
// This will be caught by ResizeBatch.Process() and displayed to the user
|
||||
var errorMessage = string.Format(CultureInfo.CurrentCulture, _aiErrorFormat, ex.Message);
|
||||
throw new InvalidOperationException(errorMessage, ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks original metadata by writing an image containing the given metadata into a memory stream.
|
||||
/// In case of errors, we try to rebuild the metadata object and check again.
|
||||
@@ -400,24 +363,19 @@ namespace ImageResizer.Models
|
||||
}
|
||||
|
||||
// Remove directory characters from the size's name.
|
||||
// For AI Size, use the scale display (e.g., "2×") instead of the full name
|
||||
string sizeName = _settings.SelectedSize is AiSize aiSize
|
||||
? aiSize.ScaleDisplay
|
||||
: _settings.SelectedSize.Name;
|
||||
string sizeNameSanitized = sizeName
|
||||
string sizeNameSanitized = _settings.SelectedSize.Name;
|
||||
sizeNameSanitized = sizeNameSanitized
|
||||
.Replace('\\', '_')
|
||||
.Replace('/', '_');
|
||||
|
||||
// Using CurrentCulture since this is user facing
|
||||
var selectedWidth = _settings.SelectedSize is AiSize ? encoder.Frames[0].PixelWidth : _settings.SelectedSize.Width;
|
||||
var selectedHeight = _settings.SelectedSize is AiSize ? encoder.Frames[0].PixelHeight : _settings.SelectedSize.Height;
|
||||
var fileName = string.Format(
|
||||
CultureInfo.CurrentCulture,
|
||||
_settings.FileNameFormat,
|
||||
originalFileName,
|
||||
sizeNameSanitized,
|
||||
selectedWidth,
|
||||
selectedHeight,
|
||||
_settings.SelectedSize.Width,
|
||||
_settings.SelectedSize.Height,
|
||||
encoder.Frames[0].PixelWidth,
|
||||
encoder.Frames[0].PixelHeight);
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace ImageResizer.Properties {
|
||||
// class via a tool like ResGen or Visual Studio.
|
||||
// To add or remove a member, edit your .ResX file then rerun ResGen
|
||||
// with the /str option, or rebuild your VS project.
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")]
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
public class Resources {
|
||||
@@ -78,33 +78,6 @@ namespace ImageResizer.Properties {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Failed to convert image format for AI processing..
|
||||
/// </summary>
|
||||
public static string Error_AiConversionFailed {
|
||||
get {
|
||||
return ResourceManager.GetString("Error_AiConversionFailed", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to AI super resolution processing failed: {0}.
|
||||
/// </summary>
|
||||
public static string Error_AiProcessingFailed {
|
||||
get {
|
||||
return ResourceManager.GetString("Error_AiProcessingFailed", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to AI scaling operation failed..
|
||||
/// </summary>
|
||||
public static string Error_AiScalingFailed {
|
||||
get {
|
||||
return ResourceManager.GetString("Error_AiScalingFailed", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Height.
|
||||
/// </summary>
|
||||
@@ -132,132 +105,6 @@ namespace ImageResizer.Properties {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Current:.
|
||||
/// </summary>
|
||||
public static string Input_AiCurrentLabel {
|
||||
get {
|
||||
return ResourceManager.GetString("Input_AiCurrentLabel", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Checking AI model availability....
|
||||
/// </summary>
|
||||
public static string Input_AiModelChecking {
|
||||
get {
|
||||
return ResourceManager.GetString("Input_AiModelChecking", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to AI feature is disabled by system settings..
|
||||
/// </summary>
|
||||
public static string Input_AiModelDisabledByUser {
|
||||
get {
|
||||
return ResourceManager.GetString("Input_AiModelDisabledByUser", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Download.
|
||||
/// </summary>
|
||||
public static string Input_AiModelDownloadButton {
|
||||
get {
|
||||
return ResourceManager.GetString("Input_AiModelDownloadButton", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Failed to download AI model. Please try again..
|
||||
/// </summary>
|
||||
public static string Input_AiModelDownloadFailed {
|
||||
get {
|
||||
return ResourceManager.GetString("Input_AiModelDownloadFailed", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Downloading AI model....
|
||||
/// </summary>
|
||||
public static string Input_AiModelDownloading {
|
||||
get {
|
||||
return ResourceManager.GetString("Input_AiModelDownloading", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to AI model not downloaded. Click Download to get started..
|
||||
/// </summary>
|
||||
public static string Input_AiModelNotAvailable {
|
||||
get {
|
||||
return ResourceManager.GetString("Input_AiModelNotAvailable", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to AI feature is not supported on this system..
|
||||
/// </summary>
|
||||
public static string Input_AiModelNotSupported {
|
||||
get {
|
||||
return ResourceManager.GetString("Input_AiModelNotSupported", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to New:.
|
||||
/// </summary>
|
||||
public static string Input_AiNewLabel {
|
||||
get {
|
||||
return ResourceManager.GetString("Input_AiNewLabel", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to {0}×.
|
||||
/// </summary>
|
||||
public static string Input_AiScaleFormat {
|
||||
get {
|
||||
return ResourceManager.GetString("Input_AiScaleFormat", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Scale.
|
||||
/// </summary>
|
||||
public static string Input_AiScaleLabel {
|
||||
get {
|
||||
return ResourceManager.GetString("Input_AiScaleLabel", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Super resolution.
|
||||
/// </summary>
|
||||
public static string Input_AiSuperResolution {
|
||||
get {
|
||||
return ResourceManager.GetString("Input_AiSuperResolution", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Upscale images using on-device AI.
|
||||
/// </summary>
|
||||
public static string Input_AiSuperResolutionDescription {
|
||||
get {
|
||||
return ResourceManager.GetString("Input_AiSuperResolutionDescription", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Unavailable.
|
||||
/// </summary>
|
||||
public static string Input_AiUnknownSize {
|
||||
get {
|
||||
return ResourceManager.GetString("Input_AiUnknownSize", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to (auto).
|
||||
/// </summary>
|
||||
|
||||
@@ -296,55 +296,4 @@
|
||||
<data name="Input_ShrinkOnly.Content" xml:space="preserve">
|
||||
<value>_Make pictures smaller but not larger</value>
|
||||
</data>
|
||||
<data name="Input_AiSuperResolution" xml:space="preserve">
|
||||
<value>Super resolution</value>
|
||||
</data>
|
||||
<data name="Input_AiUnknownSize" xml:space="preserve">
|
||||
<value>Unavailable</value>
|
||||
</data>
|
||||
<data name="Input_AiScaleFormat" xml:space="preserve">
|
||||
<value>{0}×</value>
|
||||
</data>
|
||||
<data name="Input_AiScaleLabel" xml:space="preserve">
|
||||
<value>Scale</value>
|
||||
</data>
|
||||
<data name="Input_AiCurrentLabel" xml:space="preserve">
|
||||
<value>Current:</value>
|
||||
</data>
|
||||
<data name="Input_AiNewLabel" xml:space="preserve">
|
||||
<value>New:</value>
|
||||
</data>
|
||||
<data name="Input_AiModelChecking" xml:space="preserve">
|
||||
<value>Checking AI model availability...</value>
|
||||
</data>
|
||||
<data name="Input_AiModelNotAvailable" xml:space="preserve">
|
||||
<value>AI model not downloaded. Click Download to get started.</value>
|
||||
</data>
|
||||
<data name="Input_AiModelDisabledByUser" xml:space="preserve">
|
||||
<value>AI feature is disabled by system settings.</value>
|
||||
</data>
|
||||
<data name="Input_AiModelNotSupported" xml:space="preserve">
|
||||
<value>AI feature is not supported on this system.</value>
|
||||
</data>
|
||||
<data name="Input_AiModelDownloading" xml:space="preserve">
|
||||
<value>Downloading AI model...</value>
|
||||
</data>
|
||||
<data name="Input_AiModelDownloadFailed" xml:space="preserve">
|
||||
<value>Failed to download AI model. Please try again.</value>
|
||||
</data>
|
||||
<data name="Input_AiModelDownloadButton" xml:space="preserve">
|
||||
<value>Download</value>
|
||||
</data>
|
||||
<data name="Error_AiProcessingFailed" xml:space="preserve">
|
||||
<value>AI super resolution processing failed: {0}</value>
|
||||
</data>
|
||||
<data name="Error_AiConversionFailed" xml:space="preserve">
|
||||
<value>Failed to convert image format for AI processing.</value>
|
||||
</data>
|
||||
<data name="Error_AiScalingFailed" xml:space="preserve">
|
||||
<value>AI scaling operation failed.</value>
|
||||
</data>
|
||||
<data name="Input_AiSuperResolutionDescription" xml:space="preserve">
|
||||
<value>Upscale images using on-device AI</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -19,22 +19,10 @@ using System.Threading;
|
||||
using System.Windows.Media.Imaging;
|
||||
|
||||
using ImageResizer.Models;
|
||||
using ImageResizer.Services;
|
||||
using ImageResizer.ViewModels;
|
||||
using ManagedCommon;
|
||||
|
||||
namespace ImageResizer.Properties
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the availability state of AI Super Resolution feature.
|
||||
/// </summary>
|
||||
public enum AiAvailabilityState
|
||||
{
|
||||
NotSupported, // System doesn't support AI (architecture issue or policy disabled)
|
||||
ModelNotReady, // AI supported but model not downloaded
|
||||
Ready, // AI fully ready to use
|
||||
}
|
||||
|
||||
public sealed partial class Settings : IDataErrorInfo, INotifyPropertyChanged
|
||||
{
|
||||
private static readonly IFileSystem _fileSystem = new FileSystem();
|
||||
@@ -62,7 +50,6 @@ namespace ImageResizer.Properties
|
||||
private bool _keepDateModified;
|
||||
private System.Guid _fallbackEncoder;
|
||||
private CustomSize _customSize;
|
||||
private AiSize _aiSize;
|
||||
|
||||
public Settings()
|
||||
{
|
||||
@@ -85,28 +72,9 @@ namespace ImageResizer.Properties
|
||||
KeepDateModified = false;
|
||||
FallbackEncoder = new System.Guid("19e4a5aa-5662-4fc5-a0c0-1758028e1057");
|
||||
CustomSize = new CustomSize(ResizeFit.Fit, 1024, 640, ResizeUnit.Pixel);
|
||||
AiSize = new AiSize(2); // Initialize with default scale of 2
|
||||
AllSizes = new AllSizesCollection(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates the SelectedSizeIndex to ensure it's within the valid range.
|
||||
/// This handles cross-device migration where settings saved on ARM64 with AI selected
|
||||
/// are loaded on non-ARM64 devices.
|
||||
/// </summary>
|
||||
private void ValidateSelectedSizeIndex()
|
||||
{
|
||||
// Index structure: 0 to Sizes.Count-1 (regular), Sizes.Count (CustomSize), Sizes.Count+1 (AiSize)
|
||||
var maxIndex = ImageResizer.App.AiAvailabilityState == AiAvailabilityState.NotSupported
|
||||
? Sizes.Count // CustomSize only
|
||||
: Sizes.Count + 1; // CustomSize + AiSize
|
||||
|
||||
if (_selectedSizeIndex > maxIndex)
|
||||
{
|
||||
_selectedSizeIndex = 0; // Reset to first size
|
||||
}
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public IEnumerable<ResizeSize> AllSizes { get; set; }
|
||||
|
||||
@@ -126,40 +94,15 @@ namespace ImageResizer.Properties
|
||||
[JsonIgnore]
|
||||
public ResizeSize SelectedSize
|
||||
{
|
||||
get
|
||||
{
|
||||
if (SelectedSizeIndex >= 0 && SelectedSizeIndex < Sizes.Count)
|
||||
{
|
||||
return Sizes[SelectedSizeIndex];
|
||||
}
|
||||
else if (SelectedSizeIndex == Sizes.Count)
|
||||
{
|
||||
return CustomSize;
|
||||
}
|
||||
else if (ImageResizer.App.AiAvailabilityState != AiAvailabilityState.NotSupported && SelectedSizeIndex == Sizes.Count + 1)
|
||||
{
|
||||
return AiSize;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback to CustomSize when index is out of range or AI is not available
|
||||
return CustomSize;
|
||||
}
|
||||
}
|
||||
|
||||
get => SelectedSizeIndex >= 0 && SelectedSizeIndex < Sizes.Count
|
||||
? Sizes[SelectedSizeIndex]
|
||||
: CustomSize;
|
||||
set
|
||||
{
|
||||
var index = Sizes.IndexOf(value);
|
||||
if (index == -1)
|
||||
{
|
||||
if (value is AiSize)
|
||||
{
|
||||
index = Sizes.Count + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
index = Sizes.Count;
|
||||
}
|
||||
index = Sizes.Count;
|
||||
}
|
||||
|
||||
SelectedSizeIndex = index;
|
||||
@@ -195,17 +138,13 @@ namespace ImageResizer.Properties
|
||||
|
||||
private class AllSizesCollection : IEnumerable<ResizeSize>, INotifyCollectionChanged, INotifyPropertyChanged
|
||||
{
|
||||
private readonly Settings _settings;
|
||||
private ObservableCollection<ResizeSize> _sizes;
|
||||
private CustomSize _customSize;
|
||||
private AiSize _aiSize;
|
||||
|
||||
public AllSizesCollection(Settings settings)
|
||||
{
|
||||
_settings = settings;
|
||||
_sizes = settings.Sizes;
|
||||
_customSize = settings.CustomSize;
|
||||
_aiSize = settings.AiSize;
|
||||
|
||||
_sizes.CollectionChanged += HandleCollectionChanged;
|
||||
((INotifyPropertyChanged)_sizes).PropertyChanged += HandlePropertyChanged;
|
||||
@@ -224,18 +163,6 @@ namespace ImageResizer.Properties
|
||||
oldCustomSize,
|
||||
_sizes.Count));
|
||||
}
|
||||
else if (e.PropertyName == nameof(Models.AiSize))
|
||||
{
|
||||
var oldAiSize = _aiSize;
|
||||
_aiSize = settings.AiSize;
|
||||
|
||||
OnCollectionChanged(
|
||||
new NotifyCollectionChangedEventArgs(
|
||||
NotifyCollectionChangedAction.Replace,
|
||||
_aiSize,
|
||||
oldAiSize,
|
||||
_sizes.Count + 1));
|
||||
}
|
||||
else if (e.PropertyName == nameof(Sizes))
|
||||
{
|
||||
var oldSizes = _sizes;
|
||||
@@ -258,30 +185,12 @@ namespace ImageResizer.Properties
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
public int Count
|
||||
=> _sizes.Count + 1 + (ImageResizer.App.AiAvailabilityState != AiAvailabilityState.NotSupported ? 1 : 0);
|
||||
=> _sizes.Count + 1;
|
||||
|
||||
public ResizeSize this[int index]
|
||||
{
|
||||
get
|
||||
{
|
||||
if (index < _sizes.Count)
|
||||
{
|
||||
return _sizes[index];
|
||||
}
|
||||
else if (index == _sizes.Count)
|
||||
{
|
||||
return _customSize;
|
||||
}
|
||||
else if (ImageResizer.App.AiAvailabilityState != AiAvailabilityState.NotSupported && index == _sizes.Count + 1)
|
||||
{
|
||||
return _aiSize;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(index), index, $"Index {index} is out of range for AllSizesCollection.");
|
||||
}
|
||||
}
|
||||
}
|
||||
=> index == _sizes.Count
|
||||
? _customSize
|
||||
: _sizes[index];
|
||||
|
||||
public IEnumerator<ResizeSize> GetEnumerator()
|
||||
=> new AllSizesEnumerator(this);
|
||||
@@ -501,18 +410,6 @@ namespace ImageResizer.Properties
|
||||
}
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(WrappedJsonValueConverter))]
|
||||
[JsonPropertyName("imageresizer_aiSize")]
|
||||
public AiSize AiSize
|
||||
{
|
||||
get => _aiSize;
|
||||
set
|
||||
{
|
||||
_aiSize = value;
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public static string SettingsPath { get => _settingsPath; set => _settingsPath = value; }
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
@@ -590,7 +487,6 @@ namespace ImageResizer.Properties
|
||||
KeepDateModified = jsonSettings.KeepDateModified;
|
||||
FallbackEncoder = jsonSettings.FallbackEncoder;
|
||||
CustomSize = jsonSettings.CustomSize;
|
||||
AiSize = jsonSettings.AiSize ?? new AiSize(InputViewModel.DefaultAiScale);
|
||||
SelectedSizeIndex = jsonSettings.SelectedSizeIndex;
|
||||
|
||||
if (jsonSettings.Sizes.Count > 0)
|
||||
@@ -601,10 +497,6 @@ namespace ImageResizer.Properties
|
||||
// Ensure Ids are unique and handle missing Ids
|
||||
IdRecoveryHelper.RecoverInvalidIds(Sizes);
|
||||
}
|
||||
|
||||
// Validate SelectedSizeIndex after Sizes collection has been updated
|
||||
// This handles cross-device migration (e.g., ARM64 -> non-ARM64)
|
||||
ValidateSelectedSizeIndex();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.Json;
|
||||
using ImageResizer.Properties;
|
||||
using ManagedCommon;
|
||||
|
||||
namespace ImageResizer.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Service for caching AI availability detection results.
|
||||
/// Persists results to avoid slow API calls on every startup.
|
||||
/// Runner calls ImageResizer --detect-ai to perform detection,
|
||||
/// and ImageResizer reads the cached result on normal startup.
|
||||
/// </summary>
|
||||
public static class AiAvailabilityCacheService
|
||||
{
|
||||
private const string CacheFileName = "ai_capabilities.json";
|
||||
private const int CacheVersion = 1;
|
||||
|
||||
private static readonly JsonSerializerOptions SerializerOptions = new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true,
|
||||
};
|
||||
|
||||
private static string CachePath => Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"Microsoft",
|
||||
"PowerToys",
|
||||
CacheFileName);
|
||||
|
||||
/// <summary>
|
||||
/// Load AI availability state from cache.
|
||||
/// Returns null if cache doesn't exist, is invalid, or read fails.
|
||||
/// </summary>
|
||||
public static AiAvailabilityState? LoadCache()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(CachePath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var json = File.ReadAllText(CachePath);
|
||||
var cache = JsonSerializer.Deserialize<AiCapabilityCache>(json);
|
||||
|
||||
if (!IsCacheValid(cache))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return (AiAvailabilityState)cache.State;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Read failure (file locked, corrupted JSON, etc.) - return null and use fallback
|
||||
Logger.LogError($"Failed to load AI cache: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save AI availability state to cache.
|
||||
/// Called by --detect-ai mode after performing detection.
|
||||
/// </summary>
|
||||
public static void SaveCache(AiAvailabilityState state)
|
||||
{
|
||||
try
|
||||
{
|
||||
var cache = new AiCapabilityCache
|
||||
{
|
||||
Version = CacheVersion,
|
||||
State = (int)state,
|
||||
WindowsBuild = Environment.OSVersion.Version.ToString(),
|
||||
Architecture = RuntimeInformation.ProcessArchitecture.ToString(),
|
||||
Timestamp = DateTime.UtcNow.ToString("o"),
|
||||
};
|
||||
|
||||
var dir = Path.GetDirectoryName(CachePath);
|
||||
if (!Directory.Exists(dir))
|
||||
{
|
||||
Directory.CreateDirectory(dir);
|
||||
}
|
||||
|
||||
var json = JsonSerializer.Serialize(cache, SerializerOptions);
|
||||
File.WriteAllText(CachePath, json);
|
||||
|
||||
Logger.LogInfo($"AI cache saved: {state}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to save AI cache: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validate cache against current system environment.
|
||||
/// Cache is invalid if version, architecture, or Windows build changed.
|
||||
/// </summary>
|
||||
private static bool IsCacheValid(AiCapabilityCache cache)
|
||||
{
|
||||
if (cache == null || cache.Version != CacheVersion)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (cache.Architecture != RuntimeInformation.ProcessArchitecture.ToString())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (cache.WindowsBuild != Environment.OSVersion.Version.ToString())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace ImageResizer.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Data model for AI capability cache file.
|
||||
/// </summary>
|
||||
internal sealed class AiCapabilityCache
|
||||
{
|
||||
public int Version { get; set; }
|
||||
|
||||
public int State { get; set; }
|
||||
|
||||
public string WindowsBuild { get; set; }
|
||||
|
||||
public string Architecture { get; set; }
|
||||
|
||||
public string Timestamp { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Windows.Media.Imaging;
|
||||
|
||||
namespace ImageResizer.Services
|
||||
{
|
||||
public interface IAISuperResolutionService : IDisposable
|
||||
{
|
||||
BitmapSource ApplySuperResolution(BitmapSource source, int scale, string filePath);
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Windows.Media.Imaging;
|
||||
|
||||
namespace ImageResizer.Services
|
||||
{
|
||||
public sealed class NoOpAiSuperResolutionService : IAISuperResolutionService
|
||||
{
|
||||
public static NoOpAiSuperResolutionService Instance { get; } = new NoOpAiSuperResolutionService();
|
||||
|
||||
private NoOpAiSuperResolutionService()
|
||||
{
|
||||
}
|
||||
|
||||
public BitmapSource ApplySuperResolution(BitmapSource source, int scale, string filePath)
|
||||
{
|
||||
return source;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// No resources to dispose in no-op implementation
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,261 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.InteropServices.WindowsRuntime;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using Microsoft.Windows.AI;
|
||||
using Microsoft.Windows.AI.Imaging;
|
||||
using Windows.Graphics.Imaging;
|
||||
|
||||
namespace ImageResizer.Services
|
||||
{
|
||||
public sealed class WinAiSuperResolutionService : IAISuperResolutionService
|
||||
{
|
||||
private readonly ImageScaler _imageScaler;
|
||||
private readonly object _usageLock = new object();
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="WinAiSuperResolutionService"/> class.
|
||||
/// Private constructor. Use CreateAsync() factory method to create instances.
|
||||
/// </summary>
|
||||
private WinAiSuperResolutionService(ImageScaler imageScaler)
|
||||
{
|
||||
_imageScaler = imageScaler ?? throw new ArgumentNullException(nameof(imageScaler));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Async factory method to create and initialize WinAiSuperResolutionService.
|
||||
/// Returns null if initialization fails.
|
||||
/// </summary>
|
||||
public static async Task<WinAiSuperResolutionService> CreateAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var imageScaler = await ImageScaler.CreateAsync();
|
||||
if (imageScaler == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new WinAiSuperResolutionService(imageScaler);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static AIFeatureReadyState GetModelReadyState()
|
||||
{
|
||||
try
|
||||
{
|
||||
return ImageScaler.GetReadyState();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// If we can't get the state, treat it as disabled by user
|
||||
// The caller should check if it's Ready or NotReady
|
||||
return AIFeatureReadyState.DisabledByUser;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<AIFeatureReadyResult> EnsureModelReadyAsync(IProgress<double> progress = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var operation = ImageScaler.EnsureReadyAsync();
|
||||
|
||||
// Register progress handler if provided
|
||||
if (progress != null)
|
||||
{
|
||||
operation.Progress = (asyncInfo, progressValue) =>
|
||||
{
|
||||
// progressValue is a double representing completion percentage (0.0 to 1.0 or 0 to 100)
|
||||
progress.Report(progressValue);
|
||||
};
|
||||
}
|
||||
|
||||
return await operation;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public BitmapSource ApplySuperResolution(BitmapSource source, int scale, string filePath)
|
||||
{
|
||||
if (source == null || _disposed)
|
||||
{
|
||||
return source;
|
||||
}
|
||||
|
||||
// Note: filePath parameter reserved for future use (e.g., logging, caching)
|
||||
// Currently not used by the ImageScaler API
|
||||
try
|
||||
{
|
||||
// Convert WPF BitmapSource to WinRT SoftwareBitmap
|
||||
var softwareBitmap = ConvertBitmapSourceToSoftwareBitmap(source);
|
||||
if (softwareBitmap == null)
|
||||
{
|
||||
return source;
|
||||
}
|
||||
|
||||
// Calculate target dimensions
|
||||
var newWidth = softwareBitmap.PixelWidth * scale;
|
||||
var newHeight = softwareBitmap.PixelHeight * scale;
|
||||
|
||||
// Apply super resolution with thread-safe access
|
||||
// _usageLock protects concurrent access from Parallel.ForEach threads
|
||||
SoftwareBitmap scaledBitmap;
|
||||
lock (_usageLock)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return source;
|
||||
}
|
||||
|
||||
scaledBitmap = _imageScaler.ScaleSoftwareBitmap(softwareBitmap, newWidth, newHeight);
|
||||
}
|
||||
|
||||
if (scaledBitmap == null)
|
||||
{
|
||||
return source;
|
||||
}
|
||||
|
||||
// Convert back to WPF BitmapSource
|
||||
return ConvertSoftwareBitmapToBitmapSource(scaledBitmap);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Any error, return original image gracefully
|
||||
return source;
|
||||
}
|
||||
}
|
||||
|
||||
private static SoftwareBitmap ConvertBitmapSourceToSoftwareBitmap(BitmapSource bitmapSource)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Ensure the bitmap is in a compatible format
|
||||
var convertedBitmap = new FormatConvertedBitmap();
|
||||
convertedBitmap.BeginInit();
|
||||
convertedBitmap.Source = bitmapSource;
|
||||
convertedBitmap.DestinationFormat = PixelFormats.Bgra32;
|
||||
convertedBitmap.EndInit();
|
||||
|
||||
int width = convertedBitmap.PixelWidth;
|
||||
int height = convertedBitmap.PixelHeight;
|
||||
int stride = width * 4; // 4 bytes per pixel for Bgra32
|
||||
byte[] pixels = new byte[height * stride];
|
||||
|
||||
convertedBitmap.CopyPixels(pixels, stride, 0);
|
||||
|
||||
// Create SoftwareBitmap from pixel data
|
||||
var softwareBitmap = new SoftwareBitmap(
|
||||
BitmapPixelFormat.Bgra8,
|
||||
width,
|
||||
height,
|
||||
BitmapAlphaMode.Premultiplied);
|
||||
|
||||
using (var buffer = softwareBitmap.LockBuffer(BitmapBufferAccessMode.Write))
|
||||
using (var reference = buffer.CreateReference())
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
((IMemoryBufferByteAccess)reference).GetBuffer(out byte* dataInBytes, out uint capacity);
|
||||
System.Runtime.InteropServices.Marshal.Copy(pixels, 0, (IntPtr)dataInBytes, pixels.Length);
|
||||
}
|
||||
}
|
||||
|
||||
return softwareBitmap;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static BitmapSource ConvertSoftwareBitmapToBitmapSource(SoftwareBitmap softwareBitmap)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Convert to Bgra8 format if needed
|
||||
var convertedBitmap = SoftwareBitmap.Convert(
|
||||
softwareBitmap,
|
||||
BitmapPixelFormat.Bgra8,
|
||||
BitmapAlphaMode.Premultiplied);
|
||||
|
||||
int width = convertedBitmap.PixelWidth;
|
||||
int height = convertedBitmap.PixelHeight;
|
||||
int stride = width * 4; // 4 bytes per pixel for Bgra8
|
||||
byte[] pixels = new byte[height * stride];
|
||||
|
||||
using (var buffer = convertedBitmap.LockBuffer(BitmapBufferAccessMode.Read))
|
||||
using (var reference = buffer.CreateReference())
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
((IMemoryBufferByteAccess)reference).GetBuffer(out byte* dataInBytes, out uint capacity);
|
||||
System.Runtime.InteropServices.Marshal.Copy((IntPtr)dataInBytes, pixels, 0, pixels.Length);
|
||||
}
|
||||
}
|
||||
|
||||
// Create WPF BitmapSource from pixel data
|
||||
var wpfBitmap = BitmapSource.Create(
|
||||
width,
|
||||
height,
|
||||
96, // DPI X
|
||||
96, // DPI Y
|
||||
PixelFormats.Bgra32,
|
||||
null,
|
||||
pixels,
|
||||
stride);
|
||||
|
||||
wpfBitmap.Freeze(); // Make it thread-safe
|
||||
return wpfBitmap;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
[ComImport]
|
||||
[Guid("5B0D3235-4DBA-4D44-865E-8F1D0E4FD04D")]
|
||||
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
private interface IMemoryBufferByteAccess
|
||||
{
|
||||
unsafe void GetBuffer(out byte* buffer, out uint capacity);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_usageLock)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// ImageScaler implements IDisposable
|
||||
(_imageScaler as IDisposable)?.Dispose();
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,41 +6,22 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media.Imaging;
|
||||
|
||||
using Common.UI;
|
||||
using ImageResizer.Helpers;
|
||||
using ImageResizer.Models;
|
||||
using ImageResizer.Properties;
|
||||
using ImageResizer.Services;
|
||||
using ImageResizer.Views;
|
||||
|
||||
namespace ImageResizer.ViewModels
|
||||
{
|
||||
public class InputViewModel : Observable
|
||||
{
|
||||
public const int DefaultAiScale = 2;
|
||||
private const int MinAiScale = 1;
|
||||
private const int MaxAiScale = 8;
|
||||
|
||||
private readonly ResizeBatch _batch;
|
||||
private readonly MainViewModel _mainViewModel;
|
||||
private readonly IMainView _mainView;
|
||||
private readonly bool _hasMultipleFiles;
|
||||
private bool _originalDimensionsLoaded;
|
||||
private int? _originalWidth;
|
||||
private int? _originalHeight;
|
||||
private string _currentResolutionDescription;
|
||||
private string _newResolutionDescription;
|
||||
private bool _isDownloadingModel;
|
||||
private string _modelStatusMessage;
|
||||
private double _modelDownloadProgress;
|
||||
|
||||
public enum Dimension
|
||||
{
|
||||
@@ -64,114 +45,24 @@ namespace ImageResizer.ViewModels
|
||||
_batch = batch;
|
||||
_mainViewModel = mainViewModel;
|
||||
_mainView = mainView;
|
||||
_hasMultipleFiles = _batch?.Files.Count > 1;
|
||||
|
||||
Settings = settings;
|
||||
if (settings != null)
|
||||
{
|
||||
settings.CustomSize.PropertyChanged += (sender, e) => settings.SelectedSize = (CustomSize)sender;
|
||||
settings.AiSize.PropertyChanged += (sender, e) =>
|
||||
{
|
||||
if (e.PropertyName == nameof(AiSize.Scale))
|
||||
{
|
||||
NotifyAiScaleChanged();
|
||||
}
|
||||
};
|
||||
settings.PropertyChanged += HandleSettingsPropertyChanged;
|
||||
}
|
||||
|
||||
ResizeCommand = new RelayCommand(Resize, () => CanResize);
|
||||
ResizeCommand = new RelayCommand(Resize);
|
||||
CancelCommand = new RelayCommand(Cancel);
|
||||
OpenSettingsCommand = new RelayCommand(OpenSettings);
|
||||
EnterKeyPressedCommand = new RelayCommand<KeyPressParams>(HandleEnterKeyPress);
|
||||
DownloadModelCommand = new RelayCommand(async () => await DownloadModelAsync());
|
||||
|
||||
// Initialize AI UI state based on Settings availability
|
||||
InitializeAiState();
|
||||
}
|
||||
|
||||
public Settings Settings { get; }
|
||||
|
||||
public IEnumerable<ResizeFit> ResizeFitValues => Enum.GetValues<ResizeFit>();
|
||||
public IEnumerable<ResizeFit> ResizeFitValues => Enum.GetValues(typeof(ResizeFit)).Cast<ResizeFit>();
|
||||
|
||||
public IEnumerable<ResizeUnit> ResizeUnitValues => Enum.GetValues<ResizeUnit>();
|
||||
|
||||
public int AiSuperResolutionScale
|
||||
{
|
||||
get => Settings?.AiSize?.Scale ?? DefaultAiScale;
|
||||
set
|
||||
{
|
||||
if (Settings?.AiSize != null && Settings.AiSize.Scale != value)
|
||||
{
|
||||
Settings.AiSize.Scale = value;
|
||||
NotifyAiScaleChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string AiScaleDisplay => Settings?.AiSize?.ScaleDisplay ?? string.Empty;
|
||||
|
||||
public string CurrentResolutionDescription
|
||||
{
|
||||
get => _currentResolutionDescription;
|
||||
private set => Set(ref _currentResolutionDescription, value);
|
||||
}
|
||||
|
||||
public string NewResolutionDescription
|
||||
{
|
||||
get => _newResolutionDescription;
|
||||
private set => Set(ref _newResolutionDescription, value);
|
||||
}
|
||||
|
||||
// ==================== UI State Properties ====================
|
||||
|
||||
// Show AI size descriptions only when AI size is selected and not multiple files
|
||||
public bool ShowAiSizeDescriptions => Settings?.SelectedSize is AiSize && !_hasMultipleFiles;
|
||||
|
||||
// Helper property: Is model currently being downloaded?
|
||||
public bool IsModelDownloading => _isDownloadingModel;
|
||||
|
||||
public string ModelStatusMessage
|
||||
{
|
||||
get => _modelStatusMessage;
|
||||
private set => Set(ref _modelStatusMessage, value);
|
||||
}
|
||||
|
||||
public double ModelDownloadProgress
|
||||
{
|
||||
get => _modelDownloadProgress;
|
||||
private set => Set(ref _modelDownloadProgress, value);
|
||||
}
|
||||
|
||||
// Show download prompt when: AI size is selected and model is not ready (including downloading)
|
||||
public bool ShowModelDownloadPrompt =>
|
||||
Settings?.SelectedSize is AiSize &&
|
||||
(App.AiAvailabilityState == Properties.AiAvailabilityState.ModelNotReady || _isDownloadingModel);
|
||||
|
||||
// Show AI controls when: AI size is selected and AI is ready
|
||||
public bool ShowAiControls =>
|
||||
Settings?.SelectedSize is AiSize &&
|
||||
App.AiAvailabilityState == Properties.AiAvailabilityState.Ready;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the resize operation can proceed.
|
||||
/// For AI resize: only enabled when AI is fully ready.
|
||||
/// For non-AI resize: always enabled.
|
||||
/// </summary>
|
||||
public bool CanResize
|
||||
{
|
||||
get
|
||||
{
|
||||
// If AI size is selected, only allow resize when AI is fully ready
|
||||
if (Settings?.SelectedSize is AiSize)
|
||||
{
|
||||
return App.AiAvailabilityState == Properties.AiAvailabilityState.Ready;
|
||||
}
|
||||
|
||||
// Non-AI resize can always proceed
|
||||
return true;
|
||||
}
|
||||
}
|
||||
public IEnumerable<ResizeUnit> ResizeUnitValues => Enum.GetValues(typeof(ResizeUnit)).Cast<ResizeUnit>();
|
||||
|
||||
public ICommand ResizeCommand { get; }
|
||||
|
||||
@@ -181,11 +72,9 @@ namespace ImageResizer.ViewModels
|
||||
|
||||
public ICommand EnterKeyPressedCommand { get; private set; }
|
||||
|
||||
public ICommand DownloadModelCommand { get; private set; }
|
||||
|
||||
// Any of the files is a gif
|
||||
public bool TryingToResizeGifFiles =>
|
||||
_batch?.Files.Any(filename => filename.EndsWith(".gif", System.StringComparison.InvariantCultureIgnoreCase)) == true;
|
||||
_batch.Files.Any(filename => filename.EndsWith(".gif", System.StringComparison.InvariantCultureIgnoreCase));
|
||||
|
||||
public void Resize()
|
||||
{
|
||||
@@ -213,234 +102,5 @@ namespace ImageResizer.ViewModels
|
||||
|
||||
public void Cancel()
|
||||
=> _mainView.Close();
|
||||
|
||||
private void HandleSettingsPropertyChanged(object sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
switch (e.PropertyName)
|
||||
{
|
||||
case nameof(Settings.SelectedSizeIndex):
|
||||
case nameof(Settings.SelectedSize):
|
||||
// Notify UI state properties that depend on SelectedSize
|
||||
NotifyAiStateChanged();
|
||||
UpdateAiDetails();
|
||||
|
||||
// Trigger CanExecuteChanged for ResizeCommand
|
||||
if (ResizeCommand is RelayCommand cmd)
|
||||
{
|
||||
cmd.OnCanExecuteChanged();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureAiScaleWithinRange()
|
||||
{
|
||||
if (Settings?.AiSize != null)
|
||||
{
|
||||
Settings.AiSize.Scale = Math.Clamp(
|
||||
Settings.AiSize.Scale,
|
||||
MinAiScale,
|
||||
MaxAiScale);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateAiDetails()
|
||||
{
|
||||
// Clear AI details if AI size not selected
|
||||
if (Settings == null || Settings.SelectedSize is not AiSize)
|
||||
{
|
||||
CurrentResolutionDescription = string.Empty;
|
||||
NewResolutionDescription = string.Empty;
|
||||
return;
|
||||
}
|
||||
|
||||
EnsureAiScaleWithinRange();
|
||||
|
||||
if (_hasMultipleFiles)
|
||||
{
|
||||
CurrentResolutionDescription = string.Empty;
|
||||
NewResolutionDescription = string.Empty;
|
||||
return;
|
||||
}
|
||||
|
||||
EnsureOriginalDimensionsLoaded();
|
||||
|
||||
var hasConcreteSize = _originalWidth.HasValue && _originalHeight.HasValue;
|
||||
CurrentResolutionDescription = hasConcreteSize
|
||||
? FormatDimensions(_originalWidth!.Value, _originalHeight!.Value)
|
||||
: Resources.Input_AiUnknownSize;
|
||||
|
||||
var scale = Settings.AiSize.Scale;
|
||||
NewResolutionDescription = hasConcreteSize
|
||||
? FormatDimensions((long)_originalWidth!.Value * scale, (long)_originalHeight!.Value * scale)
|
||||
: Resources.Input_AiUnknownSize;
|
||||
}
|
||||
|
||||
private static string FormatDimensions(long width, long height)
|
||||
{
|
||||
return string.Format(CultureInfo.CurrentCulture, "{0} × {1}", width, height);
|
||||
}
|
||||
|
||||
private void EnsureOriginalDimensionsLoaded()
|
||||
{
|
||||
if (_originalDimensionsLoaded)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var file = _batch?.Files.FirstOrDefault();
|
||||
if (string.IsNullOrEmpty(file))
|
||||
{
|
||||
_originalDimensionsLoaded = true;
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var stream = File.OpenRead(file);
|
||||
var decoder = BitmapDecoder.Create(stream, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.None);
|
||||
var frame = decoder.Frames.FirstOrDefault();
|
||||
if (frame != null)
|
||||
{
|
||||
_originalWidth = frame.PixelWidth;
|
||||
_originalHeight = frame.PixelHeight;
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Failed to load image dimensions - clear values
|
||||
_originalWidth = null;
|
||||
_originalHeight = null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_originalDimensionsLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes AI UI state based on App's cached availability state.
|
||||
/// Subscribe to state change event to update UI when background initialization completes.
|
||||
/// </summary>
|
||||
private void InitializeAiState()
|
||||
{
|
||||
// Subscribe to initialization completion event to refresh UI
|
||||
App.AiInitializationCompleted += OnAiInitializationCompleted;
|
||||
|
||||
// Set initial status message based on current state
|
||||
UpdateStatusMessage();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles AI initialization completion event from App.
|
||||
/// Refreshes UI when background initialization finishes.
|
||||
/// </summary>
|
||||
private void OnAiInitializationCompleted(object sender, Properties.AiAvailabilityState finalState)
|
||||
{
|
||||
UpdateStatusMessage();
|
||||
NotifyAiStateChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates status message based on current App availability state.
|
||||
/// </summary>
|
||||
private void UpdateStatusMessage()
|
||||
{
|
||||
ModelStatusMessage = App.AiAvailabilityState switch
|
||||
{
|
||||
Properties.AiAvailabilityState.Ready => string.Empty,
|
||||
Properties.AiAvailabilityState.ModelNotReady => Resources.Input_AiModelNotAvailable,
|
||||
Properties.AiAvailabilityState.NotSupported => Resources.Input_AiModelNotSupported,
|
||||
_ => string.Empty,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Notifies UI when AI state changes (model availability, download status).
|
||||
/// </summary>
|
||||
private void NotifyAiStateChanged()
|
||||
{
|
||||
OnPropertyChanged(nameof(IsModelDownloading));
|
||||
OnPropertyChanged(nameof(ShowModelDownloadPrompt));
|
||||
OnPropertyChanged(nameof(ShowAiControls));
|
||||
OnPropertyChanged(nameof(ShowAiSizeDescriptions));
|
||||
OnPropertyChanged(nameof(CanResize));
|
||||
|
||||
// Trigger CanExecuteChanged for ResizeCommand
|
||||
if (ResizeCommand is RelayCommand resizeCommand)
|
||||
{
|
||||
resizeCommand.OnCanExecuteChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Notifies UI when AI scale changes (slider value).
|
||||
/// </summary>
|
||||
private void NotifyAiScaleChanged()
|
||||
{
|
||||
OnPropertyChanged(nameof(AiSuperResolutionScale));
|
||||
OnPropertyChanged(nameof(AiScaleDisplay));
|
||||
UpdateAiDetails();
|
||||
}
|
||||
|
||||
private async Task DownloadModelAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Set downloading flag and show progress
|
||||
_isDownloadingModel = true;
|
||||
ModelStatusMessage = Resources.Input_AiModelDownloading;
|
||||
ModelDownloadProgress = 0;
|
||||
NotifyAiStateChanged();
|
||||
|
||||
// Create progress reporter to update UI
|
||||
var progress = new Progress<double>(value =>
|
||||
{
|
||||
// progressValue could be 0-1 or 0-100, normalize to 0-100
|
||||
ModelDownloadProgress = value > 1 ? value : value * 100;
|
||||
});
|
||||
|
||||
// Call EnsureReadyAsync to download and prepare the AI model
|
||||
var result = await WinAiSuperResolutionService.EnsureModelReadyAsync(progress);
|
||||
|
||||
if (result?.Status == Microsoft.Windows.AI.AIFeatureReadyResultState.Success)
|
||||
{
|
||||
// Model successfully downloaded and ready
|
||||
ModelDownloadProgress = 100;
|
||||
|
||||
// Update App's cached state
|
||||
App.AiAvailabilityState = Properties.AiAvailabilityState.Ready;
|
||||
UpdateStatusMessage();
|
||||
|
||||
// Initialize the AI service now that model is ready
|
||||
var aiService = await WinAiSuperResolutionService.CreateAsync();
|
||||
ResizeBatch.SetAiSuperResolutionService(aiService ?? (Services.IAISuperResolutionService)NoOpAiSuperResolutionService.Instance);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Download failed
|
||||
ModelStatusMessage = Resources.Input_AiModelDownloadFailed;
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Exception during download
|
||||
ModelStatusMessage = Resources.Input_AiModelDownloadFailed;
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Clear downloading flag
|
||||
_isDownloadingModel = false;
|
||||
|
||||
// Reset progress if not successful
|
||||
if (App.AiAvailabilityState != Properties.AiAvailabilityState.Ready)
|
||||
{
|
||||
ModelDownloadProgress = 0;
|
||||
}
|
||||
|
||||
NotifyAiStateChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,21 +11,11 @@ using System.Windows.Data;
|
||||
|
||||
namespace ImageResizer.Views
|
||||
{
|
||||
[ValueConversion(typeof(bool), typeof(Visibility))]
|
||||
[ValueConversion(typeof(Enum), typeof(string))]
|
||||
internal class BoolValueConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
bool boolValue = (bool)value;
|
||||
bool invert = parameter is string param && param.Equals("Inverted", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (invert)
|
||||
{
|
||||
boolValue = !boolValue;
|
||||
}
|
||||
|
||||
return boolValue ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
=> (bool)value ? Visibility.Visible : Visibility.Collapsed;
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
=> (Visibility)value == Visibility.Visible;
|
||||
|
||||
@@ -7,23 +7,6 @@
|
||||
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
|
||||
xmlns:v="clr-namespace:ImageResizer.Views">
|
||||
|
||||
<UserControl.Resources>
|
||||
<Style
|
||||
x:Key="ReadableDisabledButtonStyle"
|
||||
BasedOn="{StaticResource {x:Type ui:Button}}"
|
||||
TargetType="ui:Button">
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<!-- Improved disabled state: keep readable but clearly disabled -->
|
||||
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}" />
|
||||
<Setter Property="Background" Value="{DynamicResource {x:Static SystemColors.ControlBrushKey}}" />
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource {x:Static SystemColors.ControlDarkBrushKey}}" />
|
||||
<Setter Property="Opacity" Value="0.75" />
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</UserControl.Resources>
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
@@ -32,67 +15,61 @@
|
||||
<!-- other controls -->
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<StackPanel Grid.Row="0" Margin="16">
|
||||
<ComboBox
|
||||
Name="SizeComboBox"
|
||||
Height="64"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
VerticalContentAlignment="Stretch"
|
||||
AutomationProperties.HelpText="{Binding Settings.SelectedSize, Converter={StaticResource SizeTypeToHelpTextConverter}}"
|
||||
AutomationProperties.Name="{x:Static p:Resources.Image_Sizes}"
|
||||
ItemsSource="{Binding Settings.AllSizes}"
|
||||
SelectedItem="{Binding Settings.SelectedSize, Mode=TwoWay}">
|
||||
<ComboBox.ItemContainerStyle>
|
||||
<Style BasedOn="{StaticResource {x:Type ComboBoxItem}}" TargetType="ComboBoxItem">
|
||||
<Setter Property="AutomationProperties.Name" Value="{Binding Name}" />
|
||||
<Setter Property="AutomationProperties.HelpText" Value="{Binding ., Converter={StaticResource SizeTypeToHelpTextConverter}}" />
|
||||
</Style>
|
||||
</ComboBox.ItemContainerStyle>
|
||||
<ComboBox.Resources>
|
||||
<DataTemplate DataType="{x:Type m:ResizeSize}">
|
||||
<Grid VerticalAlignment="Center">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock Style="{StaticResource BodyStrongTextBlockStyle}" Text="{Binding Name}" />
|
||||
<StackPanel Grid.Row="1" Orientation="Horizontal">
|
||||
<TextBlock Text="{Binding Fit, Converter={StaticResource EnumValueConverter}, ConverterParameter=ThirdPersonSingular}" />
|
||||
<TextBlock
|
||||
Margin="4,0,0,0"
|
||||
Style="{StaticResource BodyStrongTextBlockStyle}"
|
||||
Text="{Binding Width, Converter={StaticResource AutoDoubleConverter}, ConverterParameter=Auto}" />
|
||||
<TextBlock
|
||||
Margin="4,0,0,0"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
|
||||
Text="×"
|
||||
Visibility="{Binding ShowHeight, Converter={StaticResource BoolValueConverter}}" />
|
||||
<TextBlock
|
||||
Margin="4,0,0,0"
|
||||
Style="{StaticResource BodyStrongTextBlockStyle}"
|
||||
Text="{Binding Height, Converter={StaticResource AutoDoubleConverter}, ConverterParameter=Auto}"
|
||||
Visibility="{Binding ShowHeight, Converter={StaticResource BoolValueConverter}}" />
|
||||
<TextBlock Margin="4,0,0,0" Text="{Binding Unit, Converter={StaticResource EnumValueConverter}, ConverterParameter=ToLower}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate DataType="{x:Type m:CustomSize}">
|
||||
<Grid VerticalAlignment="Center">
|
||||
<TextBlock FontWeight="SemiBold" Text="{Binding Name}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate DataType="{x:Type m:AiSize}">
|
||||
<StackPanel VerticalAlignment="Center" Orientation="Vertical">
|
||||
<TextBlock FontWeight="SemiBold" Text="{x:Static p:Resources.Input_AiSuperResolution}" />
|
||||
<TextBlock Text="{x:Static p:Resources.Input_AiSuperResolutionDescription}" />
|
||||
<ComboBox
|
||||
Name="SizeComboBox"
|
||||
Grid.Row="0"
|
||||
Height="64"
|
||||
Margin="16"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
VerticalContentAlignment="Stretch"
|
||||
AutomationProperties.HelpText="{Binding Settings.SelectedSize, Converter={StaticResource SizeTypeToHelpTextConverter}}"
|
||||
AutomationProperties.Name="{x:Static p:Resources.Image_Sizes}"
|
||||
ItemsSource="{Binding Settings.AllSizes}"
|
||||
SelectedIndex="{Binding Settings.SelectedSizeIndex}">
|
||||
<ComboBox.ItemContainerStyle>
|
||||
<Style BasedOn="{StaticResource {x:Type ComboBoxItem}}" TargetType="ComboBoxItem">
|
||||
<Setter Property="AutomationProperties.Name" Value="{Binding Name}" />
|
||||
<Setter Property="AutomationProperties.HelpText" Value="{Binding ., Converter={StaticResource SizeTypeToHelpTextConverter}}" />
|
||||
</Style>
|
||||
</ComboBox.ItemContainerStyle>
|
||||
<ComboBox.Resources>
|
||||
<DataTemplate DataType="{x:Type m:ResizeSize}">
|
||||
<Grid VerticalAlignment="Center">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock Style="{StaticResource BodyStrongTextBlockStyle}" Text="{Binding Name}" />
|
||||
<StackPanel Grid.Row="1" Orientation="Horizontal">
|
||||
<TextBlock Text="{Binding Fit, Converter={StaticResource EnumValueConverter}, ConverterParameter=ThirdPersonSingular}" />
|
||||
<TextBlock
|
||||
Margin="4,0,0,0"
|
||||
Style="{StaticResource BodyStrongTextBlockStyle}"
|
||||
Text="{Binding Width, Converter={StaticResource AutoDoubleConverter}, ConverterParameter=Auto}" />
|
||||
<TextBlock
|
||||
Margin="4,0,0,0"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
|
||||
Text="×"
|
||||
Visibility="{Binding ShowHeight, Converter={StaticResource BoolValueConverter}}" />
|
||||
<TextBlock
|
||||
Margin="4,0,0,0"
|
||||
Style="{StaticResource BodyStrongTextBlockStyle}"
|
||||
Text="{Binding Height, Converter={StaticResource AutoDoubleConverter}, ConverterParameter=Auto}"
|
||||
Visibility="{Binding ShowHeight, Converter={StaticResource BoolValueConverter}}" />
|
||||
<TextBlock Margin="4,0,0,0" Text="{Binding Unit, Converter={StaticResource EnumValueConverter}, ConverterParameter=ToLower}" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ComboBox.Resources>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate DataType="{x:Type m:CustomSize}">
|
||||
<Grid VerticalAlignment="Center">
|
||||
<TextBlock FontWeight="SemiBold" Text="{Binding Name}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ComboBox.Resources>
|
||||
</ComboBox>
|
||||
|
||||
<Grid Grid.Row="1">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
@@ -107,90 +84,6 @@
|
||||
BorderBrush="{DynamicResource CardStrokeColorDefaultBrush}"
|
||||
BorderThickness="0,1,0,0" />
|
||||
|
||||
<!-- AI Configuration Panel -->
|
||||
<Grid Margin="16">
|
||||
<!-- AI Model Download Prompt -->
|
||||
<StackPanel>
|
||||
<StackPanel.Style>
|
||||
<Style TargetType="StackPanel">
|
||||
<Setter Property="Visibility" Value="Collapsed" />
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding ShowModelDownloadPrompt}" Value="True">
|
||||
<Setter Property="Visibility" Value="Visible" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</StackPanel.Style>
|
||||
|
||||
<ui:InfoBar
|
||||
IsClosable="False"
|
||||
IsOpen="True"
|
||||
Message="{Binding ModelStatusMessage}"
|
||||
Severity="Informational" />
|
||||
|
||||
<ui:Button
|
||||
Margin="0,8,0,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
Appearance="Primary"
|
||||
Command="{Binding DownloadModelCommand}"
|
||||
Content="{x:Static p:Resources.Input_AiModelDownloadButton}"
|
||||
Visibility="{Binding IsModelDownloading, Converter={StaticResource BoolValueConverter}, ConverterParameter=Inverted}" />
|
||||
|
||||
<StackPanel Margin="0,8,0,0" Visibility="{Binding IsModelDownloading, Converter={StaticResource BoolValueConverter}}">
|
||||
<ui:ProgressRing IsIndeterminate="True" />
|
||||
<TextBlock
|
||||
Margin="0,8,0,0"
|
||||
HorizontalAlignment="Center"
|
||||
Text="{Binding ModelStatusMessage}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<!-- AI Scale Controls -->
|
||||
<StackPanel>
|
||||
<StackPanel.Style>
|
||||
<Style TargetType="StackPanel">
|
||||
<Setter Property="Visibility" Value="Collapsed" />
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding ShowAiControls}" Value="True">
|
||||
<Setter Property="Visibility" Value="Visible" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</StackPanel.Style>
|
||||
|
||||
<Grid>
|
||||
<TextBlock Text="{x:Static p:Resources.Input_AiCurrentLabel}" />
|
||||
<TextBlock HorizontalAlignment="Right" Text="{Binding AiScaleDisplay}" />
|
||||
</Grid>
|
||||
|
||||
<Slider
|
||||
Margin="0,8,0,0"
|
||||
AutomationProperties.Name="{x:Static p:Resources.Input_AiScaleLabel}"
|
||||
IsSelectionRangeEnabled="False"
|
||||
IsSnapToTickEnabled="True"
|
||||
Maximum="8"
|
||||
Minimum="1"
|
||||
TickFrequency="1"
|
||||
TickPlacement="BottomRight"
|
||||
Ticks="1,2,3,4,5,6,7,8"
|
||||
Value="{Binding AiSuperResolutionScale, Mode=TwoWay}" />
|
||||
|
||||
<StackPanel Margin="0,16,0,0" Visibility="{Binding ShowAiSizeDescriptions, Converter={StaticResource BoolValueConverter}}">
|
||||
<Grid>
|
||||
<TextBlock Foreground="{DynamicResource TextFillColorSecondaryBrush}" Text="{x:Static p:Resources.Input_AiCurrentLabel}" />
|
||||
<TextBlock
|
||||
HorizontalAlignment="Right"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
|
||||
Text="{Binding CurrentResolutionDescription}" />
|
||||
</Grid>
|
||||
<Grid Margin="0,8,0,0">
|
||||
<TextBlock Text="{x:Static p:Resources.Input_AiNewLabel}" />
|
||||
<TextBlock HorizontalAlignment="Right" Text="{Binding NewResolutionDescription}" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- "Custom" input matrix -->
|
||||
<Grid Margin="16" Visibility="{Binding ElementName=SizeComboBox, Path=SelectedValue, Converter={StaticResource SizeTypeToVisibilityConverter}}">
|
||||
<Grid.ColumnDefinitions>
|
||||
@@ -387,8 +280,7 @@
|
||||
Appearance="Primary"
|
||||
AutomationProperties.Name="{x:Static p:Resources.Resize_Tooltip}"
|
||||
Command="{Binding ResizeCommand}"
|
||||
IsDefault="True"
|
||||
Style="{StaticResource ReadableDisabledButtonStyle}">
|
||||
IsDefault="True">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<ui:SymbolIcon FontSize="16" Symbol="ResizeLarge16" />
|
||||
<TextBlock Margin="8,0,0,0" Text="{x:Static p:Resources.Input_Resize}" />
|
||||
|
||||
19
src/modules/powerrename/PowerRenameUILib/packages.config
Normal file
19
src/modules/powerrename/PowerRenameUILib/packages.config
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="boost" version="1.87.0" targetFramework="native" />
|
||||
<package id="boost_regex-vc143" version="1.87.0" targetFramework="native" />
|
||||
<package id="Microsoft.Web.WebView2" version="1.0.2903.40" targetFramework="native" />
|
||||
<package id="Microsoft.Windows.CppWinRT" version="2.0.240111.5" targetFramework="native" />
|
||||
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.231216.1" targetFramework="native" />
|
||||
<package id="Microsoft.Windows.SDK.BuildTools" version="10.0.26100.4188" targetFramework="native" />
|
||||
<package id="Microsoft.WindowsAppSDK" version="1.8.250907003" targetFramework="native" />
|
||||
<package id="Microsoft.WindowsAppSDK.Base" version="1.8.250831001" targetFramework="native" />
|
||||
<package id="Microsoft.WindowsAppSDK.Foundation" version="1.8.250906002" targetFramework="native" />
|
||||
<package id="Microsoft.WindowsAppSDK.WinUI" version="1.8.250906003" targetFramework="native" />
|
||||
<package id="Microsoft.WindowsAppSDK.Runtime" version="1.8.250907003" targetFramework="native" />
|
||||
<package id="Microsoft.WindowsAppSDK.DWrite" version="1.8.25090401" targetFramework="native" />
|
||||
<package id="Microsoft.WindowsAppSDK.InteractiveExperiences" version="1.8.250906004" targetFramework="native" />
|
||||
<package id="Microsoft.WindowsAppSDK.Widgets" version="1.8.250904007" targetFramework="native" />
|
||||
<package id="Microsoft.WindowsAppSDK.AI" version="1.8.37" targetFramework="native" />
|
||||
<package id="Microsoft.Windows.SDK.BuildTools.MSIX" version="1.7.20250829.1" targetFramework="native" />
|
||||
</packages>
|
||||
@@ -407,18 +407,15 @@ HRESULT GetDatedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source, SY
|
||||
hour12 = 12;
|
||||
}
|
||||
|
||||
// Order matters. Longer patterns are processed before any prefixes.
|
||||
// Years.
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%04d"), L"$01", fileTime.wYear);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$YYYY"), replaceTerm);
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", (fileTime.wYear % 100));
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$YY"), replaceTerm);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$YY(?![A-Z])"), replaceTerm); // Negative lookahead prevents matching $YYY, $YYYY, or metadata patterns
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", (fileTime.wYear % 10));
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$Y"), replaceTerm);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$Y(?![A-Z])"), replaceTerm); // Negative lookahead prevents matching $YY, $YYYY, or metadata patterns
|
||||
|
||||
// Months.
|
||||
GetDateFormatEx(localeName, NULL, &fileTime, L"MMMM", formattedDate, MAX_PATH, NULL);
|
||||
formattedDate[0] = towupper(formattedDate[0]);
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%s"), L"$01", formattedDate);
|
||||
@@ -427,15 +424,14 @@ HRESULT GetDatedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source, SY
|
||||
GetDateFormatEx(localeName, NULL, &fileTime, L"MMM", formattedDate, MAX_PATH, NULL);
|
||||
formattedDate[0] = towupper(formattedDate[0]);
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%s"), L"$01", formattedDate);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$MMM"), replaceTerm);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$MMM(?!M)"), replaceTerm); // Negative lookahead prevents matching $MMMM
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", fileTime.wMonth);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$MM"), replaceTerm);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$MM(?![A-Z])"), replaceTerm); // Negative lookahead prevents matching $MMM, $MMMM, or metadata patterns
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", fileTime.wMonth);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$M"), replaceTerm);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$M(?![A-Z])"), replaceTerm); // Negative lookahead prevents matching $MM, $MMM, $MMMM, or metadata patterns
|
||||
|
||||
// Days.
|
||||
GetDateFormatEx(localeName, NULL, &fileTime, L"dddd", formattedDate, MAX_PATH, NULL);
|
||||
formattedDate[0] = towupper(formattedDate[0]);
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%s"), L"$01", formattedDate);
|
||||
@@ -444,27 +440,19 @@ HRESULT GetDatedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source, SY
|
||||
GetDateFormatEx(localeName, NULL, &fileTime, L"ddd", formattedDate, MAX_PATH, NULL);
|
||||
formattedDate[0] = towupper(formattedDate[0]);
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%s"), L"$01", formattedDate);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$DDD"), replaceTerm);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$DDD(?![A-Z])"), replaceTerm); // Negative lookahead prevents matching $DDDD or metadata patterns
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", fileTime.wDay);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$DD"), replaceTerm);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$DD(?![A-Z])"), replaceTerm); // Negative lookahead prevents matching $DDD, $DDDD, or metadata patterns
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", fileTime.wDay);
|
||||
// $D overlaps with metadata patterns like $DATE_TAKEN_YYYY, so we use negative
|
||||
// lookahead to prevent matching those.
|
||||
res = regex_replace(
|
||||
res,
|
||||
std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$D(?!(ATE_TAKEN_|ESCRIPTION|OCUMENT_ID))"), /* #no-spell-check-line */
|
||||
replaceTerm);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$D(?![A-Z])"), replaceTerm); // Negative lookahead prevents matching $DD, $DDD, $DDDD, or metadata patterns like $DATE_TAKEN_YYYY
|
||||
|
||||
// Time.
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", hour12);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$HH"), replaceTerm);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$HH(?![A-Z])"), replaceTerm); // Negative lookahead prevents matching $HHH or metadata patterns
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", hour12);
|
||||
// $H overlaps with metadata's $HEIGHT, so we use negative lookahead to prevent
|
||||
// matching that.
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$H(?!(EIGHT))"), replaceTerm);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$H(?![A-Z])"), replaceTerm); // Negative lookahead prevents matching $HH or metadata patterns
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%s"), L"$01", (fileTime.wHour < 12) ? L"AM" : L"PM");
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$TT"), replaceTerm);
|
||||
@@ -473,31 +461,31 @@ HRESULT GetDatedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source, SY
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$tt"), replaceTerm);
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", fileTime.wHour);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$hh"), replaceTerm);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$hh(?!h)"), replaceTerm); // Negative lookahead prevents matching $hhh
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", fileTime.wHour);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$h"), replaceTerm);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$h(?!h)"), replaceTerm); // Negative lookahead prevents matching $hh
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", fileTime.wMinute);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$mm"), replaceTerm);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$mm(?!m)"), replaceTerm); // Negative lookahead prevents matching $mmm
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", fileTime.wMinute);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$m"), replaceTerm);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$m(?!m)"), replaceTerm); // Negative lookahead prevents matching $mm
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", fileTime.wSecond);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$ss"), replaceTerm);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$ss(?!s)"), replaceTerm); // Negative lookahead prevents matching $sss
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", fileTime.wSecond);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$s"), replaceTerm);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$s(?!s)"), replaceTerm); // Negative lookahead prevents matching $ss
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%03d"), L"$01", fileTime.wMilliseconds);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$fff"), replaceTerm);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$fff(?!f)"), replaceTerm); // Negative lookahead prevents matching $ffff
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", fileTime.wMilliseconds / 10);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$ff"), replaceTerm);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$ff(?!f)"), replaceTerm); // Negative lookahead prevents matching $fff
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", fileTime.wMilliseconds / 100);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$f"), replaceTerm);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$f(?!f)"), replaceTerm); // Negative lookahead prevents matching $ff or $fff
|
||||
|
||||
hr = StringCchCopy(result, cchMax, res.c_str());
|
||||
}
|
||||
|
||||
@@ -507,26 +507,24 @@ namespace HelpersTests
|
||||
return testTime;
|
||||
}
|
||||
|
||||
// Category 1: Tests for patterns with extra characters. Verifies negative
|
||||
// lookahead doesn't cause issues with partially matched patterns and the
|
||||
// ordering of pattern matches is correct, i.e. longer patterns are matched
|
||||
// first.
|
||||
// Category 1: Tests for invalid patterns with extra characters (verify negative lookahead prevents wrong matching)
|
||||
|
||||
TEST_METHOD(ValidPattern_YYY_PartiallyMatched)
|
||||
TEST_METHOD(InvalidPattern_YYY_NotMatched)
|
||||
{
|
||||
// Test $YYY (3 Y's) is recognized as a valid pattern $YY plus a verbatim 'Y'
|
||||
// Test $YYY (3 Y's) is not a valid pattern and should remain unchanged
|
||||
// Negative lookahead in $YY(?!Y) prevents matching $YYY
|
||||
SYSTEMTIME testTime = GetTestTime();
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$YYY", testTime);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"file_24Y", result);
|
||||
Assert::AreEqual(L"file_$YYY", result); // $YYY is invalid, should remain unchanged
|
||||
}
|
||||
|
||||
TEST_METHOD(ValidPattern_DDD_Matched)
|
||||
TEST_METHOD(InvalidPattern_DDD_NotPartiallyMatched)
|
||||
{
|
||||
// Test that $DDD (short weekday) is not confused with $DD (2-digit day)
|
||||
// Verifies that the matching of $DDD before $DD works correctly
|
||||
// This verifies negative lookahead works correctly
|
||||
SYSTEMTIME testTime = GetTestTime();
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$DDD", testTime);
|
||||
@@ -535,10 +533,9 @@ namespace HelpersTests
|
||||
Assert::AreEqual(L"file_Fri", result); // Should be "Fri", not "15D"
|
||||
}
|
||||
|
||||
TEST_METHOD(ValidPattern_MMM_Matched)
|
||||
TEST_METHOD(InvalidPattern_MMM_NotPartiallyMatched)
|
||||
{
|
||||
// Test that $MMM (short month name) is not confused with $MM (2-digit month)
|
||||
// Verifies that the matching of $MMM before $MM works correctly
|
||||
SYSTEMTIME testTime = GetTestTime();
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$MMM", testTime);
|
||||
@@ -547,16 +544,15 @@ namespace HelpersTests
|
||||
Assert::AreEqual(L"file_Mar", result); // Should be "Mar", not "03M"
|
||||
}
|
||||
|
||||
TEST_METHOD(ValidPattern_HHH_PartiallyMatched)
|
||||
TEST_METHOD(InvalidPattern_HHH_NotMatched)
|
||||
{
|
||||
// Test $HHH (3 H's) should match $HH and leave extra H unchanged
|
||||
// Also confirms that $HH is matched before $H
|
||||
// Test $HHH (3 H's) is not valid and negative lookahead prevents $HH from matching
|
||||
SYSTEMTIME testTime = GetTestTime();
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$HHH", testTime);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"file_02H", result);
|
||||
Assert::AreEqual(L"file_$HHH", result); // Should remain unchanged
|
||||
}
|
||||
|
||||
TEST_METHOD(SeparatedPatterns_SingleY)
|
||||
@@ -673,9 +669,9 @@ namespace HelpersTests
|
||||
Assert::AreEqual(E_INVALIDARG, hr);
|
||||
}
|
||||
|
||||
// Category 4: Tests to explicitly verify execution order
|
||||
// Category 4: Tests to explicitly verify negative lookahead is working
|
||||
|
||||
TEST_METHOD(ExecutionOrder_YearNotMatchedInYYYY)
|
||||
TEST_METHOD(NegativeLookahead_YearNotMatchedInYYYY)
|
||||
{
|
||||
// Verify $Y doesn't match when part of $YYYY
|
||||
SYSTEMTIME testTime = GetTestTime();
|
||||
@@ -686,9 +682,9 @@ namespace HelpersTests
|
||||
Assert::AreEqual(L"file_2024", result); // Should be "2024", not "202Y"
|
||||
}
|
||||
|
||||
TEST_METHOD(ExecutionOrder_MonthNotMatchedInMMM)
|
||||
TEST_METHOD(NegativeLookahead_MonthNotMatchedInMMM)
|
||||
{
|
||||
// Verify $M or $MM don't match when $MMM is given
|
||||
// Verify $M doesn't match when part of $MMM
|
||||
SYSTEMTIME testTime = GetTestTime();
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$MMM", testTime);
|
||||
@@ -697,9 +693,9 @@ namespace HelpersTests
|
||||
Assert::AreEqual(L"file_Mar", result); // Should be "Mar", not "3ar"
|
||||
}
|
||||
|
||||
TEST_METHOD(ExecutionOrder_DayNotMatchedInDDDD)
|
||||
TEST_METHOD(NegativeLookahead_DayNotMatchedInDDDD)
|
||||
{
|
||||
// Verify $D or $DD don't match when $DDDD is given
|
||||
// Verify $D doesn't match when part of $DDDD
|
||||
SYSTEMTIME testTime = GetTestTime();
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$DDDD", testTime);
|
||||
@@ -708,7 +704,7 @@ namespace HelpersTests
|
||||
Assert::AreEqual(L"file_Friday", result); // Should be "Friday", not "15riday"
|
||||
}
|
||||
|
||||
TEST_METHOD(ExecutionOrder_HourNotMatchedInHH)
|
||||
TEST_METHOD(NegativeLookahead_HourNotMatchedInHH)
|
||||
{
|
||||
// Verify $H doesn't match when part of $HH
|
||||
// Note: $HH is 12-hour format, so 14:00 (2 PM) displays as "02"
|
||||
@@ -720,9 +716,9 @@ namespace HelpersTests
|
||||
Assert::AreEqual(L"file_02", result); // 14:00 in 12-hour format is "02 PM"
|
||||
}
|
||||
|
||||
TEST_METHOD(ExecutionOrder_MillisecondNotMatchedInFFF)
|
||||
TEST_METHOD(NegativeLookahead_MillisecondNotMatchedInFFF)
|
||||
{
|
||||
// Verify $f or $ff don't match when $fff is given
|
||||
// Verify $f doesn't match when part of $fff
|
||||
SYSTEMTIME testTime = GetTestTime();
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$fff", testTime);
|
||||
@@ -766,68 +762,5 @@ namespace HelpersTests
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"15-15-Fri-Friday", result);
|
||||
}
|
||||
|
||||
// Category 6: Specific bug fixes and collision avoidance
|
||||
|
||||
TEST_METHOD(BugFix_DDT_AllowsSuffixT)
|
||||
{
|
||||
// #44202 - $DDT should be allowed and matched as $DD plus verbatim 'T'. It
|
||||
// was previously blocked due to the negative lookahead for any capital
|
||||
// letter after $DD.
|
||||
SYSTEMTIME testTime = GetTestTime();
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$DDT", testTime);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"file_15T", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(RelaxedConstraint_VerbatimCapitalAfterPatterns)
|
||||
{
|
||||
// Verify that patterns can be followed by capital letters that are not part
|
||||
// of longer patterns, e.g., $DDC should match $DD + 'C'.
|
||||
SYSTEMTIME testTime = GetTestTime();
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$YYYYA_$MMB_$DDC", testTime); /* #no-spell-check-line */
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"file_2024A_03B_15C", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(Collision_DateTaken_Protected)
|
||||
{
|
||||
// Verify that date patterns do not collide with metadata patterns like
|
||||
// DATE_TAKEN_YYYY.
|
||||
SYSTEMTIME testTime = GetTestTime();
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$DATE_TAKEN_YYYY", testTime);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"file_$DATE_TAKEN_YYYY", result); // Not replaced
|
||||
}
|
||||
|
||||
TEST_METHOD(Collision_Height_Protected)
|
||||
{
|
||||
// Verify that HEIGHT metadata pattern does not collide with date pattern $H.
|
||||
SYSTEMTIME testTime = GetTestTime();
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$HEIGHT", testTime);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"file_$HEIGHT", result); // Not replaced
|
||||
}
|
||||
|
||||
TEST_METHOD(Collision_SafeSuffix_Deer)
|
||||
{
|
||||
// Verifies that patterns can be safely followed by certain suffix letters as
|
||||
// long as they don't match a longer pattern. $DEER should be matched as
|
||||
// $D + 'EER'
|
||||
SYSTEMTIME testTime = GetTestTime();
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$DEER", testTime);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"file_15EER", result);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
// Detect AI capabilities by calling ImageResizer in detection mode.
|
||||
// This runs in a background thread to avoid blocking.
|
||||
// ImageResizer writes the result to a cache file that it reads on normal startup.
|
||||
//
|
||||
// Parameters:
|
||||
// skipSettingsCheck - If true, skip checking if ImageResizer is enabled in settings.
|
||||
// Use this when called from apply_general_settings where we know
|
||||
// ImageResizer is being enabled but settings file may not be saved yet.
|
||||
void DetectAiCapabilitiesAsync(bool skipSettingsCheck = false);
|
||||
@@ -10,7 +10,6 @@
|
||||
#include <common/themes/windows_colors.h>
|
||||
|
||||
#include "trace.h"
|
||||
#include "ai_detection.h"
|
||||
#include <common/utils/elevation.h>
|
||||
#include <common/version/version.h>
|
||||
#include <common/utils/resources.h>
|
||||
@@ -280,13 +279,6 @@ void apply_general_settings(const json::JsonObject& general_configs, bool save)
|
||||
powertoy->enable();
|
||||
auto& hkmng = HotkeyConflictDetector::HotkeyConflictManager::GetInstance();
|
||||
hkmng.EnableHotkeyByModule(name);
|
||||
|
||||
// Trigger AI capability detection when ImageResizer is enabled
|
||||
if (name == L"Image Resizer")
|
||||
{
|
||||
Logger::info(L"ImageResizer enabled, triggering AI capability detection");
|
||||
DetectAiCapabilitiesAsync(true); // Skip settings check since we know it's being enabled
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -34,11 +34,8 @@
|
||||
|
||||
#include <Psapi.h>
|
||||
#include <RestartManager.h>
|
||||
#include <shellapi.h>
|
||||
#include "centralized_kb_hook.h"
|
||||
#include "centralized_hotkeys.h"
|
||||
#include "ai_detection.h"
|
||||
#include <common/utils/package.h>
|
||||
|
||||
#if _DEBUG && _WIN64
|
||||
#include "unhandled_exception_handler.h"
|
||||
@@ -79,87 +76,6 @@ void chdir_current_executable()
|
||||
}
|
||||
}
|
||||
|
||||
// Detect AI capabilities by calling ImageResizer in detection mode.
|
||||
// This runs in a background thread to avoid blocking the main startup.
|
||||
// ImageResizer writes the result to a cache file that it reads on normal startup.
|
||||
void DetectAiCapabilitiesAsync(bool skipSettingsCheck)
|
||||
{
|
||||
std::thread([skipSettingsCheck]() {
|
||||
try
|
||||
{
|
||||
// Check if ImageResizer module is enabled (skip if called from apply_general_settings)
|
||||
if (!skipSettingsCheck)
|
||||
{
|
||||
auto settings = PTSettingsHelper::load_general_settings();
|
||||
if (json::has(settings, L"enabled", json::JsonValueType::Object))
|
||||
{
|
||||
auto enabledModules = settings.GetNamedObject(L"enabled");
|
||||
if (json::has(enabledModules, L"Image Resizer", json::JsonValueType::Boolean))
|
||||
{
|
||||
bool isEnabled = enabledModules.GetNamedBoolean(L"Image Resizer", false);
|
||||
if (!isEnabled)
|
||||
{
|
||||
Logger::info(L"ImageResizer module is disabled, skipping AI detection");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get ImageResizer.exe path (located in WinUI3Apps folder)
|
||||
std::wstring imageResizerPath = get_module_folderpath();
|
||||
imageResizerPath += L"\\WinUI3Apps\\PowerToys.ImageResizer.exe";
|
||||
|
||||
if (!std::filesystem::exists(imageResizerPath))
|
||||
{
|
||||
Logger::warn(L"ImageResizer.exe not found at {}, skipping AI detection", imageResizerPath);
|
||||
return;
|
||||
}
|
||||
|
||||
Logger::info(L"Starting AI capability detection via ImageResizer");
|
||||
|
||||
// Call ImageResizer --detect-ai
|
||||
SHELLEXECUTEINFO sei = { sizeof(sei) };
|
||||
sei.fMask = SEE_MASK_NOCLOSEPROCESS | SEE_MASK_FLAG_NO_UI;
|
||||
sei.lpFile = imageResizerPath.c_str();
|
||||
sei.lpParameters = L"--detect-ai";
|
||||
sei.nShow = SW_HIDE;
|
||||
|
||||
if (ShellExecuteExW(&sei))
|
||||
{
|
||||
// Wait for detection to complete (with timeout)
|
||||
DWORD waitResult = WaitForSingleObject(sei.hProcess, 30000); // 30 second timeout
|
||||
CloseHandle(sei.hProcess);
|
||||
|
||||
if (waitResult == WAIT_OBJECT_0)
|
||||
{
|
||||
Logger::info(L"AI capability detection completed successfully");
|
||||
}
|
||||
else if (waitResult == WAIT_TIMEOUT)
|
||||
{
|
||||
Logger::warn(L"AI capability detection timed out");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::warn(L"AI capability detection wait failed");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::warn(L"Failed to launch ImageResizer for AI detection, error: {}", GetLastError());
|
||||
}
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
Logger::error("Exception during AI capability detection: {}", e.what());
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::error("Unknown exception during AI capability detection");
|
||||
}
|
||||
}).detach();
|
||||
}
|
||||
|
||||
inline wil::unique_mutex_nothrow create_msi_mutex()
|
||||
{
|
||||
return createAppMutex(POWERTOYS_MSI_MUTEX_NAME);
|
||||
@@ -211,18 +127,6 @@ int runner(bool isProcessElevated, bool openSettings, std::string settingsWindow
|
||||
PeriodicUpdateWorker();
|
||||
} }.detach();
|
||||
|
||||
// Start AI capability detection in background (Windows 11+ only)
|
||||
// AI Super Resolution is not supported on Windows 10
|
||||
// This calls ImageResizer --detect-ai which writes result to cache file
|
||||
if (package::IsWin11OrGreater())
|
||||
{
|
||||
DetectAiCapabilitiesAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::info(L"AI capability detection skipped: Windows 10 does not support AI Super Resolution");
|
||||
}
|
||||
|
||||
std::thread{ [] {
|
||||
if (updating::uninstall_previous_msix_version_async().get())
|
||||
{
|
||||
|
||||
@@ -81,7 +81,6 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="ActionRunnerUtils.h" />
|
||||
<ClInclude Include="ai_detection.h" />
|
||||
<ClInclude Include="auto_start_helper.h" />
|
||||
<ClInclude Include="bug_report.h" />
|
||||
<ClInclude Include="centralized_hotkeys.h" />
|
||||
@@ -133,6 +132,9 @@
|
||||
<Project>{17da04df-e393-4397-9cf0-84dabe11032e}</Project>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Manifest Include="PowerToys.exe.manifest" />
|
||||
</ItemGroup>
|
||||
@@ -146,6 +148,7 @@
|
||||
<ItemGroup>
|
||||
<!-- Remove ALL injected versions of the file -->
|
||||
<ClCompile Remove="@(ClCompile)" Condition="'%(Filename)' == 'WindowsAppRuntimeAutoInitializer'" />
|
||||
|
||||
<!-- Add ONE copy back manually -->
|
||||
<ClCompile Include="$(PkgMicrosoft_WindowsAppSDK_Foundation)\include\WindowsAppRuntimeAutoInitializer.cpp">
|
||||
<PrecompiledHeader>NotUsing</PrecompiledHeader>
|
||||
|
||||
@@ -81,9 +81,6 @@
|
||||
<ClInclude Include="ActionRunnerUtils.h">
|
||||
<Filter>Utils</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="ai_detection.h">
|
||||
<Filter>Utils</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="resource.h">
|
||||
<Filter>Utils</Filter>
|
||||
</ClInclude>
|
||||
@@ -118,6 +115,7 @@
|
||||
<CopyFileToFolders Include="svgs\icon.ico" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
<None Include="runner.base.rc" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
@@ -314,7 +314,10 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
string selectedType = draft.ServiceType ?? string.Empty;
|
||||
AIServiceType serviceKind = draft.ServiceTypeKind;
|
||||
|
||||
bool requiresEndpoint = RequiresEndpointForService(serviceKind);
|
||||
bool requiresEndpoint = serviceKind is AIServiceType.AzureOpenAI
|
||||
or AIServiceType.AzureAIInference
|
||||
or AIServiceType.Mistral
|
||||
or AIServiceType.Ollama;
|
||||
bool requiresDeployment = serviceKind == AIServiceType.AzureOpenAI;
|
||||
bool requiresApiVersion = serviceKind == AIServiceType.AzureOpenAI;
|
||||
bool requiresModelPath = serviceKind == AIServiceType.Onnx;
|
||||
@@ -785,17 +788,12 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
string serviceType = draft.ServiceType ?? "OpenAI";
|
||||
string apiKey = PasteAIApiKeyPasswordBox.Password;
|
||||
string trimmedApiKey = apiKey?.Trim() ?? string.Empty;
|
||||
var serviceKind = draft.ServiceTypeKind;
|
||||
bool requiresEndpoint = RequiresEndpointForService(serviceKind);
|
||||
string endpoint = (draft.EndpointUrl ?? string.Empty).Trim();
|
||||
|
||||
// Never persist placeholder text or stale values for services that don't use an endpoint.
|
||||
if (!requiresEndpoint)
|
||||
if (endpoint == string.Empty)
|
||||
{
|
||||
endpoint = string.Empty;
|
||||
endpoint = GetEndpointPlaceholder(draft.ServiceTypeKind);
|
||||
}
|
||||
|
||||
// For endpoint-based services, keep empty if the user didn't provide a value.
|
||||
if (RequiresApiKeyForService(serviceType) && string.IsNullOrWhiteSpace(trimmedApiKey))
|
||||
{
|
||||
args.Cancel = true;
|
||||
@@ -835,14 +833,6 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
};
|
||||
}
|
||||
|
||||
private static bool RequiresEndpointForService(AIServiceType serviceKind)
|
||||
{
|
||||
return serviceKind is AIServiceType.AzureOpenAI
|
||||
or AIServiceType.AzureAIInference
|
||||
or AIServiceType.Mistral
|
||||
or AIServiceType.Ollama;
|
||||
}
|
||||
|
||||
private static string GetEndpointPlaceholder(AIServiceType serviceKind)
|
||||
{
|
||||
return serviceKind switch
|
||||
@@ -851,7 +841,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
AIServiceType.AzureAIInference => "https://{resource-name}.cognitiveservices.azure.com/",
|
||||
AIServiceType.Mistral => "https://api.mistral.ai/v1/",
|
||||
AIServiceType.Ollama => "http://localhost:11434/",
|
||||
_ => string.Empty,
|
||||
_ => "https://your-resource.openai.azure.com/",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,14 @@
|
||||
Build and package PowerToys (CmdPal and installer) for a specific platform and configuration LOCALLY.
|
||||
|
||||
.DESCRIPTION
|
||||
Builds and packages PowerToys (CmdPal and installer) locally. Handles solution build, signing, and WiX installer generation.
|
||||
This script automates the end-to-end build and packaging process for PowerToys, including:
|
||||
- Restoring and building all necessary solutions (CmdPal, BugReportTool, etc.)
|
||||
- Cleaning up old output
|
||||
- Signing generated .msix packages
|
||||
- Building the WiX v5 (VNext) MSI and bootstrapper installers
|
||||
|
||||
It is designed to work in local development.
|
||||
The cert used to sign the packages is generated by
|
||||
|
||||
.PARAMETER Platform
|
||||
Specifies the target platform for the build (e.g., 'arm64', 'x64'). Default is 'x64'.
|
||||
@@ -27,12 +34,10 @@ Runs the pipeline for x64 Release.
|
||||
Runs the pipeline for x64 Release with machine-wide installer.
|
||||
|
||||
.NOTES
|
||||
- Make sure to run this script from a Developer PowerShell (e.g., VS2022 Developer PowerShell).
|
||||
- Generated MSIX files will be signed using cert-sign-package.ps1.
|
||||
- This script uses git to manage workspace state:
|
||||
* Uncommitted changes are stashed before build and popped afterwards.
|
||||
* Version files and manifests modified during build are reverted.
|
||||
* Untracked generated files are cleaned up.
|
||||
- Use the -Clean parameter to clean build outputs (bin/obj) and ignored files.
|
||||
- This script will clean previous outputs under the build directories and installer directory (except *.exe files).
|
||||
- First time run need admin permission to trust the certificate.
|
||||
- The built installer will be placed under: installer/PowerToysSetupVNext/[Platform]/[Configuration]/User[Machine]Setup
|
||||
relative to the solution root directory.
|
||||
- To run the full installation in other machines, call "./cert-management.ps1" to export the cert used to sign the packages.
|
||||
@@ -40,29 +45,11 @@ Runs the pipeline for x64 Release with machine-wide installer.
|
||||
#>
|
||||
|
||||
param (
|
||||
[string]$Platform = 'x64',
|
||||
[string]$Platform = '',
|
||||
[string]$Configuration = 'Release',
|
||||
[string]$PerUser = 'true',
|
||||
[string]$Version,
|
||||
[switch]$EnableCmdPalAOT,
|
||||
[switch]$Clean,
|
||||
[switch]$SkipBuild,
|
||||
[switch]$Help
|
||||
[string]$PerUser = 'true'
|
||||
)
|
||||
|
||||
if ($Help) {
|
||||
Write-Host "Usage: .\build-installer.ps1 [-Platform <x64|arm64>] [-Configuration <Release|Debug>] [-PerUser <true|false>] [-Version <0.0.1>] [-EnableCmdPalAOT] [-Clean] [-SkipBuild]"
|
||||
Write-Host " -Platform Target platform (default: auto-detect or x64)"
|
||||
Write-Host " -Configuration Build configuration (default: Release)"
|
||||
Write-Host " -PerUser Build per-user installer (default: true)"
|
||||
Write-Host " -Version Sets the PowerToys version (default: from src\Version.props)"
|
||||
Write-Host " -EnableCmdPalAOT Enable AOT compilation for CmdPal (slower build)"
|
||||
Write-Host " -Clean Clean output directories before building"
|
||||
Write-Host " -SkipBuild Skip building the main solution and tools (assumes they are already built)"
|
||||
Write-Host " -Help Show this help message"
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Ensure helpers are available
|
||||
. "$PSScriptRoot\build-common.ps1"
|
||||
|
||||
@@ -102,310 +89,62 @@ if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot "PowerToys.slnx")))
|
||||
}
|
||||
|
||||
Write-Host "PowerToys repository root detected: $repoRoot"
|
||||
# WiX v5 projects use WixToolset.Sdk via NuGet/MSBuild; no separate WiX installation is required.
|
||||
Write-Host ("[PIPELINE] Start | Platform={0} Configuration={1} PerUser={2}" -f $Platform, $Configuration, $PerUser)
|
||||
Write-Host ''
|
||||
|
||||
$cmdpalOutputPath = Join-Path $repoRoot "$Platform\$Configuration\WinUI3Apps\CmdPal"
|
||||
$buildOutputPath = Join-Path $repoRoot "$Platform\$Configuration"
|
||||
|
||||
# Clean should be done first before any other steps
|
||||
if ($Clean) {
|
||||
if (Test-Path $cmdpalOutputPath) {
|
||||
Write-Host "[CLEAN] Removing previous output: $cmdpalOutputPath"
|
||||
Remove-Item $cmdpalOutputPath -Recurse -Force -ErrorAction Ignore
|
||||
}
|
||||
if (Test-Path $buildOutputPath) {
|
||||
Write-Host "[CLEAN] Removing previous build output: $buildOutputPath"
|
||||
Remove-Item $buildOutputPath -Recurse -Force -ErrorAction Ignore
|
||||
}
|
||||
|
||||
Write-Host "[CLEAN] Cleaning all build artifacts (git clean -Xfd)..."
|
||||
Push-Location $repoRoot
|
||||
try {
|
||||
git clean -Xfd | Out-Null
|
||||
} catch {
|
||||
Write-Warning "[CLEAN] git clean failed: $_"
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
|
||||
Write-Host "[CLEAN] Cleaning solution (msbuild /t:Clean)..."
|
||||
RunMSBuild 'PowerToys.slnx' '/t:Clean' $Platform $Configuration
|
||||
if (Test-Path $cmdpalOutputPath) {
|
||||
Write-Host "[CLEAN] Removing previous output: $cmdpalOutputPath"
|
||||
Remove-Item $cmdpalOutputPath -Recurse -Force -ErrorAction Ignore
|
||||
}
|
||||
|
||||
# Git Stash Logic to handle workspace cleanup
|
||||
$stashedChanges = $false
|
||||
$scriptPathRelative = "tools/build/build-installer.ps1"
|
||||
$commonArgs = '/p:CIBuild=true'
|
||||
# No local projects found (or continuing) - build full solution and tools
|
||||
RestoreThenBuild 'PowerToys.slnx' $commonArgs $Platform $Configuration
|
||||
|
||||
# Calculate relative path of this script to exclude it from stash/reset
|
||||
$currentScriptPath = $MyInvocation.MyCommand.Definition
|
||||
if ($currentScriptPath.StartsWith($repoRoot)) {
|
||||
$scriptPathRelative = $currentScriptPath.Substring($repoRoot.Length).TrimStart('\', '/')
|
||||
$scriptPathRelative = $scriptPathRelative -replace '\\', '/'
|
||||
$msixSearchRoot = Join-Path $repoRoot "$Platform\$Configuration"
|
||||
$msixFiles = Get-ChildItem -Path $msixSearchRoot -Recurse -Filter *.msix |
|
||||
Select-Object -ExpandProperty FullName
|
||||
|
||||
if ($msixFiles.Count) {
|
||||
Write-Host ("[SIGN] .msix file(s): {0}" -f ($msixFiles -join '; '))
|
||||
& (Join-Path $PSScriptRoot "cert-sign-package.ps1") -TargetPaths $msixFiles
|
||||
}
|
||||
else {
|
||||
Write-Warning "[SIGN] No .msix files found in $msixSearchRoot"
|
||||
}
|
||||
|
||||
# Generate DSC manifest files
|
||||
Write-Host '[DSC] Generating DSC manifest files...'
|
||||
$dscScriptPath = Join-Path $repoRoot '.\tools\build\generate-dsc-manifests.ps1'
|
||||
if (Test-Path $dscScriptPath) {
|
||||
& $dscScriptPath -BuildPlatform $Platform -BuildConfiguration $Configuration -RepoRoot $repoRoot
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "DSC manifest generation failed with exit code $LASTEXITCODE"
|
||||
exit 1
|
||||
}
|
||||
Write-Host '[DSC] DSC manifest files generated successfully'
|
||||
} else {
|
||||
Write-Warning "[DSC] DSC manifest generator script not found at: $dscScriptPath"
|
||||
}
|
||||
|
||||
RestoreThenBuild 'tools\BugReportTool\BugReportTool.sln' $commonArgs $Platform $Configuration
|
||||
RestoreThenBuild 'tools\StylesReportTool\StylesReportTool.sln' $commonArgs $Platform $Configuration
|
||||
|
||||
Write-Host '[CLEAN] installer (keep *.exe)'
|
||||
Push-Location $repoRoot
|
||||
try {
|
||||
$gitStatus = git status --porcelain
|
||||
if ($gitStatus.Length -gt 0) {
|
||||
Write-Host "[GIT] Uncommitted changes detected. Stashing (excluding this script)..."
|
||||
$stashCountBefore = (git stash list).Count
|
||||
|
||||
# Exclude the current script from stash so we don't revert it while running
|
||||
git stash push --include-untracked -m "PowerToys Build Auto-Stash" -- . ":(exclude)$scriptPathRelative"
|
||||
|
||||
$stashCountAfter = (git stash list).Count
|
||||
if ($stashCountAfter -gt $stashCountBefore) {
|
||||
$stashedChanges = $true
|
||||
Write-Host "[GIT] Changes stashed."
|
||||
} else {
|
||||
Write-Host "[GIT] No changes to stash (likely only this script is modified)."
|
||||
}
|
||||
}
|
||||
git clean -xfd -e '*.exe' -- .\installer\ | Out-Null
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
|
||||
try {
|
||||
if ($Version) {
|
||||
Write-Host "[VERSION] Setting PowerToys version to $Version using versionSetting.ps1..."
|
||||
$versionScript = Join-Path $repoRoot ".pipelines\versionSetting.ps1"
|
||||
if (Test-Path $versionScript) {
|
||||
& $versionScript -versionNumber $Version -DevEnvironment 'Local'
|
||||
if (-not $?) {
|
||||
Write-Error "versionSetting.ps1 failed"
|
||||
exit 1
|
||||
}
|
||||
} else {
|
||||
Write-Error "Could not find versionSetting.ps1 at: $versionScript"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
RunMSBuild 'installer\PowerToysSetup.slnx' "$commonArgs /t:restore /p:RestorePackagesConfig=true" $Platform $Configuration
|
||||
|
||||
Write-Host "[VERSION] Setting up versioning using Microsoft.Windows.Terminal.Versioning..."
|
||||
RunMSBuild 'installer\PowerToysSetup.slnx' "$commonArgs /m /t:PowerToysInstallerVNext /p:PerUser=$PerUser" $Platform $Configuration
|
||||
|
||||
# Check for nuget.exe - download to AppData if not available
|
||||
$nugetDownloaded = $false
|
||||
$nugetPath = $null
|
||||
if (-not (Get-Command nuget -ErrorAction SilentlyContinue)) {
|
||||
Write-Warning "nuget.exe not found in PATH. Attempting to download..."
|
||||
$nugetUrl = "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe"
|
||||
$nugetDir = Join-Path $env:LOCALAPPDATA "PowerToys\BuildTools"
|
||||
if (-not (Test-Path $nugetDir)) { New-Item -ItemType Directory -Path $nugetDir -Force | Out-Null }
|
||||
$nugetPath = Join-Path $nugetDir "nuget.exe"
|
||||
if (-not (Test-Path $nugetPath)) {
|
||||
try {
|
||||
Invoke-WebRequest $nugetUrl -OutFile $nugetPath
|
||||
$nugetDownloaded = $true
|
||||
} catch {
|
||||
Write-Error "Failed to download nuget.exe. Please install it manually and add to PATH."
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
$env:Path += ";$nugetDir"
|
||||
}
|
||||
|
||||
# Install Terminal versioning package to AppData
|
||||
$versioningDir = Join-Path $env:LOCALAPPDATA "PowerToys\BuildTools\.versioning"
|
||||
if (-not (Test-Path $versioningDir)) { New-Item -ItemType Directory -Path $versioningDir -Force | Out-Null }
|
||||
|
||||
$configFile = Join-Path $repoRoot ".pipelines\release-nuget.config"
|
||||
|
||||
# Install the package
|
||||
# Use -ExcludeVersion to make the path predictable
|
||||
nuget install Microsoft.Windows.Terminal.Versioning -ConfigFile $configFile -OutputDirectory $versioningDir -ExcludeVersion -NonInteractive
|
||||
|
||||
$versionRoot = Join-Path $versioningDir "Microsoft.Windows.Terminal.Versioning"
|
||||
$setupScript = Join-Path $versionRoot "build\Setup.ps1"
|
||||
|
||||
if (Test-Path $setupScript) {
|
||||
& $setupScript -ProjectDirectory (Join-Path $repoRoot "src\modules\cmdpal") -Verbose
|
||||
} else {
|
||||
Write-Error "Could not find Setup.ps1 in $versionRoot"
|
||||
}
|
||||
|
||||
# WiX v5 projects use WixToolset.Sdk via NuGet/MSBuild; no separate WiX installation is required.
|
||||
Write-Host ("[PIPELINE] Start | Platform={0} Configuration={1} PerUser={2}" -f $Platform, $Configuration, $PerUser)
|
||||
Write-Host ''
|
||||
|
||||
$commonArgs = '/p:CIBuild=true /p:IsPipeline=true'
|
||||
|
||||
if ($EnableCmdPalAOT) {
|
||||
$commonArgs += " /p:EnableCmdPalAOT=true"
|
||||
}
|
||||
|
||||
# No local projects found (or continuing) - build full solution and tools
|
||||
if (-not $SkipBuild) {
|
||||
RestoreThenBuild 'PowerToys.slnx' $commonArgs $Platform $Configuration
|
||||
}
|
||||
|
||||
$msixSearchRoot = Join-Path $repoRoot "$Platform\$Configuration"
|
||||
$msixFiles = Get-ChildItem -Path $msixSearchRoot -Recurse -Filter *.msix |
|
||||
Select-Object -ExpandProperty FullName
|
||||
|
||||
if ($msixFiles.Count) {
|
||||
Write-Host ("[SIGN] .msix file(s): {0}" -f ($msixFiles -join '; '))
|
||||
& (Join-Path $PSScriptRoot "cert-sign-package.ps1") -TargetPaths $msixFiles
|
||||
}
|
||||
else {
|
||||
Write-Warning "[SIGN] No .msix files found in $msixSearchRoot"
|
||||
}
|
||||
|
||||
# Generate DSC v2 manifests (PowerToys.Settings.DSC.Schema.Generator)
|
||||
# The csproj PostBuild event is skipped on ARM64, so we run it manually here if needed.
|
||||
if ($Platform -eq 'arm64') {
|
||||
Write-Host "[DSC] Manually generating DSC v2 manifests for ARM64..."
|
||||
|
||||
# 1. Get Version
|
||||
$versionPropsPath = Join-Path $repoRoot "src\Version.props"
|
||||
[xml]$versionProps = Get-Content $versionPropsPath
|
||||
$ptVersion = $versionProps.Project.PropertyGroup.Version
|
||||
# Directory.Build.props appends .0 to the version for .csproj files
|
||||
$ptVersionFull = "$ptVersion.0"
|
||||
|
||||
# 2. Build the Generator
|
||||
$generatorProj = Join-Path $repoRoot "src\dsc\PowerToys.Settings.DSC.Schema.Generator\PowerToys.Settings.DSC.Schema.Generator.csproj"
|
||||
RunMSBuild $generatorProj "/t:Build" $Platform $Configuration
|
||||
|
||||
# 3. Define paths
|
||||
# The generator output path is in the project's bin folder
|
||||
$generatorExe = Join-Path $repoRoot "src\dsc\PowerToys.Settings.DSC.Schema.Generator\bin\$Platform\$Configuration\PowerToys.Settings.DSC.Schema.Generator.exe"
|
||||
|
||||
if (-not (Test-Path $generatorExe)) {
|
||||
Write-Warning "Could not find generator at expected path: $generatorExe"
|
||||
Write-Warning "Searching in build output..."
|
||||
$found = Get-ChildItem -Path (Join-Path $repoRoot "$Platform\$Configuration") -Filter "PowerToys.Settings.DSC.Schema.Generator.exe" -Recurse | Select-Object -First 1
|
||||
if ($found) {
|
||||
$generatorExe = $found.FullName
|
||||
}
|
||||
}
|
||||
|
||||
$settingsLibDll = Join-Path $repoRoot "$Platform\$Configuration\WinUI3Apps\PowerToys.Settings.UI.Lib.dll"
|
||||
|
||||
$dscGenDir = Join-Path $repoRoot "src\dsc\Microsoft.PowerToys.Configure\Generated\Microsoft.PowerToys.Configure\$ptVersionFull"
|
||||
if (-not (Test-Path $dscGenDir)) {
|
||||
New-Item -ItemType Directory -Path $dscGenDir -Force | Out-Null
|
||||
}
|
||||
|
||||
$outPsm1 = Join-Path $dscGenDir "Microsoft.PowerToys.Configure.psm1"
|
||||
$outPsd1 = Join-Path $dscGenDir "Microsoft.PowerToys.Configure.psd1"
|
||||
|
||||
# 4. Run Generator
|
||||
if (Test-Path $generatorExe) {
|
||||
Write-Host "[DSC] Executing: $generatorExe"
|
||||
|
||||
$generatorDir = Split-Path -Parent $generatorExe
|
||||
$winUI3AppsDir = Join-Path $repoRoot "$Platform\$Configuration\WinUI3Apps"
|
||||
|
||||
# Copy dependencies from WinUI3Apps to Generator directory to satisfy WinRT/WinUI3 dependencies
|
||||
# This avoids "Class not registered" errors without polluting the WinUI3Apps directory which is used for packaging.
|
||||
if (Test-Path $winUI3AppsDir) {
|
||||
Write-Host "[DSC] Copying dependencies from $winUI3AppsDir to $generatorDir"
|
||||
Get-ChildItem -Path $winUI3AppsDir -Filter "*.dll" | ForEach-Object {
|
||||
$destPath = Join-Path $generatorDir $_.Name
|
||||
if (-not (Test-Path $destPath)) {
|
||||
Copy-Item -Path $_.FullName -Destination $destPath -Force
|
||||
}
|
||||
}
|
||||
# Also copy resources.pri if it exists, as it might be needed for resource lookup
|
||||
$priFile = Join-Path $winUI3AppsDir "resources.pri"
|
||||
if (Test-Path $priFile) {
|
||||
Copy-Item -Path $priFile -Destination $generatorDir -Force
|
||||
}
|
||||
}
|
||||
|
||||
Push-Location $generatorDir
|
||||
try {
|
||||
# Now we can use the local DLLs
|
||||
$localSettingsLibDll = Join-Path $generatorDir "PowerToys.Settings.UI.Lib.dll"
|
||||
|
||||
if (Test-Path $localSettingsLibDll) {
|
||||
Write-Host "[DSC] Using local DLL: $localSettingsLibDll"
|
||||
& $generatorExe $localSettingsLibDll $outPsm1 $outPsd1
|
||||
} else {
|
||||
# Fallback (shouldn't happen if copy succeeded or build was correct)
|
||||
Write-Warning "[DSC] Local DLL not found, falling back to: $settingsLibDll"
|
||||
& $generatorExe $settingsLibDll $outPsm1 $outPsd1
|
||||
}
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "DSC v2 generation failed with exit code $LASTEXITCODE"
|
||||
exit 1
|
||||
}
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
|
||||
Write-Host "[DSC] DSC v2 manifests generated successfully."
|
||||
} else {
|
||||
Write-Error "Could not find generator executable at $generatorExe"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Generate DSC manifest files
|
||||
Write-Host '[DSC] Generating DSC manifest files...'
|
||||
$dscScriptPath = Join-Path $repoRoot '.\tools\build\generate-dsc-manifests.ps1'
|
||||
if (Test-Path $dscScriptPath) {
|
||||
& $dscScriptPath -BuildPlatform $Platform -BuildConfiguration $Configuration -RepoRoot $repoRoot
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "DSC manifest generation failed with exit code $LASTEXITCODE"
|
||||
exit 1
|
||||
}
|
||||
Write-Host '[DSC] DSC manifest files generated successfully'
|
||||
} else {
|
||||
Write-Warning "[DSC] DSC manifest generator script not found at: $dscScriptPath"
|
||||
}
|
||||
|
||||
if (-not $SkipBuild) {
|
||||
RestoreThenBuild 'tools\BugReportTool\BugReportTool.sln' $commonArgs $Platform $Configuration
|
||||
RestoreThenBuild 'tools\StylesReportTool\StylesReportTool.sln' $commonArgs $Platform $Configuration
|
||||
}
|
||||
|
||||
# Set NUGET_PACKAGES environment variable if not set, to help wixproj find heat.exe
|
||||
if (-not $env:NUGET_PACKAGES) {
|
||||
$env:NUGET_PACKAGES = Join-Path $env:USERPROFILE ".nuget\packages"
|
||||
Write-Host "[ENV] Set NUGET_PACKAGES to $env:NUGET_PACKAGES"
|
||||
}
|
||||
|
||||
RunMSBuild 'installer\PowerToysSetup.slnx' "$commonArgs /t:restore /p:RestorePackagesConfig=true" $Platform $Configuration
|
||||
|
||||
RunMSBuild 'installer\PowerToysSetup.slnx' "$commonArgs /m /t:PowerToysInstallerVNext /p:PerUser=$PerUser" $Platform $Configuration
|
||||
|
||||
# Fix: WiX v5 locally puts the MSI in an 'en-us' subfolder, but the Bootstrapper expects it in the root of UserSetup/MachineSetup.
|
||||
# We move it up one level to match expectations.
|
||||
$setupType = if ($PerUser -eq 'true') { 'UserSetup' } else { 'MachineSetup' }
|
||||
$msiParentDir = Join-Path $repoRoot "installer\PowerToysSetupVNext\$Platform\$Configuration\$setupType"
|
||||
$msiEnUsDir = Join-Path $msiParentDir "en-us"
|
||||
|
||||
if (Test-Path $msiEnUsDir) {
|
||||
Write-Host "[FIX] Moving MSI files from $msiEnUsDir to $msiParentDir"
|
||||
Get-ChildItem -Path $msiEnUsDir -Filter *.msi | Move-Item -Destination $msiParentDir -Force
|
||||
}
|
||||
|
||||
RunMSBuild 'installer\PowerToysSetup.slnx' "$commonArgs /m /t:PowerToysBootstrapperVNext /p:PerUser=$PerUser" $Platform $Configuration
|
||||
|
||||
} finally {
|
||||
# Restore workspace state using Git
|
||||
Write-Host "[GIT] Cleaning up build artifacts..."
|
||||
Push-Location $repoRoot
|
||||
try {
|
||||
# Revert all changes EXCEPT the script itself
|
||||
# This cleans up Version.props, AppxManifests, etc.
|
||||
git checkout HEAD -- . ":(exclude)$scriptPathRelative"
|
||||
|
||||
# Remove untracked files (generated manifests, etc.)
|
||||
# -f: force, -d: remove directories, -q: quiet
|
||||
git clean -fd -q
|
||||
|
||||
if ($stashedChanges) {
|
||||
Write-Host "[GIT] Restoring stashed changes..."
|
||||
git stash pop --index
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Warning "[GIT] 'git stash pop' reported conflicts or errors. Your changes are in the stash list."
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
}
|
||||
RunMSBuild 'installer\PowerToysSetup.slnx' "$commonArgs /m /t:PowerToysBootstrapperVNext /p:PerUser=$PerUser" $Platform $Configuration
|
||||
|
||||
Write-Host '[PIPELINE] Completed'
|
||||
|
||||
@@ -61,18 +61,8 @@ function ImportAndVerifyCertificate {
|
||||
try {
|
||||
$null = Import-Certificate -FilePath $cerPath -CertStoreLocation $storePath -ErrorAction Stop
|
||||
} catch {
|
||||
if ($_.Exception.Message -match "Access is denied" -or $_.Exception.InnerException.Message -match "Access is denied") {
|
||||
Write-Warning "Access denied to $storePath. Attempting to import with admin privileges..."
|
||||
try {
|
||||
Start-Process powershell -ArgumentList "-NoProfile", "-Command", "& { Import-Certificate -FilePath '$cerPath' -CertStoreLocation '$storePath' }" -Verb RunAs -Wait
|
||||
} catch {
|
||||
Write-Warning "Failed to request admin privileges: $_"
|
||||
return $false
|
||||
}
|
||||
} else {
|
||||
Write-Warning "Failed to import certificate to $storePath : $_"
|
||||
return $false
|
||||
}
|
||||
Write-Warning "Failed to import certificate to $storePath : $_"
|
||||
return $false
|
||||
}
|
||||
|
||||
$imported = Get-ChildItem -Path $storePath | Where-Object { $_.Thumbprint -eq $thumbprint }
|
||||
|
||||
Reference in New Issue
Block a user