Compare commits

..

6 Commits

Author SHA1 Message Date
Jaylyn Barbee
a557182383 adding onnx dll to signing list 2025-12-19 08:57:37 -05:00
Jaylyn Barbee
27ec2fc6d1 Allowing dllmain to register and unregister the telemetry service 2025-12-19 08:50:52 -05:00
Jaylyn Barbee
eef5a3423a updated telemtry 2025-12-18 11:14:27 -05:00
Jaylyn Barbee
ad0d5e7f49 fixed typo in data and priv chart 2025-12-12 10:30:31 -05:00
Jaylyn Barbee
702e223360 Removed unneeded files 2025-12-12 10:24:50 -05:00
Jaylyn Barbee
c077cc46f1 Adding telemetry events for Light Switch 2025-12-12 10:21:47 -05:00
120 changed files with 975 additions and 7760 deletions

View File

@@ -335,7 +335,3 @@ azp
feedbackhub
needinfo
reportbug
#ffmpeg
crf
nostdin

View File

@@ -111,7 +111,6 @@ AUTORADIOBUTTON
Autorun
AUTOTICKS
AUTOUPDATE
autopf
AValid
AWAYMODE
azcliversion
@@ -268,8 +267,6 @@ CONFIGW
CONFLICTINGMODIFIERKEY
CONFLICTINGMODIFIERSHORTCUT
CONOUT
Contoso
coreclr
constexpr
contentdialog
contentfiles
@@ -754,7 +751,6 @@ IFACEMETHOD
IFACEMETHODIMP
ifd
IGNOREUNKNOWN
ignoreversion
IGo
iid
IIM
@@ -781,7 +777,6 @@ INITDIALOG
INITGUID
INITTOLOGFONTSTRUCT
INLINEPREFIX
Inno
inlines
Inno
INPC
@@ -816,7 +811,6 @@ IPTC
irow
irprops
isbi
iscc
isfinite
iss
issecret
@@ -1449,7 +1443,6 @@ RECTDESTINATION
rectp
RECTSOURCE
recyclebin
recursesubdirs
Redist
Reencode
REFCLSID
@@ -1801,7 +1794,6 @@ TILEDWINDOW
TILLSON
timedate
timediff
timestamped
timeunion
timeutil
TITLEBARINFO
@@ -1862,8 +1854,6 @@ uitests
UITo
ULONGLONG
ums
UMax
UMin
uncompilable
UNCPRIORITY
UNDNAME

View File

@@ -1,7 +1,7 @@
Param(
# Using the default value of 1.7 for winAppSdkVersionNumber and useExperimentalVersion as false
[Parameter(Mandatory=$False,Position=1)]
[string]$winAppSdkVersionNumber = "1.8",
[string]$winAppSdkVersionNumber = "1.7",
# When the pipeline calls the PS1 file, the passed parameters are converted to string type
[Parameter(Mandatory=$False,Position=2)]
@@ -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 $installDir -Patterns $allLocalPackages
$xml.Save($nugetConfig)
Write-Host "Updated nuget.config with localpackages mapping."
} catch {
Write-Warning "Failed to resolve dependencies: $_"
} finally {
Remove-Item $tempConfig -Force -ErrorAction SilentlyContinue
}
}
# Execute nuget list and capture the output
if ($useExperimentalVersion) {
# The nuget list for experimental versions will cost more time
@@ -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

View File

@@ -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

View File

@@ -19,20 +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'
workingDirectory: '$(build.sourcesdirectory)'
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

View File

@@ -681,6 +681,30 @@ _If you want to find diagnostic data events in the source code, these two links
</tr>
</table>
### Light Switch
<table style="width:100%">
<tr>
<th>Event Name</th>
<th>Description</th>
</tr>
<tr>
<td>Microsoft.PowerToys.LightSwitch_EnableLightSwitch</td>
<td>Triggered when Light Switch is enabled or disabled.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.LightSwitch_ShortcutInvoked</td>
<td>Occurs when the shortcut for Light Switch is invoked.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.LightSwitch_ScheduleModeToggled</td>
<td>Occurs when a new schedule mode is selected for Light Switch.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.LightSwitch_ThemeTargetChanged</td>
<td>Occurs when the options for targeting the system or apps is updated.</td>
</tr>
</table>
### Mouse Highlighter
<table style="width:100%">
<tr>

View File

@@ -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>

View File

@@ -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 &gt;= 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 -->

View File

@@ -1,399 +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.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.PowerToys.UITest
{
/// <summary>
/// Provides methods for recording the screen during UI tests.
/// Requires FFmpeg to be installed and available in PATH.
/// </summary>
internal class ScreenRecording : IDisposable
{
private readonly string outputDirectory;
private readonly string framesDirectory;
private readonly string outputFilePath;
private readonly List<string> capturedFrames;
private readonly SemaphoreSlim recordingLock = new(1, 1);
private readonly Stopwatch recordingStopwatch = new();
private readonly string? ffmpegPath;
private CancellationTokenSource? recordingCancellation;
private Task? recordingTask;
private bool isRecording;
private int frameCount;
[DllImport("user32.dll")]
private static extern IntPtr GetDC(IntPtr hWnd);
[DllImport("gdi32.dll")]
private static extern int GetDeviceCaps(IntPtr hdc, int nIndex);
[DllImport("user32.dll")]
private static extern IntPtr ReleaseDC(IntPtr hWnd, IntPtr hDC);
[DllImport("user32.dll")]
private static extern bool GetCursorInfo(out ScreenCapture.CURSORINFO pci);
[DllImport("user32.dll")]
private static extern bool DrawIconEx(IntPtr hdc, int x, int y, IntPtr hIcon, int cx, int cy, int istepIfAniCur, IntPtr hbrFlickerFreeDraw, int diFlags);
private const int CURSORSHOWING = 0x00000001;
private const int DESKTOPHORZRES = 118;
private const int DESKTOPVERTRES = 117;
private const int DINORMAL = 0x0003;
private const int TargetFps = 15; // 15 FPS for good balance of quality and size
/// <summary>
/// Initializes a new instance of the <see cref="ScreenRecording"/> class.
/// </summary>
/// <param name="outputDirectory">Directory where the recording will be saved.</param>
public ScreenRecording(string outputDirectory)
{
this.outputDirectory = outputDirectory;
string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
framesDirectory = Path.Combine(outputDirectory, $"frames_{timestamp}");
outputFilePath = Path.Combine(outputDirectory, $"recording_{timestamp}.mp4");
capturedFrames = new List<string>();
frameCount = 0;
// Check if FFmpeg is available
ffmpegPath = FindFfmpeg();
if (ffmpegPath == null)
{
Console.WriteLine("FFmpeg not found. Screen recording will be disabled.");
Console.WriteLine("To enable video recording, install FFmpeg: https://ffmpeg.org/download.html");
}
}
/// <summary>
/// Gets a value indicating whether screen recording is available (FFmpeg found).
/// </summary>
public bool IsAvailable => ffmpegPath != null;
/// <summary>
/// Starts recording the screen.
/// </summary>
/// <returns>A task representing the asynchronous operation.</returns>
public async Task StartRecordingAsync()
{
await recordingLock.WaitAsync();
try
{
if (isRecording || !IsAvailable)
{
return;
}
// Create frames directory
Directory.CreateDirectory(framesDirectory);
recordingCancellation = new CancellationTokenSource();
isRecording = true;
recordingStopwatch.Start();
// Start the recording task
recordingTask = Task.Run(() => RecordFrames(recordingCancellation.Token));
Console.WriteLine($"Started screen recording at {TargetFps} FPS");
}
catch (Exception ex)
{
Console.WriteLine($"Failed to start recording: {ex.Message}");
isRecording = false;
}
finally
{
recordingLock.Release();
}
}
/// <summary>
/// Stops recording and encodes video.
/// </summary>
/// <returns>A task representing the asynchronous operation.</returns>
public async Task StopRecordingAsync()
{
await recordingLock.WaitAsync();
try
{
if (!isRecording || recordingCancellation == null)
{
return;
}
// Signal cancellation
recordingCancellation.Cancel();
// Wait for recording task to complete
if (recordingTask != null)
{
await recordingTask;
}
recordingStopwatch.Stop();
isRecording = false;
double duration = recordingStopwatch.Elapsed.TotalSeconds;
Console.WriteLine($"Recording stopped. Captured {capturedFrames.Count} frames in {duration:F2} seconds");
// Encode to video
await EncodeToVideoAsync();
}
catch (Exception ex)
{
Console.WriteLine($"Error stopping recording: {ex.Message}");
}
finally
{
Cleanup();
recordingLock.Release();
}
}
/// <summary>
/// Records frames from the screen.
/// </summary>
private void RecordFrames(CancellationToken cancellationToken)
{
try
{
int frameInterval = 1000 / TargetFps;
var frameTimer = Stopwatch.StartNew();
while (!cancellationToken.IsCancellationRequested)
{
var frameStart = frameTimer.ElapsedMilliseconds;
try
{
CaptureFrame();
}
catch (Exception ex)
{
Console.WriteLine($"Error capturing frame: {ex.Message}");
}
// Sleep for remaining time to maintain target FPS
var frameTime = frameTimer.ElapsedMilliseconds - frameStart;
var sleepTime = Math.Max(0, frameInterval - (int)frameTime);
if (sleepTime > 0)
{
Thread.Sleep(sleepTime);
}
}
}
catch (OperationCanceledException)
{
// Expected when stopping
}
catch (Exception ex)
{
Console.WriteLine($"Error during recording: {ex.Message}");
}
}
/// <summary>
/// Captures a single frame.
/// </summary>
private void CaptureFrame()
{
IntPtr hdc = GetDC(IntPtr.Zero);
int screenWidth = GetDeviceCaps(hdc, DESKTOPHORZRES);
int screenHeight = GetDeviceCaps(hdc, DESKTOPVERTRES);
ReleaseDC(IntPtr.Zero, hdc);
Rectangle bounds = new Rectangle(0, 0, screenWidth, screenHeight);
using (Bitmap bitmap = new Bitmap(bounds.Width, bounds.Height, PixelFormat.Format24bppRgb))
{
using (Graphics g = Graphics.FromImage(bitmap))
{
g.CopyFromScreen(bounds.Location, Point.Empty, bounds.Size);
ScreenCapture.CURSORINFO cursorInfo;
cursorInfo.CbSize = Marshal.SizeOf<ScreenCapture.CURSORINFO>();
if (GetCursorInfo(out cursorInfo) && cursorInfo.Flags == CURSORSHOWING)
{
IntPtr hdcDest = g.GetHdc();
DrawIconEx(hdcDest, cursorInfo.PTScreenPos.X, cursorInfo.PTScreenPos.Y, cursorInfo.HCursor, 0, 0, 0, IntPtr.Zero, DINORMAL);
g.ReleaseHdc(hdcDest);
}
}
string framePath = Path.Combine(framesDirectory, $"frame_{frameCount:D6}.jpg");
bitmap.Save(framePath, ImageFormat.Jpeg);
capturedFrames.Add(framePath);
frameCount++;
}
}
/// <summary>
/// Encodes captured frames to video using ffmpeg.
/// </summary>
private async Task EncodeToVideoAsync()
{
if (capturedFrames.Count == 0)
{
Console.WriteLine("No frames captured");
return;
}
try
{
// Build ffmpeg command with proper non-interactive flags
string inputPattern = Path.Combine(framesDirectory, "frame_%06d.jpg");
// -y: overwrite without asking
// -nostdin: disable interaction
// -loglevel error: only show errors
// -stats: show encoding progress
string args = $"-y -nostdin -loglevel error -stats -framerate {TargetFps} -i \"{inputPattern}\" -c:v libx264 -pix_fmt yuv420p -crf 23 \"{outputFilePath}\"";
Console.WriteLine($"Encoding {capturedFrames.Count} frames to video...");
var startInfo = new ProcessStartInfo
{
FileName = ffmpegPath!,
Arguments = args,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
RedirectStandardInput = true, // Important: redirect stdin to prevent hanging
CreateNoWindow = true,
};
using var process = Process.Start(startInfo);
if (process != null)
{
// Close stdin immediately to ensure FFmpeg doesn't wait for input
process.StandardInput.Close();
// Read output streams asynchronously to prevent deadlock
var outputTask = process.StandardOutput.ReadToEndAsync();
var errorTask = process.StandardError.ReadToEndAsync();
// Wait for process to exit
await process.WaitForExitAsync();
// Get the output
string stdout = await outputTask;
string stderr = await errorTask;
if (process.ExitCode == 0 && File.Exists(outputFilePath))
{
var fileInfo = new FileInfo(outputFilePath);
Console.WriteLine($"Video created: {outputFilePath} ({fileInfo.Length / 1024 / 1024:F1} MB)");
}
else
{
Console.WriteLine($"FFmpeg encoding failed with exit code {process.ExitCode}");
if (!string.IsNullOrWhiteSpace(stderr))
{
Console.WriteLine($"FFmpeg error: {stderr}");
}
}
}
}
catch (Exception ex)
{
Console.WriteLine($"Error encoding video: {ex.Message}");
}
}
/// <summary>
/// Finds ffmpeg executable.
/// </summary>
private static string? FindFfmpeg()
{
// Check if ffmpeg is in PATH
var pathDirs = Environment.GetEnvironmentVariable("PATH")?.Split(Path.PathSeparator) ?? Array.Empty<string>();
foreach (var dir in pathDirs)
{
var ffmpegPath = Path.Combine(dir, "ffmpeg.exe");
if (File.Exists(ffmpegPath))
{
return ffmpegPath;
}
}
// Check common installation locations
var commonPaths = new[]
{
@"C:\.tools\ffmpeg\bin\ffmpeg.exe",
@"C:\ffmpeg\bin\ffmpeg.exe",
@"C:\Program Files\ffmpeg\bin\ffmpeg.exe",
@"C:\Program Files (x86)\ffmpeg\bin\ffmpeg.exe",
@$"{Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)}\Microsoft\WinGet\Links\ffmpeg.exe",
};
foreach (var path in commonPaths)
{
if (File.Exists(path))
{
return path;
}
}
return null;
}
/// <summary>
/// Gets the path to the recorded video file.
/// </summary>
public string OutputFilePath => outputFilePath;
/// <summary>
/// Gets the directory containing recordings.
/// </summary>
public string OutputDirectory => outputDirectory;
/// <summary>
/// Cleans up resources.
/// </summary>
private void Cleanup()
{
recordingCancellation?.Dispose();
recordingCancellation = null;
recordingTask = null;
// Clean up frames directory if it exists
try
{
if (Directory.Exists(framesDirectory))
{
Directory.Delete(framesDirectory, true);
}
}
catch (Exception ex)
{
Console.WriteLine($"Failed to cleanup frames directory: {ex.Message}");
}
}
/// <summary>
/// Disposes resources.
/// </summary>
public void Dispose()
{
if (isRecording)
{
StopRecordingAsync().GetAwaiter().GetResult();
}
Cleanup();
recordingLock.Dispose();
GC.SuppressFinalize(this);
}
}
}

View File

@@ -130,13 +130,9 @@ namespace Microsoft.PowerToys.UITest
/// </summary>
/// <param name="appPath">The path to the application executable.</param>
/// <param name="args">Optional command line arguments to pass to the application.</param>
public void StartExe(string appPath, string[]? args = null, string? enableModules = null)
public void StartExe(string appPath, string[]? args = null)
{
var opts = new AppiumOptions();
if (!string.IsNullOrEmpty(enableModules))
{
opts.AddAdditionalCapability("enableModules", enableModules);
}
if (scope == PowerToysModule.PowerToysSettings)
{
@@ -173,66 +169,27 @@ namespace Microsoft.PowerToys.UITest
private void TryLaunchPowerToysSettings(AppiumOptions opts)
{
if (opts.ToCapabilities().HasCapability("enableModules"))
try
{
var modulesString = (string)opts.ToCapabilities().GetCapability("enableModules");
var modulesArray = modulesString.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
SettingsConfigHelper.ConfigureGlobalModuleSettings(modulesArray);
}
else
{
SettingsConfigHelper.ConfigureGlobalModuleSettings();
}
const int maxTries = 3;
const int delayMs = 5000;
const int maxRetries = 3;
for (int tryCount = 1; tryCount <= maxTries; tryCount++)
{
try
var runnerProcessInfo = new ProcessStartInfo
{
var runnerProcessInfo = new ProcessStartInfo
{
FileName = locationPath + runnerPath,
Verb = "runas",
Arguments = "--open-settings",
};
FileName = locationPath + runnerPath,
Verb = "runas",
Arguments = "--open-settings",
};
ExitExe(runnerProcessInfo.FileName);
ExitExe(runnerProcessInfo.FileName);
runner = Process.Start(runnerProcessInfo);
// Verify process was killed
string exeName = Path.GetFileNameWithoutExtension(runnerProcessInfo.FileName);
var remainingProcesses = Process.GetProcessesByName(exeName);
WaitForWindowAndSetCapability(opts, "PowerToys Settings", 5000, 5);
runner = Process.Start(runnerProcessInfo);
if (WaitForWindowAndSetCapability(opts, "PowerToys Settings", delayMs, maxRetries))
{
// Exit CmdPal UI before launching new process if use installer for test
ExitExeByName("Microsoft.CmdPal.UI");
return;
}
// Window not found, kill all PowerToys processes and retry
if (tryCount < maxTries)
{
KillPowerToysProcesses();
}
}
catch (Exception ex)
{
if (tryCount == maxTries)
{
throw new InvalidOperationException($"Failed to launch PowerToys Settings after {maxTries} attempts: {ex.Message}", ex);
}
// Kill processes and retry
KillPowerToysProcesses();
}
// Exit CmdPal UI before launching new process if use installer for test
ExitExeByName("Microsoft.CmdPal.UI");
}
catch (Exception ex)
{
throw new InvalidOperationException($"Failed to launch PowerToys Settings: {ex.Message}", ex);
}
throw new InvalidOperationException($"Failed to launch PowerToys Settings: Window not found after {maxTries} attempts.");
}
private void TryLaunchCommandPalette(AppiumOptions opts)
@@ -254,10 +211,7 @@ namespace Microsoft.PowerToys.UITest
var process = Process.Start(processStartInfo);
process?.WaitForExit();
if (!WaitForWindowAndSetCapability(opts, "Command Palette", 5000, 10))
{
throw new TimeoutException("Failed to find Command Palette window after multiple attempts.");
}
WaitForWindowAndSetCapability(opts, "Command Palette", 5000, 10);
}
catch (Exception ex)
{
@@ -265,7 +219,7 @@ namespace Microsoft.PowerToys.UITest
}
}
private bool WaitForWindowAndSetCapability(AppiumOptions opts, string windowName, int delayMs, int maxRetries)
private void WaitForWindowAndSetCapability(AppiumOptions opts, string windowName, int delayMs, int maxRetries)
{
for (int attempt = 1; attempt <= maxRetries; attempt++)
{
@@ -276,16 +230,18 @@ namespace Microsoft.PowerToys.UITest
{
var hexHwnd = window[0].HWnd.ToString("x");
opts.AddAdditionalCapability("appTopLevelWindow", hexHwnd);
return true;
return;
}
if (attempt < maxRetries)
{
Thread.Sleep(delayMs);
}
else
{
throw new TimeoutException($"Failed to find {windowName} window after multiple attempts.");
}
}
return false;
}
/// <summary>
@@ -336,17 +292,17 @@ namespace Microsoft.PowerToys.UITest
catch (Exception ex)
{
// Handle exceptions if needed
Console.WriteLine($"Exception during Cleanup: {ex.Message}");
Debug.WriteLine($"Exception during Cleanup: {ex.Message}");
}
}
/// <summary>
/// Restarts now exe and takes control of it.
/// </summary>
public void RestartScopeExe(string? enableModules = null)
public void RestartScopeExe()
{
ExitScopeExe();
StartExe(locationPath + sessionPath, commandLineArgs, enableModules);
StartExe(locationPath + sessionPath, this.commandLineArgs);
}
public WindowsDriver<WindowsElement> GetRoot()
@@ -371,31 +327,5 @@ namespace Microsoft.PowerToys.UITest
this.ExitExe(winAppDriverProcessInfo.FileName);
SessionHelper.appDriver = Process.Start(winAppDriverProcessInfo);
}
private void KillPowerToysProcesses()
{
var powerToysProcessNames = new[] { "PowerToys", "Microsoft.CmdPal.UI" };
foreach (var processName in powerToysProcessNames)
{
try
{
var processes = Process.GetProcessesByName(processName);
foreach (var process in processes)
{
process.Kill();
process.WaitForExit();
}
// Verify processes are actually gone
var remainingProcesses = Process.GetProcessesByName(processName);
}
catch (Exception ex)
{
Console.WriteLine($"[KillPowerToysProcesses] Failed to kill process {processName}: {ex.Message}");
}
}
}
}
}

View File

@@ -26,13 +26,14 @@ namespace Microsoft.PowerToys.UITest
/// <summary>
/// Configures global PowerToys settings to enable only specified modules and disable all others.
/// </summary>
/// <param name="modulesToEnable">Array of module names to enable (e.g., "Peek", "FancyZones"). All other modules will be disabled. If null or empty, all modules will be disabled.</param>
/// <param name="modulesToEnable">Array of module names to enable (e.g., "Peek", "FancyZones"). All other modules will be disabled.</param>
/// <exception cref="ArgumentNullException">Thrown when modulesToEnable is null.</exception>
/// <exception cref="InvalidOperationException">Thrown when settings file operations fail.</exception>
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "This is test code and will not be trimmed")]
[UnconditionalSuppressMessage("AOT", "IL3050", Justification = "This is test code and will not be AOT compiled")]
public static void ConfigureGlobalModuleSettings(params string[]? modulesToEnable)
public static void ConfigureGlobalModuleSettings(params string[] modulesToEnable)
{
modulesToEnable ??= Array.Empty<string>();
ArgumentNullException.ThrowIfNull(modulesToEnable);
try
{

View File

@@ -29,8 +29,6 @@ namespace Microsoft.PowerToys.UITest
public string? ScreenshotDirectory { get; set; }
public string? RecordingDirectory { get; set; }
public static MonitorInfoData.ParamsWrapper MonitorInfoData { get; set; } = new MonitorInfoData.ParamsWrapper() { Monitors = new List<MonitorInfoData.MonitorInfoDataWrapper>() };
private readonly PowerToysModule scope;
@@ -38,7 +36,6 @@ namespace Microsoft.PowerToys.UITest
private readonly string[]? commandLineArgs;
private SessionHelper? sessionHelper;
private System.Threading.Timer? screenshotTimer;
private ScreenRecording? screenRecording;
public UITestBase(PowerToysModule scope = PowerToysModule.PowerToysSettings, WindowSize size = WindowSize.UnSpecified, string[]? commandLineArgs = null)
{
@@ -68,35 +65,12 @@ namespace Microsoft.PowerToys.UITest
CloseOtherApplications();
if (IsInPipeline)
{
string baseDirectory = this.TestContext.TestResultsDirectory ?? string.Empty;
ScreenshotDirectory = Path.Combine(baseDirectory, "UITestScreenshots_" + Guid.NewGuid().ToString());
ScreenshotDirectory = Path.Combine(this.TestContext.TestResultsDirectory ?? string.Empty, "UITestScreenshots_" + Guid.NewGuid().ToString());
Directory.CreateDirectory(ScreenshotDirectory);
RecordingDirectory = Path.Combine(baseDirectory, "UITestRecordings_" + Guid.NewGuid().ToString());
Directory.CreateDirectory(RecordingDirectory);
// Take screenshot every 1 second
screenshotTimer = new System.Threading.Timer(ScreenCapture.TimerCallback, ScreenshotDirectory, TimeSpan.Zero, TimeSpan.FromMilliseconds(1000));
// Start screen recording (requires FFmpeg)
try
{
screenRecording = new ScreenRecording(RecordingDirectory);
if (screenRecording.IsAvailable)
{
_ = screenRecording.StartRecordingAsync();
}
else
{
screenRecording = null;
}
}
catch (Exception ex)
{
Console.WriteLine($"Failed to start screen recording: {ex.Message}");
screenRecording = null;
}
// Escape Popups before starting
System.Windows.Forms.SendKeys.SendWait("{ESC}");
}
@@ -114,36 +88,15 @@ namespace Microsoft.PowerToys.UITest
if (IsInPipeline)
{
screenshotTimer?.Change(Timeout.Infinite, Timeout.Infinite);
// Stop screen recording
if (screenRecording != null)
{
try
{
screenRecording.StopRecordingAsync().GetAwaiter().GetResult();
}
catch (Exception ex)
{
Console.WriteLine($"Failed to stop screen recording: {ex.Message}");
}
}
Dispose();
if (TestContext.CurrentTestOutcome is UnitTestOutcome.Failed
or UnitTestOutcome.Error
or UnitTestOutcome.Unknown)
{
Task.Delay(1000).Wait();
AddScreenShotsToTestResultsDirectory();
AddRecordingsToTestResultsDirectory();
AddLogFilesToTestResultsDirectory();
}
else
{
// Clean up recording if test passed
CleanupRecordingDirectory();
}
Dispose();
}
this.Session.Cleanup();
@@ -153,7 +106,6 @@ namespace Microsoft.PowerToys.UITest
public void Dispose()
{
screenshotTimer?.Dispose();
screenRecording?.Dispose();
GC.SuppressFinalize(this);
}
@@ -648,47 +600,6 @@ namespace Microsoft.PowerToys.UITest
}
}
/// <summary>
/// Adds screen recordings to test results directory when test fails.
/// </summary>
protected void AddRecordingsToTestResultsDirectory()
{
if (RecordingDirectory != null && Directory.Exists(RecordingDirectory))
{
// Add video files (MP4)
var videoFiles = Directory.GetFiles(RecordingDirectory, "*.mp4");
foreach (string file in videoFiles)
{
this.TestContext.AddResultFile(file);
var fileInfo = new FileInfo(file);
Console.WriteLine($"Added video recording: {Path.GetFileName(file)} ({fileInfo.Length / 1024 / 1024:F1} MB)");
}
if (videoFiles.Length == 0)
{
Console.WriteLine("No video recording available (FFmpeg not found). Screenshots are still captured.");
}
}
}
/// <summary>
/// Cleans up recording directory when test passes.
/// </summary>
private void CleanupRecordingDirectory()
{
if (RecordingDirectory != null && Directory.Exists(RecordingDirectory))
{
try
{
Directory.Delete(RecordingDirectory, true);
}
catch (Exception ex)
{
Console.WriteLine($"Failed to cleanup recording directory: {ex.Message}");
}
}
}
/// <summary>
/// Copies PowerToys log files to test results directory when test fails.
/// Renames files to include the directory structure after \PowerToys.
@@ -778,11 +689,11 @@ namespace Microsoft.PowerToys.UITest
/// <summary>
/// Restart scope exe.
/// </summary>
public Session RestartScopeExe(string? enableModules = null)
public void RestartScopeExe()
{
this.sessionHelper!.RestartScopeExe(enableModules);
this.sessionHelper!.RestartScopeExe();
this.Session = new Session(this.sessionHelper.GetRoot(), this.sessionHelper.GetDriver(), this.scope, this.size);
return Session;
return;
}
/// <summary>

View File

@@ -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:

View File

@@ -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);
}
}
}
}

View File

@@ -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();

View File

@@ -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();
}
}

View File

@@ -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,
}

View File

@@ -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,
};

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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);

View File

@@ -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)),
};

View File

@@ -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),
});
}

View File

@@ -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

View File

@@ -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.

View File

@@ -394,6 +394,7 @@ public:
{
m_enabled = true;
Logger::info(L"Enabling Light Switch module...");
Trace::Enable(true);
unsigned long powertoys_pid = GetCurrentProcessId();
std::wstring args = L"--pid " + std::to_wstring(powertoys_pid);
@@ -469,6 +470,7 @@ public:
CloseHandle(m_process);
m_process = nullptr;
}
Trace::Enable(false);
}
// Returns if the powertoys is enabled
@@ -524,6 +526,8 @@ public:
if (m_enabled)
{
Logger::trace(L"Light Switch hotkey pressed");
Trace::ShortcutInvoked();
if (!is_process_running())
{
enable();

View File

@@ -19,12 +19,21 @@ void Trace::UnregisterProvider()
TraceLoggingUnregister(g_hProvider);
}
void Trace::MyEvent()
void Trace::Enable(bool enabled) noexcept
{
TraceLoggingWrite(
g_hProvider,
"PowerToyName_MyEvent",
"LightSwitch_EnableLightSwitch",
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE),
TraceLoggingBoolean(enabled, "Enabled"));
}
void Trace::ShortcutInvoked() noexcept
{
TraceLoggingWrite(
g_hProvider,
"LightSwitch_ShortcutInvoked",
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
TraceLoggingBoolean(TRUE, "UTCReplace_AppSessionGuid"),
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE));
}

View File

@@ -11,5 +11,6 @@ class Trace
public:
static void RegisterProvider();
static void UnregisterProvider();
static void MyEvent();
static void Enable(bool enabled) noexcept;
static void ShortcutInvoked() noexcept;
};

View File

@@ -14,6 +14,7 @@
#include "LightSwitchStateManager.h"
#include <LightSwitchUtils.h>
#include <NightLightRegistryObserver.h>
#include <trace.h>
SERVICE_STATUS g_ServiceStatus = {};
SERVICE_STATUS_HANDLE g_StatusHandle = nullptr;
@@ -371,5 +372,6 @@ int APIENTRY wWinMain(HINSTANCE, HINSTANCE, PWSTR, int)
LPWSTR* argv = CommandLineToArgvW(GetCommandLineW(), &argc);
int rc = _tmain(argc, argv); // reuse your existing logic
LocalFree(argv);
return rc;
}

View File

@@ -80,6 +80,7 @@
<ClCompile Include="SettingsConstants.cpp" />
<ClCompile Include="ThemeHelper.cpp" />
<ClCompile Include="ThemeScheduler.cpp" />
<ClCompile Include="trace.cpp" />
<ClCompile Include="WinHookEventIDs.cpp" />
</ItemGroup>
<ItemGroup>
@@ -94,6 +95,7 @@
<ClInclude Include="SettingsObserver.h" />
<ClInclude Include="ThemeHelper.h" />
<ClInclude Include="ThemeScheduler.h" />
<ClInclude Include="trace.h" />
<ClInclude Include="WinHookEventIDs.h" />
</ItemGroup>
<ItemGroup>

View File

@@ -39,6 +39,9 @@
<ClCompile Include="NightLightRegistryObserver.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="trace.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="ThemeScheduler.h">
@@ -68,6 +71,9 @@
<ClInclude Include="NightLightRegistryObserver.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="trace.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<Natvis Include="$(MSBuildThisFileDirectory)..\..\natvis\wil.natvis" />

View File

@@ -5,6 +5,7 @@
#include <filesystem>
#include <fstream>
#include <logger.h>
#include <LightSwitchService/trace.h>
using namespace std;
@@ -151,6 +152,7 @@ void LightSwitchSettings::LoadSettings()
if (m_settings.scheduleMode != newMode)
{
m_settings.scheduleMode = newMode;
Trace::LightSwitch::ScheduleModeToggled(val);
NotifyObservers(SettingId::ScheduleMode);
}
}
@@ -220,6 +222,8 @@ void LightSwitchSettings::LoadSettings()
}
}
bool themeTargetChanged = false;
// ChangeSystem
if (const auto jsonVal = values.get_bool_value(L"changeSystem"))
{
@@ -227,6 +231,7 @@ void LightSwitchSettings::LoadSettings()
if (m_settings.changeSystem != val)
{
m_settings.changeSystem = val;
themeTargetChanged = true;
NotifyObservers(SettingId::ChangeSystem);
}
}
@@ -238,9 +243,16 @@ void LightSwitchSettings::LoadSettings()
if (m_settings.changeApps != val)
{
m_settings.changeApps = val;
themeTargetChanged = true;
NotifyObservers(SettingId::ChangeApps);
}
}
// For ChangeSystem/ChangeApps changes, log telemetry
if (themeTargetChanged)
{
Trace::LightSwitch::ThemeTargetChanged(m_settings.changeApps, m_settings.changeSystem);
}
}
catch (...)
{

View File

@@ -0,0 +1,33 @@
#include "pch.h"
#include "trace.h"
// Telemetry strings should not be localized.
#define LoggingProviderKey "Microsoft.PowerToys"
TRACELOGGING_DEFINE_PROVIDER(
g_hProvider,
LoggingProviderKey,
// {38e8889b-9731-53f5-e901-e8a7c1753074}
(0x38e8889b, 0x9731, 0x53f5, 0xe9, 0x01, 0xe8, 0xa7, 0xc1, 0x75, 0x30, 0x74),
TraceLoggingOptionProjectTelemetry());
void Trace::LightSwitch::ScheduleModeToggled(const std::wstring& newMode) noexcept
{
TraceLoggingWriteWrapper(
g_hProvider,
"LightSwitch_ScheduleModeToggled",
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE),
TraceLoggingWideString(newMode.c_str(), "NewMode"));
}
void Trace::LightSwitch::ThemeTargetChanged(bool changeApps, bool changeSystem) noexcept
{
TraceLoggingWriteWrapper(
g_hProvider,
"LightSwitch_ThemeTargetChanged",
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE),
TraceLoggingBoolean(changeApps, "ChangeApps"),
TraceLoggingBoolean(changeSystem, "ChangeSystem"));
}

View File

@@ -0,0 +1,15 @@
#pragma once
#include <common/Telemetry/TraceBase.h>
#include <string>
class Trace
{
public:
class LightSwitch : public telemetry::TraceBase
{
public:
static void ScheduleModeToggled(const std::wstring& newMode) noexcept;
static void ThemeTargetChanged(bool changeApps, bool changeSystem) noexcept;
};
};

View File

@@ -617,8 +617,6 @@ namespace MouseUtils.UITests
private void LaunchFromSetting(bool reload = false, bool launchAsAdmin = false)
{
Session = RestartScopeExe("FindMyMouse,MouseHighlighter,MouseJump,MousePointerCrosshairs,CursorWrap");
// this.Session.Attach(PowerToysModule.PowerToysSettings);
this.Session.SetMainWindowSize(WindowSize.Large);

View 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();

View File

@@ -19,8 +19,6 @@ public partial class DetailsViewModel(IDetails _details, WeakReference<IPageCont
public string Body { get; private set; } = string.Empty;
public ContentSize? Size { get; private set; } = ContentSize.Small;
// Metadata is an array of IDetailsElement,
// where IDetailsElement = {IDetailsTags, IDetailsLink, IDetailsSeparator}
public List<DetailsElementViewModel> Metadata { get; private set; } = [];
@@ -42,21 +40,6 @@ public partial class DetailsViewModel(IDetails _details, WeakReference<IPageCont
UpdateProperty(nameof(Body));
UpdateProperty(nameof(HeroImage));
if (model is IExtendedAttributesProvider provider)
{
if (provider.GetProperties()?.TryGetValue("Size", out var rawValue) == true)
{
if (rawValue is int sizeAsInt)
{
Size = (ContentSize)sizeAsInt;
}
}
}
Size ??= ContentSize.Small;
UpdateProperty(nameof(Size));
var meta = model.Metadata;
if (meta is not null)
{

View File

@@ -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;
}

View File

@@ -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));
}
}

View File

@@ -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,

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<Package
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
@@ -8,19 +8,13 @@
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
IgnorableNamespaces="uap uap3 rescap">
<!-- FOR PUBLISHING TO MICROSOFT STORE -->
<!-- When you're ready to publish your extension to Microsoft Store,you'll need to
change the values in the Identity & Properties tags below
Name = replace with Microsoft Store's Package/Identity/Name
Publisher = replace with Microsoft Store's Package/Identity/Publisher
DisplayName = replace with the reserved name from Partner Center
PublisherDisplayName = replace with Microsoft Store's Package/Properties/PublisherDisplayName
Logo = Confirm that this image exist at the path
-->
<Identity
Name="TemplateCmdPalExtension"
Publisher="CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US"
Version="0.0.1.0" />
<!-- When you're ready to publish your extension, you'll need to change the
Publisher= to match your own identity -->
<Properties>
<DisplayName>TemplateDisplayName</DisplayName>
<PublisherDisplayName>A Lone Developer</PublisherDisplayName>

View File

@@ -1,129 +0,0 @@
# Publication Setup
This folder contains tools to help you prepare your CmdPal extension for publication to the Microsoft Store and WinGet.
## Files and Folders in this Directory
### Scripts
- **`one-time-store-publishing-setup.ps1`** - Configure your project for Microsoft Store publishing (run once)
- **`build-msix-bundles.ps1`** - Build MSIX packages and create bundles for Store submission
- **`one-time-winget-publishing-setup.ps1`** - Configure your project for WinGet publishing (run once)
### Resource Folders
- **`microsoft-store-resources/`** - Contains files used for Microsoft Store publishing:
- `bundle_mapping.txt` - Auto-generated file that maps MSIX files for bundle creation
- **`winget-resources/`** - Contains templates and scripts for WinGet publishing:
- `build-exe.ps1` - Script to build standalone EXE installer
- `setup-template.iss` - Inno Setup installer template
- `release-extension.yml` - GitHub Actions workflow template (moved to `.github/workflows/` during setup)
- `Backups/` - Backup copies of configuration files (created during setup)
## Microsoft Store Quick Start
1. Open PowerShell and navigate to the Publication folder:
```powershell
cd <YourProject>\Publication
```
2. Run the one-time setup script:
```powershell
.\one-time-store-publishing-setup.ps1
```
3. Follow the prompts to enter your Microsoft Store information from Partner Center:
- Package Identity Name
- Publisher Certificate
- Display Name
- Publisher Display Name
The script will update your `Package.appxmanifest` with Store-specific values.
4. Once configured, build your bundle:
```powershell
.\build-msix-bundles.ps1
```
This script will:
- Build x64 and ARM64 MSIX packages
- Automatically update `microsoft-store-resources\bundle_mapping.txt` with correct paths
- Create a combined MSIX bundle
- Display the bundle location when complete
5. Upload the resulting `.msixbundle` file from `microsoft-store-resources\` to Partner Center
## Troubleshooting
### makeappx.exe not found
The build script requires the Windows SDK. Install it via:
- Visual Studio Installer (Individual Components → Windows SDK)
- [Standalone Windows SDK](https://developer.microsoft.com/windows/downloads/windows-sdk/)
### Build errors
Ensure you have:
- .NET 9.0 SDK installed
- Windows SDK 10.0.26100.0 or compatible version
- No other instances of Visual Studio building the project
### Bundle creation fails
Check that:
- Both x64 and ARM64 builds completed successfully
- `microsoft-store-resources\bundle_mapping.txt` paths are correct (auto-updated by script)
- No file locks on the MSIX files
## WinGet Quick Start
1. Open PowerShell and navigate to the Publication folder:
```powershell
cd <YourProject>\Publication
```
2. Run the one-time setup script:
```powershell
.\one-time-winget-publishing-setup.ps1
```
3. Follow the prompts to enter:
- GitHub Repository URL (where releases will be published)
- Developer/Publisher Name
The script will:
- Configure `winget-resources\build-exe.ps1` with your extension details
- Configure `winget-resources\setup-template.iss` with your extension information
- Move `release-extension.yml` to `.github\workflows\` in your repository root
4. Commit and push changes to GitHub:
```powershell
git add .
git commit -m "Configure extension for WinGet publishing"
git push
```
5. Trigger the GitHub Action to build and release:
```powershell
gh workflow run release-extension.yml --ref main -f "release_notes=**First Release of <ExtensionName> Extension for Command Palette**
The inaugural release of the <ExtensionName> for Command Palette..."
```
Or create a release manually through the GitHub web interface.
## Additional Resources
- [Command Palette Extension Publishing Documentation](https://learn.microsoft.com/en-us/windows/powertoys/command-palette/publish-extension)
- [Microsoft Store Publishing Guide](https://learn.microsoft.com/windows/apps/publish/)

View File

@@ -1,570 +0,0 @@
# Build MSIX Bundles Script for CmdPal Extension
# This script automates the process of building MSIX packages for x64 and ARM64 architectures
# and creating an MSIX bundle for distribution
# Version: 1.0
#Requires -Version 5.1
# Enable strict mode for better error detection
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
Write-Host "================================================================" -ForegroundColor Cyan
Write-Host " CmdPal Extension MSIX Builder" -ForegroundColor Cyan
Write-Host "================================================================" -ForegroundColor Cyan
Write-Host ""
# Determine project root (parent of Publication folder)
$projectRoot = Split-Path -Parent $PSScriptRoot
$projectName = Split-Path -Leaf $projectRoot
Write-Host "Project Configuration:" -ForegroundColor Yellow
Write-Host " Project Root: $projectRoot" -ForegroundColor Gray
Write-Host " Project Name: $projectName" -ForegroundColor Gray
Write-Host ""
# Verify we're in the right location
$csprojPath = Join-Path $projectRoot "$projectName.csproj"
$manifestPath = Join-Path $projectRoot "Package.appxmanifest"
if (-not (Test-Path $csprojPath)) {
Write-Host "ERROR: Could not find .csproj file at: $csprojPath" -ForegroundColor Red
Write-Host ""
Write-Host "This script must be run from the Publication folder within your project." -ForegroundColor Yellow
Write-Host "Expected structure:" -ForegroundColor Gray
Write-Host " <ProjectRoot>\" -ForegroundColor Gray
Write-Host " <ProjectName>.csproj" -ForegroundColor Gray
Write-Host " Publication\" -ForegroundColor Gray
Write-Host " build-msix-bundles.ps1 (this script)" -ForegroundColor Gray
Write-Host ""
Write-Host "Press any key to exit..." -ForegroundColor Gray
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
exit 1
}
if (-not (Test-Path $manifestPath)) {
Write-Host "ERROR: Could not find Package.appxmanifest at: $manifestPath" -ForegroundColor Red
Write-Host "Press any key to exit..." -ForegroundColor Gray
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
exit 1
}
Write-Host " [OK] Project files validated" -ForegroundColor Green
Write-Host ""
# Extract version from Package.appxmanifest
Write-Host "Reading package information..." -ForegroundColor Cyan
try {
[xml]$manifest = Get-Content $manifestPath -ErrorAction Stop
$packageName = $manifest.Package.Identity.Name
$packageVersion = $manifest.Package.Identity.Version
Write-Host " Package Name: $packageName" -ForegroundColor White
Write-Host " Version: $packageVersion" -ForegroundColor White
Write-Host ""
}
catch {
Write-Host "ERROR: Could not read Package.appxmanifest: $($_.Exception.Message)" -ForegroundColor Red
Write-Host "Press any key to exit..." -ForegroundColor Gray
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
exit 1
}
# Ask user what to build
Write-Host "================================================================" -ForegroundColor Cyan
Write-Host " Build Options" -ForegroundColor Cyan
Write-Host "================================================================" -ForegroundColor Cyan
Write-Host ""
Write-Host "What would you like to build?" -ForegroundColor Yellow
Write-Host ""
Write-Host " [1] x64 MSIX only" -ForegroundColor White
Write-Host " [2] ARM64 MSIX only" -ForegroundColor White
Write-Host " [3] Complete Bundle (x64 + ARM64 + Bundle file)" -ForegroundColor White
Write-Host ""
Write-Host "Enter your choice (1-3): " -ForegroundColor Yellow -NoNewline
$buildChoice = Read-Host
Write-Host ""
# Validate choice
if ($buildChoice -notmatch '^[1-3]$') {
Write-Host "ERROR: Invalid choice. Please enter 1, 2, or 3." -ForegroundColor Red
Write-Host "Press any key to exit..." -ForegroundColor Gray
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
exit 1
}
# Determine what to build
$buildX64 = $false
$buildARM64 = $false
$createBundle = $false
switch ($buildChoice) {
"1" {
$buildX64 = $true
Write-Host "Building: x64 MSIX only" -ForegroundColor Cyan
}
"2" {
$buildARM64 = $true
Write-Host "Building: ARM64 MSIX only" -ForegroundColor Cyan
}
"3" {
$buildX64 = $true
$buildARM64 = $true
$createBundle = $true
Write-Host "Building: Complete Bundle (x64 + ARM64 + Bundle)" -ForegroundColor Cyan
}
}
Write-Host ""
# Clean previous builds (optional)
Write-Host "Do you want to clean previous builds? (Y/N): " -ForegroundColor Yellow -NoNewline
$cleanBuilds = Read-Host
if ($cleanBuilds -match '^[Yy]') {
Write-Host ""
Write-Host "Cleaning previous builds..." -ForegroundColor Cyan
$appPackagesPath = Join-Path $projectRoot "AppPackages"
if (Test-Path $appPackagesPath) {
try {
Remove-Item $appPackagesPath -Recurse -Force -ErrorAction Stop
Write-Host " [OK] Cleaned AppPackages folder" -ForegroundColor Green
}
catch {
Write-Host " [WARNING] Could not clean AppPackages: $($_.Exception.Message)" -ForegroundColor Yellow
}
}
# Clean old bundles in microsoft-store-resources folder
$microsoftStoreResourcesPath = Join-Path $PSScriptRoot "microsoft-store-resources"
if (Test-Path $microsoftStoreResourcesPath) {
$oldBundles = Get-ChildItem $microsoftStoreResourcesPath -Filter "*.msixbundle" -ErrorAction SilentlyContinue
if ($oldBundles) {
foreach ($bundle in $oldBundles) {
try {
Remove-Item $bundle.FullName -Force -ErrorAction Stop
Write-Host " [OK] Removed old bundle: $($bundle.Name)" -ForegroundColor Green
}
catch {
Write-Host " [WARNING] Could not remove $($bundle.Name): $($_.Exception.Message)" -ForegroundColor Yellow
}
}
}
}
Write-Host ""
}
else {
Write-Host ""
}
# Track built files for summary
$builtFiles = @()
$x64Msix = $null
$arm64Msix = $null
# Build x64 MSIX (if requested)
if ($buildX64) {
Write-Host "================================================================" -ForegroundColor Cyan
Write-Host " Building x64 MSIX Package" -ForegroundColor Cyan
Write-Host "================================================================" -ForegroundColor Cyan
Write-Host ""
Write-Host "Running: dotnet build (x64)..." -ForegroundColor Yellow
Write-Host "This may take a few seconds" -ForegroundColor Yellow
Write-Host ""
Push-Location $projectRoot
try {
$buildOutput = & dotnet build --configuration Release -p:GenerateAppxPackageOnBuild=true -p:Platform=x64 -p:AppxPackageDir="AppPackages\x64\" 2>&1
if ($LASTEXITCODE -ne 0) {
Write-Host ""
Write-Host "ERROR: x64 build failed with exit code $LASTEXITCODE" -ForegroundColor Red
Write-Host ""
Write-Host "Build output:" -ForegroundColor Gray
$buildOutput | ForEach-Object { Write-Host " $_" -ForegroundColor Gray }
Write-Host ""
Write-Host "Press any key to exit..." -ForegroundColor Gray
Pop-Location
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
exit 1
}
Write-Host " [SUCCESS] x64 build completed" -ForegroundColor Green
Write-Host ""
}
catch {
Write-Host ""
Write-Host "ERROR: x64 build failed: $($_.Exception.Message)" -ForegroundColor Red
Pop-Location
Write-Host "Press any key to exit..." -ForegroundColor Gray
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
exit 1
}
finally {
Pop-Location
}
}
# Build ARM64 MSIX (if requested)
if ($buildARM64) {
Write-Host "================================================================" -ForegroundColor Cyan
Write-Host " Building ARM64 MSIX Package" -ForegroundColor Cyan
Write-Host "================================================================" -ForegroundColor Cyan
Write-Host ""
Write-Host "Running: dotnet build (ARM64)..." -ForegroundColor Yellow
Write-Host "This may take a few seconds" -ForegroundColor Yellow
Write-Host ""
Push-Location $projectRoot
try {
$buildOutput = & dotnet build --configuration Release -p:GenerateAppxPackageOnBuild=true -p:Platform=ARM64 -p:AppxPackageDir="AppPackages\ARM64\" 2>&1
if ($LASTEXITCODE -ne 0) {
Write-Host ""
Write-Host "ERROR: ARM64 build failed with exit code $LASTEXITCODE" -ForegroundColor Red
Write-Host ""
Write-Host "Build output:" -ForegroundColor Gray
$buildOutput | ForEach-Object { Write-Host " $_" -ForegroundColor Gray }
Write-Host ""
Write-Host "Press any key to exit..." -ForegroundColor Gray
Pop-Location
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
exit 1
}
Write-Host " [SUCCESS] ARM64 build completed" -ForegroundColor Green
Write-Host ""
}
catch {
Write-Host ""
Write-Host "ERROR: ARM64 build failed: $($_.Exception.Message)" -ForegroundColor Red
Pop-Location
Write-Host "Press any key to exit..." -ForegroundColor Gray
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
exit 1
}
finally {
Pop-Location
}
}
# Locate MSIX files (if bundle creation is needed or for summary)
if ($createBundle -or $buildX64 -or $buildARM64) {
Write-Host "================================================================" -ForegroundColor Cyan
Write-Host " Locating MSIX Files" -ForegroundColor Cyan
Write-Host "================================================================" -ForegroundColor Cyan
Write-Host ""
Push-Location $projectRoot
try {
$msixFiles = Get-ChildItem "AppPackages" -Recurse -Filter "*.msix" -ErrorAction SilentlyContinue
if (-not $msixFiles) {
# Try alternate location
Write-Host " MSIX files not found in AppPackages, checking bin folder..." -ForegroundColor Yellow
$msixFiles = Get-ChildItem "bin" -Recurse -Filter "*.msix" -ErrorAction SilentlyContinue
}
if ($buildX64 -and $buildARM64 -and $createBundle -and (-not $msixFiles -or $msixFiles.Count -lt 2)) {
Write-Host "ERROR: Could not find both x64 and ARM64 MSIX files" -ForegroundColor Red
Write-Host ""
Write-Host "Expected files:" -ForegroundColor Gray
Write-Host " - ${packageName}_${packageVersion}_x64.msix" -ForegroundColor Gray
Write-Host " - ${packageName}_${packageVersion}_arm64.msix" -ForegroundColor Gray
Write-Host ""
if ($msixFiles) {
Write-Host "Found files:" -ForegroundColor Yellow
$msixFiles | ForEach-Object { Write-Host " - $($_.FullName)" -ForegroundColor Gray }
Write-Host ""
}
Pop-Location
Write-Host "Press any key to exit..." -ForegroundColor Gray
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
exit 1
}
if ($msixFiles) {
Write-Host " Found MSIX files:" -ForegroundColor Green
$msixFiles | ForEach-Object {
$relativePath = $_.FullName -replace [regex]::Escape($projectRoot + "\"), ""
Write-Host " [OK] $relativePath" -ForegroundColor White
}
Write-Host ""
}
# Find specific x64 and ARM64 files
if ($buildX64) {
$x64Msix = $msixFiles | Where-Object { $_.Name -match "_x64\.msix$" } | Select-Object -First 1
if ($x64Msix) {
$builtFiles += $x64Msix.FullName
}
}
if ($buildARM64) {
$arm64Msix = $msixFiles | Where-Object { $_.Name -match "_arm64\.msix$" } | Select-Object -First 1
if ($arm64Msix) {
$builtFiles += $arm64Msix.FullName
}
}
# Validate files for bundle creation
if ($createBundle) {
if (-not $x64Msix) {
Write-Host "ERROR: Could not find x64 MSIX file" -ForegroundColor Red
Pop-Location
Write-Host "Press any key to exit..." -ForegroundColor Gray
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
exit 1
}
if (-not $arm64Msix) {
Write-Host "ERROR: Could not find ARM64 MSIX file" -ForegroundColor Red
Pop-Location
Write-Host "Press any key to exit..." -ForegroundColor Gray
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
exit 1
}
}
}
finally {
Pop-Location
}
}
# Create bundle (if requested)
if ($createBundle) {
# Update bundle_mapping.txt
Write-Host "================================================================" -ForegroundColor Cyan
Write-Host " Updating bundle_mapping.txt" -ForegroundColor Cyan
Write-Host "================================================================" -ForegroundColor Cyan
Write-Host ""
$microsoftStoreResourcesPath = Join-Path $PSScriptRoot "microsoft-store-resources"
$bundleMappingPath = Join-Path $microsoftStoreResourcesPath "bundle_mapping.txt"
# Ensure microsoft-store-resources directory exists
if (-not (Test-Path $microsoftStoreResourcesPath)) {
Write-Host " Creating microsoft-store-resources folder..." -ForegroundColor Yellow
try {
New-Item -Path $microsoftStoreResourcesPath -ItemType Directory -Force | Out-Null
Write-Host " [OK] Folder created" -ForegroundColor Green
}
catch {
Write-Host " [ERROR] Could not create folder: $($_.Exception.Message)" -ForegroundColor Red
}
}
# Get relative paths from project root
$x64RelativePath = $x64Msix.FullName -replace [regex]::Escape($projectRoot + "\"), ""
$arm64RelativePath = $arm64Msix.FullName -replace [regex]::Escape($projectRoot + "\"), ""
# Create bundle mapping content
$line1 = "`"$x64RelativePath`" `"$($x64Msix.Name)`""
$line2 = "`"$arm64RelativePath`" `"$($arm64Msix.Name)`""
$bundleMappingContent = "[Files]`r`n$line1`r`n$line2"
try {
Set-Content -Path $bundleMappingPath -Value $bundleMappingContent -NoNewline -ErrorAction Stop
Write-Host " [SUCCESS] bundle_mapping.txt updated" -ForegroundColor Green
Write-Host ""
Write-Host " Content:" -ForegroundColor Gray
Write-Host " [Files]" -ForegroundColor DarkGray
Write-Host (' "' + $x64RelativePath + '" "' + $x64Msix.Name + '"') -ForegroundColor DarkGray
Write-Host (' "' + $arm64RelativePath + '" "' + $arm64Msix.Name + '"') -ForegroundColor DarkGray
Write-Host ""
}
catch {
Write-Host " [ERROR] Could not update bundle_mapping.txt: $($_.Exception.Message)" -ForegroundColor Red
Write-Host " Continuing with bundle creation..." -ForegroundColor Yellow
Write-Host ""
}
# Find makeappx.exe
Write-Host "================================================================" -ForegroundColor Cyan
Write-Host " Creating MSIX Bundle" -ForegroundColor Cyan
Write-Host "================================================================" -ForegroundColor Cyan
Write-Host ""
Write-Host "Locating makeappx.exe..." -ForegroundColor Yellow
$arch = switch ($env:PROCESSOR_ARCHITECTURE) {
"AMD64" { "x64" }
"x86" { "x86" }
"ARM64" { "arm64" }
default { "x64" }
}
Write-Host " Detected architecture: $arch" -ForegroundColor Gray
$makeappxPath = Get-ChildItem "C:\Program Files (x86)\Windows Kits\10\bin\*\$arch\makeappx.exe" -ErrorAction SilentlyContinue |
Sort-Object Name -Descending |
Select-Object -First 1
if (-not $makeappxPath) {
Write-Host ""
Write-Host "ERROR: makeappx.exe not found" -ForegroundColor Red
Write-Host ""
Write-Host "makeappx.exe is part of the Windows SDK." -ForegroundColor Yellow
Write-Host "Please install the Windows SDK from:" -ForegroundColor Yellow
Write-Host " https://developer.microsoft.com/windows/downloads/windows-sdk/" -ForegroundColor Cyan
Write-Host ""
Write-Host "Or ensure the Windows SDK is installed with Visual Studio." -ForegroundColor Yellow
Write-Host ""
Write-Host "Press any key to exit..." -ForegroundColor Gray
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
exit 1
}
Write-Host " [OK] Found: $($makeappxPath.FullName)" -ForegroundColor Green
Write-Host ""
# Create bundle
$bundleFileName = "${packageName}_${packageVersion}_Bundle.msixbundle"
$bundleOutputPath = Join-Path $microsoftStoreResourcesPath $bundleFileName
Write-Host "Creating bundle: $bundleFileName" -ForegroundColor Yellow
Write-Host ""
Push-Location $projectRoot
try {
# Use absolute path to bundle_mapping.txt
$bundleMappingAbsolute = Join-Path $microsoftStoreResourcesPath "bundle_mapping.txt"
# Verify the mapping file exists
if (-not (Test-Path $bundleMappingAbsolute)) {
Write-Host "ERROR: bundle_mapping.txt not found at: $bundleMappingAbsolute" -ForegroundColor Red
Pop-Location
Write-Host "Press any key to exit..." -ForegroundColor Gray
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
exit 1
}
$makeappxArgs = @(
"bundle",
"/v",
"/f", "`"$bundleMappingAbsolute`"",
"/p", "`"$bundleOutputPath`""
)
Write-Host " Running: makeappx bundle /v /f `"$bundleMappingAbsolute`" /p `"$bundleOutputPath`"" -ForegroundColor Gray
Write-Host ""
# Run makeappx with proper quoting
$bundleOutput = & $makeappxPath.FullName bundle /v /f $bundleMappingAbsolute /p $bundleOutputPath 2>&1
if ($LASTEXITCODE -ne 0) {
Write-Host ""
Write-Host "ERROR: Bundle creation failed with exit code $LASTEXITCODE" -ForegroundColor Red
Write-Host ""
Write-Host "Output:" -ForegroundColor Gray
$bundleOutput | ForEach-Object { Write-Host " $_" -ForegroundColor Gray }
Write-Host ""
Pop-Location
Write-Host "Press any key to exit..." -ForegroundColor Gray
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
exit 1
}
Write-Host " [SUCCESS] Bundle created" -ForegroundColor Green
Write-Host ""
}
catch {
Write-Host ""
Write-Host "ERROR: Bundle creation failed: $($_.Exception.Message)" -ForegroundColor Red
Pop-Location
Write-Host "Press any key to exit..." -ForegroundColor Gray
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
exit 1
}
finally {
Pop-Location
}
# Verify bundle was created
if (-not (Test-Path $bundleOutputPath)) {
Write-Host "ERROR: Bundle file was not created at expected location" -ForegroundColor Red
Write-Host " Expected: $bundleOutputPath" -ForegroundColor Gray
Write-Host ""
Write-Host "Press any key to exit..." -ForegroundColor Gray
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
exit 1
}
# Add bundle to built files
$builtFiles += $bundleOutputPath
}
# Final Summary
Write-Host "================================================================" -ForegroundColor Green
Write-Host " BUILD COMPLETED SUCCESSFULLY!" -ForegroundColor Green
Write-Host "================================================================" -ForegroundColor Green
Write-Host ""
# Display what was built
if ($buildChoice -eq "1") {
Write-Host "Built: x64 MSIX Package" -ForegroundColor Cyan
}
elseif ($buildChoice -eq "2") {
Write-Host "Built: ARM64 MSIX Package" -ForegroundColor Cyan
}
elseif ($buildChoice -eq "3") {
Write-Host "Built: Complete Bundle (x64 + ARM64 + Bundle file)" -ForegroundColor Cyan
}
Write-Host ""
Write-Host "Package Information:" -ForegroundColor Yellow
Write-Host " Name: $packageName" -ForegroundColor White
Write-Host " Version: $packageVersion" -ForegroundColor White
Write-Host ""
# Display built files
Write-Host "Built Files:" -ForegroundColor Yellow
if ($x64Msix) {
$x64Size = "{0:N2} MB" -f ((Get-Item $x64Msix.FullName).Length / 1MB)
Write-Host " [x64 MSIX]" -ForegroundColor Cyan
Write-Host " Location: $($x64Msix.FullName)" -ForegroundColor White
Write-Host " Size: $x64Size" -ForegroundColor White
Write-Host ""
}
if ($arm64Msix) {
$arm64Size = "{0:N2} MB" -f ((Get-Item $arm64Msix.FullName).Length / 1MB)
Write-Host " [ARM64 MSIX]" -ForegroundColor Cyan
Write-Host " Location: $($arm64Msix.FullName)" -ForegroundColor White
Write-Host " Size: $arm64Size" -ForegroundColor White
Write-Host ""
}
if ($createBundle -and (Test-Path $bundleOutputPath)) {
$bundleSize = "{0:N2} MB" -f ((Get-Item $bundleOutputPath).Length / 1MB)
Write-Host " [MSIX Bundle]" -ForegroundColor Cyan
Write-Host " Location: $bundleOutputPath" -ForegroundColor White
Write-Host " Size: $bundleSize" -ForegroundColor White
Write-Host ""
}
Write-Host "================================================================" -ForegroundColor Green
Write-Host ""
# Display appropriate next steps based on what was built
Write-Host "Next Steps:" -ForegroundColor Cyan
if ($createBundle) {
Write-Host " 1. Test the bundle by installing it locally" -ForegroundColor Gray
Write-Host " 2. Upload the bundle to Microsoft Store Partner Center" -ForegroundColor Gray
Write-Host " 3. Or distribute via other channels" -ForegroundColor Gray
}
else {
Write-Host " 1. Test the MSIX package by installing it locally" -ForegroundColor Gray
if ($buildX64) {
Write-Host " 2. Build ARM64 package (option 2) or complete bundle (option 3)" -ForegroundColor Gray
}
else {
Write-Host " 2. Build x64 package (option 1) or complete bundle (option 3)" -ForegroundColor Gray
}
Write-Host " 3. Or distribute this individual package" -ForegroundColor Gray
}
Write-Host ""
Write-Host "Press any key to exit..." -ForegroundColor Gray
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")

View File

@@ -1,3 +0,0 @@
[Files]
"AppPackages\x64\TemplateCmdPalExtension_0.0.1.0_x64_Test\TemplateCmdPalExtension_0.0.1.0_x64.msix" "TemplateCmdPalExtension_0.0.1.0_x64.msix"
"AppPackages\ARM64\TemplateCmdPalExtension_0.0.1.0_arm64_Test\TemplateCmdPalExtension_0.0.1.0_arm64.msix" "TemplateCmdPalExtension_0.0.1.0_arm64.msix"

View File

@@ -1,829 +0,0 @@
# One-Time Publication Setup Script for CmdPal Extension
# This script collects Microsoft Store publication information and updates project files
# Version: 1.1
#Requires -Version 5.1
# Enable strict mode for better error detection
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
Write-Host "================================================================" -ForegroundColor Cyan
Write-Host " Microsoft Store Publication Setup" -ForegroundColor Cyan
Write-Host " CmdPal Extension Publisher" -ForegroundColor Cyan
Write-Host "================================================================" -ForegroundColor Cyan
Write-Host ""
# Path to the project files
$projectRoot = Split-Path -Parent $PSScriptRoot
$csprojPath = Join-Path $projectRoot "TemplateCmdPalExtension.csproj"
$manifestPath = Join-Path $projectRoot "Package.appxmanifest"
Write-Host "Validating project structure..." -ForegroundColor Cyan
Write-Host " Project Root: $projectRoot" -ForegroundColor Gray
# Verify files exist with detailed error messages
if (-not (Test-Path $csprojPath)) {
Write-Host ""
Write-Host "ERROR: Could not find .csproj file" -ForegroundColor Red
Write-Host " Expected location: $csprojPath" -ForegroundColor Gray
Write-Host ""
Write-Host "This script must be run from the Publication folder within your project." -ForegroundColor Yellow
Write-Host "Please navigate to: <YourProject>\Publication\" -ForegroundColor Yellow
Write-Host "Press any key to exit..." -ForegroundColor Gray
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
exit 1
}
if (-not (Test-Path $manifestPath)) {
Write-Host ""
Write-Host "ERROR: Could not find Package.appxmanifest file" -ForegroundColor Red
Write-Host " Expected location: $manifestPath" -ForegroundColor Gray
Write-Host ""
Write-Host "Your project structure may be incomplete or corrupted." -ForegroundColor Yellow
Write-Host "Please ensure Package.appxmanifest exists in your project root." -ForegroundColor Yellow
Write-Host "Press any key to exit..." -ForegroundColor Gray
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
exit 1
}
Write-Host " [OK] .csproj file found" -ForegroundColor Green
Write-Host " [OK] Package.appxmanifest file found" -ForegroundColor Green
Write-Host ""
# Create backup directory if it doesn't exist
$backupDir = Join-Path $projectRoot "Publication\Backups"
if (-not (Test-Path $backupDir)) {
try {
New-Item -Path $backupDir -ItemType Directory -Force | Out-Null
Write-Host "Created backup directory: $backupDir" -ForegroundColor Gray
}
catch {
Write-Host "WARNING: Could not create backup directory. Proceeding without backups." -ForegroundColor Yellow
$backupDir = $null
}
}
# Create timestamped backups
if ($backupDir) {
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
try {
Copy-Item $csprojPath -Destination (Join-Path $backupDir "TemplateCmdPalExtension.csproj.$timestamp.bak") -Force
Copy-Item $manifestPath -Destination (Join-Path $backupDir "Package.appxmanifest.$timestamp.bak") -Force
Write-Host "Backup created: $timestamp" -ForegroundColor Gray
Write-Host ""
}
catch {
Write-Host "WARNING: Could not create backup files. Proceeding anyway." -ForegroundColor Yellow
Write-Host ""
}
}
Write-Host "This script will collect information needed to publish your extension" -ForegroundColor White
Write-Host "to the Microsoft Store. You can find this information in your" -ForegroundColor White
Write-Host "Microsoft Partner Center account." -ForegroundColor White
Write-Host ""
Write-Host "IMPORTANT: Have your Partner Center information ready before proceeding." -ForegroundColor Yellow
Write-Host " - Package Identity Name" -ForegroundColor Gray
Write-Host " - Publisher Certificate Name" -ForegroundColor Gray
Write-Host " - Reserved App Name" -ForegroundColor Gray
Write-Host " - Publisher Display Name" -ForegroundColor Gray
Write-Host ""
Write-Host "TIP: You can find this in Partner Center > Product Management > Product Identity" -ForegroundColor Cyan
Write-Host ""
# Prompt to continue
Write-Host "Do you want to continue? (Y/N): " -ForegroundColor Yellow -NoNewline
$continue = Read-Host
if ($continue -notmatch '^[Yy]') {
Write-Host ""
Write-Host "Setup cancelled by user." -ForegroundColor Yellow
Write-Host "Press any key to exit..." -ForegroundColor Gray
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
exit 0
}
Write-Host ""
# Function to validate package identity name format
function Test-PackageIdentityName {
param([string]$name)
# Package identity name rules:
# - Between 3 and 50 characters
# - Can contain: letters, numbers, periods, hyphens
# - Cannot start/end with period
# - Cannot have consecutive periods
if ([string]::IsNullOrWhiteSpace($name)) { return $false }
if ($name.Length -lt 3 -or $name.Length -gt 50) { return $false }
if ($name -match '^\.|\.$|\.\.') { return $false }
if ($name -notmatch '^[a-zA-Z0-9.-]+$') { return $false }
return $true
}
# Function to validate publisher certificate format
function Test-PublisherFormat {
param([string]$publisher)
if ([string]::IsNullOrWhiteSpace($publisher)) { return $false }
# Should start with CN= and follow distinguished name format
if ($publisher -notmatch '^CN=.+') { return $false }
# Check for valid characters in DN
if ($publisher -match '[<>]') { return $false }
return $true
}
# Function to validate display name
function Test-DisplayName {
param([string]$name)
if ([string]::IsNullOrWhiteSpace($name)) { return $false }
# Display name should be reasonable length and not contain control characters
if ($name.Length -lt 1 -or $name.Length -gt 256) { return $false }
if ($name -match '[\x00-\x1F\x7F]') { return $false }
return $true
}
# Collect Microsoft Store Package Identity Name
Write-Host "================================================================" -ForegroundColor Cyan
Write-Host " Step 1: Package Identity Name" -ForegroundColor Cyan
Write-Host "================================================================" -ForegroundColor Cyan
Write-Host ""
Write-Host "Enter Microsoft Store Package/Identity/Name:" -ForegroundColor Yellow
Write-Host " Location: Partner Center > Product Identity > Package/Identity/Name" -ForegroundColor Gray
Write-Host ""
Write-Host " Format Requirements:" -ForegroundColor Gray
Write-Host " - 3-50 characters" -ForegroundColor DarkGray
Write-Host " - Letters, numbers, periods, hyphens only" -ForegroundColor DarkGray
Write-Host " - Cannot start/end with period or have consecutive periods" -ForegroundColor DarkGray
Write-Host ""
Write-Host " Example: Publisher.MyAwesomeExtension" -ForegroundColor DarkGray
Write-Host ""
$packageIdentityName = ""
$maxAttempts = 3
$attempt = 0
do {
$attempt++
Write-Host "Package Identity Name" -NoNewline -ForegroundColor Yellow
if ($attempt -gt 1) {
Write-Host " (Attempt $attempt of $maxAttempts)" -NoNewline -ForegroundColor Red
}
Write-Host ": " -NoNewline -ForegroundColor Yellow
$packageIdentityName = Read-Host
if ([string]::IsNullOrWhiteSpace($packageIdentityName)) {
Write-Host " [ERROR] Package Identity Name cannot be empty." -ForegroundColor Red
Write-Host ""
}
elseif (-not (Test-PackageIdentityName $packageIdentityName)) {
Write-Host " [ERROR] Invalid Package Identity Name format." -ForegroundColor Red
Write-Host " Please ensure it meets the format requirements listed above." -ForegroundColor Yellow
Write-Host ""
}
else {
Write-Host " [OK] Package Identity Name accepted: $packageIdentityName" -ForegroundColor Green
Write-Host ""
break
}
if ($attempt -ge $maxAttempts) {
Write-Host ""
Write-Host "Maximum attempts reached. Please verify your Partner Center information and try again." -ForegroundColor Red
Write-Host "Press any key to exit..." -ForegroundColor Gray
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
exit 1
}
} while ($true)
# Collect Microsoft Store Package Identity Publisher
Write-Host "================================================================" -ForegroundColor Cyan
Write-Host " Step 2: Publisher Certificate" -ForegroundColor Cyan
Write-Host "================================================================" -ForegroundColor Cyan
Write-Host ""
Write-Host "Enter Microsoft Store Package/Identity/Publisher:" -ForegroundColor Yellow
Write-Host " Location: Partner Center > Product Identity > Package/Identity/Publisher" -ForegroundColor Gray
Write-Host ""
Write-Host " Format Requirements:" -ForegroundColor Gray
Write-Host " - Must start with 'CN=' (Certificate Name)" -ForegroundColor DarkGray
Write-Host " - This is the publisher certificate distinguished name" -ForegroundColor DarkGray
Write-Host ""
Write-Host " Example: CN=12345678-1234-1234-1234-123456789012" -ForegroundColor DarkGray
Write-Host " Example: CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US" -ForegroundColor DarkGray
Write-Host ""
$packageIdentityPublisher = ""
$attempt = 0
do {
$attempt++
Write-Host "Publisher Certificate" -NoNewline -ForegroundColor Yellow
if ($attempt -gt 1) {
Write-Host " (Attempt $attempt of $maxAttempts)" -NoNewline -ForegroundColor Red
}
Write-Host ": " -NoNewline -ForegroundColor Yellow
$packageIdentityPublisher = Read-Host
if ([string]::IsNullOrWhiteSpace($packageIdentityPublisher)) {
Write-Host " [ERROR] Publisher cannot be empty." -ForegroundColor Red
Write-Host ""
}
elseif (-not (Test-PublisherFormat $packageIdentityPublisher)) {
if ($packageIdentityPublisher -notmatch '^CN=') {
Write-Host " [ERROR] Publisher must start with 'CN='." -ForegroundColor Red
Write-Host " Copy the entire string from Partner Center, including 'CN='." -ForegroundColor Yellow
}
else {
Write-Host " [ERROR] Invalid publisher format." -ForegroundColor Red
Write-Host " Please ensure you copied the complete certificate name from Partner Center." -ForegroundColor Yellow
}
Write-Host ""
}
else {
Write-Host " [OK] Publisher certificate accepted" -ForegroundColor Green
Write-Host ""
break
}
if ($attempt -ge $maxAttempts) {
Write-Host ""
Write-Host "Maximum attempts reached. Please verify your Partner Center information and try again." -ForegroundColor Red
Write-Host "Press any key to exit..." -ForegroundColor Gray
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
exit 1
}
} while ($true)
# Collect Reserved Display Name
Write-Host "================================================================" -ForegroundColor Cyan
Write-Host " Step 3: Display Name" -ForegroundColor Cyan
Write-Host "================================================================" -ForegroundColor Cyan
Write-Host ""
Write-Host "Enter the reserved Display Name from Partner Center:" -ForegroundColor Yellow
Write-Host " Location: Partner Center > Product Management > Store Listing" -ForegroundColor Gray
Write-Host ""
Write-Host " This is the app name visible to users in the Microsoft Store." -ForegroundColor Gray
Write-Host " It must match EXACTLY what you reserved in Partner Center." -ForegroundColor Gray
Write-Host ""
Write-Host " Example: My Awesome CmdPal Extension" -ForegroundColor DarkGray
Write-Host ""
$displayName = ""
$attempt = 0
do {
$attempt++
Write-Host "Display Name" -NoNewline -ForegroundColor Yellow
if ($attempt -gt 1) {
Write-Host " (Attempt $attempt of $maxAttempts)" -NoNewline -ForegroundColor Red
}
Write-Host ": " -NoNewline -ForegroundColor Yellow
$displayName = Read-Host
if ([string]::IsNullOrWhiteSpace($displayName)) {
Write-Host " [ERROR] Display Name cannot be empty." -ForegroundColor Red
Write-Host ""
}
elseif (-not (Test-DisplayName $displayName)) {
Write-Host " [ERROR] Invalid Display Name." -ForegroundColor Red
Write-Host " Display name must be 1-256 characters and cannot contain control characters." -ForegroundColor Yellow
Write-Host ""
}
else {
Write-Host " [OK] Display Name accepted: $displayName" -ForegroundColor Green
Write-Host ""
break
}
if ($attempt -ge $maxAttempts) {
Write-Host ""
Write-Host "Maximum attempts reached. Please try again with valid information." -ForegroundColor Red
Write-Host "Press any key to exit..." -ForegroundColor Gray
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
exit 1
}
} while ($true)
# Collect Publisher Display Name
Write-Host "================================================================" -ForegroundColor Cyan
Write-Host " Step 4: Publisher Display Name" -ForegroundColor Cyan
Write-Host "================================================================" -ForegroundColor Cyan
Write-Host ""
Write-Host "Enter Microsoft Store Package/Properties/PublisherDisplayName:" -ForegroundColor Yellow
Write-Host " Location: Partner Center > Product Identity > Package/Properties" -ForegroundColor Gray
Write-Host ""
Write-Host " This is your company or developer name shown to users." -ForegroundColor Gray
Write-Host ""
Write-Host " Example: Contoso Software Inc." -ForegroundColor DarkGray
Write-Host " Example: Jessica Cha" -ForegroundColor DarkGray
Write-Host ""
$publisherDisplayName = ""
$attempt = 0
do {
$attempt++
Write-Host "Publisher Display Name" -NoNewline -ForegroundColor Yellow
if ($attempt -gt 1) {
Write-Host " (Attempt $attempt of $maxAttempts)" -NoNewline -ForegroundColor Red
}
Write-Host ": " -NoNewline -ForegroundColor Yellow
$publisherDisplayName = Read-Host
if ([string]::IsNullOrWhiteSpace($publisherDisplayName)) {
Write-Host " [ERROR] Publisher Display Name cannot be empty." -ForegroundColor Red
Write-Host ""
}
elseif (-not (Test-DisplayName $publisherDisplayName)) {
Write-Host " [ERROR] Invalid Publisher Display Name." -ForegroundColor Red
Write-Host " Publisher name must be 1-256 characters and cannot contain control characters." -ForegroundColor Yellow
Write-Host ""
}
else {
Write-Host " [OK] Publisher Display Name accepted: $publisherDisplayName" -ForegroundColor Green
Write-Host ""
break
}
if ($attempt -ge $maxAttempts) {
Write-Host ""
Write-Host "Maximum attempts reached. Please try again with valid information." -ForegroundColor Red
Write-Host "Press any key to exit..." -ForegroundColor Gray
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
exit 1
}
} while ($true)
# Check for required assets
Write-Host "================================================================" -ForegroundColor Cyan
Write-Host " Step 5: Validating Required Assets" -ForegroundColor Cyan
Write-Host "================================================================" -ForegroundColor Cyan
Write-Host ""
Write-Host "Checking for Microsoft Store required asset images..." -ForegroundColor Yellow
Write-Host ""
$assetsPath = Join-Path $projectRoot "Assets"
# Check if Assets folder exists
if (-not (Test-Path $assetsPath)) {
Write-Host " [ERROR] Assets folder not found at: $assetsPath" -ForegroundColor Red
Write-Host ""
Write-Host " Please create the Assets folder and add the required images." -ForegroundColor Yellow
Write-Host " Press any key to continue anyway (you'll need to add assets later)..." -ForegroundColor Gray
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
Write-Host ""
}
$requiredAssets = @(
@{ Name = "StoreLogo.png"; Size = "50x50"; Description = "Store logo for listings" },
@{ Name = "Square150x150Logo.scale-200.png"; Size = "300x300"; Description = "Medium tile" },
@{ Name = "Square44x44Logo.scale-200.png"; Size = "88x88"; Description = "App list icon" },
@{ Name = "Wide310x150Logo.scale-200.png"; Size = "620x300"; Description = "Wide tile" },
@{ Name = "SplashScreen.scale-200.png"; Size = "1240x600"; Description = "Splash screen" },
@{ Name = "StoreLogo.scale-100.png"; Size = "50x50"; Description = "Store logo (100% scale)" }
)
$missingAssets = @()
$foundAssets = @()
Write-Host " Asset Validation Results:" -ForegroundColor Cyan
Write-Host " " -NoNewline
Write-Host ("{0,-45} {1,-15} {2}" -f "File", "Size", "Status") -ForegroundColor Gray
Write-Host " " -NoNewline
Write-Host ("{0,-45} {1,-15} {2}" -f "----", "----", "------") -ForegroundColor DarkGray
foreach ($asset in $requiredAssets) {
$assetPath = Join-Path $assetsPath $asset.Name
$statusPrefix = " "
if (Test-Path $assetPath) {
try {
$fileInfo = Get-Item $assetPath
$fileSize = "{0:N2} KB" -f ($fileInfo.Length / 1KB)
Write-Host " " -NoNewline
Write-Host ("{0,-45} {1,-15} " -f $asset.Name, $asset.Size) -NoNewline -ForegroundColor White
Write-Host "[OK]" -ForegroundColor Green
$foundAssets += $asset.Name
}
catch {
Write-Host " " -NoNewline
Write-Host ("{0,-45} {1,-15} " -f $asset.Name, $asset.Size) -NoNewline -ForegroundColor White
Write-Host "[WARNING]" -ForegroundColor Yellow
Write-Host " (File exists but couldn't read properties)" -ForegroundColor DarkGray
$foundAssets += $asset.Name
}
}
else {
Write-Host " " -NoNewline
Write-Host ("{0,-45} {1,-15} " -f $asset.Name, $asset.Size) -NoNewline -ForegroundColor White
Write-Host "[MISSING]" -ForegroundColor Red
Write-Host " ($($asset.Description))" -ForegroundColor DarkGray
$missingAssets += $asset
}
}
Write-Host ""
Write-Host " Summary: " -NoNewline -ForegroundColor Cyan
Write-Host "$($foundAssets.Count) of $($requiredAssets.Count) assets found" -ForegroundColor White
# Auto-fix: Copy StoreLogo.scale-100.png to StoreLogo.png if needed
$storeLogoPath = Join-Path $assetsPath "StoreLogo.png"
$storeLogoScaledPath = Join-Path $assetsPath "StoreLogo.scale-100.png"
if (-not (Test-Path $storeLogoPath) -and (Test-Path $storeLogoScaledPath)) {
Write-Host ""
Write-Host " [AUTO-FIX] Creating StoreLogo.png from StoreLogo.scale-100.png..." -ForegroundColor Cyan
try {
Copy-Item $storeLogoScaledPath -Destination $storeLogoPath -Force -ErrorAction Stop
Write-Host " [SUCCESS] StoreLogo.png created successfully" -ForegroundColor Green
# Update the missing/found counts
$missingAssets = $missingAssets | Where-Object { $_.Name -ne "StoreLogo.png" }
if ($foundAssets -notcontains "StoreLogo.png") {
$foundAssets += "StoreLogo.png"
}
}
catch {
Write-Host " [ERROR] Could not copy file: $($_.Exception.Message)" -ForegroundColor Red
}
}
if ($missingAssets.Count -gt 0) {
Write-Host ""
Write-Host " [WARNING] $($missingAssets.Count) asset(s) missing" -ForegroundColor Yellow
Write-Host ""
Write-Host " The Microsoft Store requires specific image assets for your app listing." -ForegroundColor Gray
Write-Host " You'll need to add these before you can publish." -ForegroundColor Gray
Write-Host ""
Write-Host " TIP: Use the Windows App SDK project templates or design tools to create" -ForegroundColor Cyan
Write-Host " properly sized assets. Each image must be exactly the size specified." -ForegroundColor Cyan
Write-Host ""
}
else {
Write-Host " [OK] All required assets are present!" -ForegroundColor Green
Write-Host ""
}
Write-Host ""
Write-Host "================================================================" -ForegroundColor Cyan
Write-Host " Updating Project Files..." -ForegroundColor Cyan
Write-Host "================================================================" -ForegroundColor Cyan
Write-Host ""
Write-Host "The script will now update your project files with the information you provided." -ForegroundColor White
Write-Host "Original files have been backed up in the Publication\Backups folder." -ForegroundColor Gray
Write-Host ""
# Update Package.appxmanifest
Write-Host "[1/2] Updating Package.appxmanifest..." -ForegroundColor Cyan
try {
$manifestContent = Get-Content $manifestPath -Raw -ErrorAction Stop
}
catch {
Write-Host " [ERROR] Could not read Package.appxmanifest: $($_.Exception.Message)" -ForegroundColor Red
Write-Host " The file may be locked by another process." -ForegroundColor Yellow
Write-Host " Press any key to exit..." -ForegroundColor Gray
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
exit 1
}
# Backup original content
$manifestBackup = $manifestContent
$manifestUpdateCount = 0
# Update Identity element (Name and Publisher)
Write-Host " Updating Identity Name..." -NoNewline -ForegroundColor Gray
$identityNamePattern = '(?<=<Identity\s+Name=")[^"]*'
if ($manifestContent -match $identityNamePattern) {
$oldValue = $Matches[0]
$identityNameUpdated = $manifestContent -replace $identityNamePattern, $packageIdentityName
if ($identityNameUpdated -ne $manifestContent) {
$manifestContent = $identityNameUpdated
$manifestUpdateCount++
Write-Host " [OK]" -ForegroundColor Green
Write-Host " Changed from: '$oldValue'" -ForegroundColor DarkGray
Write-Host " Changed to: '$packageIdentityName'" -ForegroundColor DarkGray
}
else {
Write-Host " [NO CHANGE]" -ForegroundColor Yellow
}
}
else {
Write-Host " [WARNING] Could not find Identity Name attribute" -ForegroundColor Yellow
}
Write-Host " Updating Publisher..." -NoNewline -ForegroundColor Gray
$publisherPattern = '(?<=Publisher=")[^"]*(?=")'
if ($manifestContent -match $publisherPattern) {
$oldValue = $Matches[0]
$identityPublisherUpdated = $manifestContent -replace $publisherPattern, $packageIdentityPublisher
if ($identityPublisherUpdated -ne $manifestContent) {
$manifestContent = $identityPublisherUpdated
$manifestUpdateCount++
Write-Host " [OK]" -ForegroundColor Green
Write-Host " Changed from: '$oldValue'" -ForegroundColor DarkGray
Write-Host " Changed to: '$packageIdentityPublisher'" -ForegroundColor DarkGray
}
else {
Write-Host " [NO CHANGE]" -ForegroundColor Yellow
}
}
else {
Write-Host " [WARNING] Could not find Publisher attribute" -ForegroundColor Yellow
}
# Update Properties (DisplayName and PublisherDisplayName)
Write-Host " Updating Display Name..." -NoNewline -ForegroundColor Gray
$displayNamePattern = '(?<=<DisplayName>)[^<]*(?=</DisplayName>)'
if ($manifestContent -match $displayNamePattern) {
$oldValue = $Matches[0]
$displayNameUpdated = $manifestContent -replace $displayNamePattern, $displayName
if ($displayNameUpdated -ne $manifestContent) {
$manifestContent = $displayNameUpdated
$manifestUpdateCount++
Write-Host " [OK]" -ForegroundColor Green
Write-Host " Changed from: '$oldValue'" -ForegroundColor DarkGray
Write-Host " Changed to: '$displayName'" -ForegroundColor DarkGray
}
else {
Write-Host " [NO CHANGE]" -ForegroundColor Yellow
}
}
else {
Write-Host " [WARNING] Could not find DisplayName element" -ForegroundColor Yellow
}
Write-Host " Updating Publisher Display Name..." -NoNewline -ForegroundColor Gray
$publisherDisplayNamePattern = '(?<=<PublisherDisplayName>)[^<]*(?=</PublisherDisplayName>)'
if ($manifestContent -match $publisherDisplayNamePattern) {
$oldValue = $Matches[0]
$publisherDisplayNameUpdated = $manifestContent -replace $publisherDisplayNamePattern, $publisherDisplayName
if ($publisherDisplayNameUpdated -ne $manifestContent) {
$manifestContent = $publisherDisplayNameUpdated
$manifestUpdateCount++
Write-Host " [OK]" -ForegroundColor Green
Write-Host " Changed from: '$oldValue'" -ForegroundColor DarkGray
Write-Host " Changed to: '$publisherDisplayName'" -ForegroundColor DarkGray
}
else {
Write-Host " [NO CHANGE]" -ForegroundColor Yellow
}
}
else {
Write-Host " [WARNING] Could not find PublisherDisplayName element" -ForegroundColor Yellow
}
# Also update the VisualElements DisplayName
Write-Host " Updating VisualElements Display Name..." -NoNewline -ForegroundColor Gray
$visualElementsPattern = '(?<=<uap:VisualElements[^>]*DisplayName=")[^"]*'
if ($manifestContent -match $visualElementsPattern) {
$oldValue = $Matches[0]
if ($oldValue -ne $displayName) {
$visualElementsUpdated = $manifestContent -replace $visualElementsPattern, $displayName
if ($visualElementsUpdated -ne $manifestContent) {
$manifestContent = $visualElementsUpdated
$manifestUpdateCount++
Write-Host " [OK]" -ForegroundColor Green
}
else {
Write-Host " [NO CHANGE]" -ForegroundColor Yellow
}
}
else {
Write-Host " [ALREADY SET]" -ForegroundColor Green
}
}
else {
Write-Host " [SKIP]" -ForegroundColor Gray
}
# Write the updated manifest
if ($manifestContent -ne $manifestBackup) {
try {
Set-Content -Path $manifestPath -Value $manifestContent -NoNewline -ErrorAction Stop
Write-Host ""
Write-Host " [SUCCESS] Package.appxmanifest updated ($manifestUpdateCount changes)" -ForegroundColor Green
}
catch {
Write-Host ""
Write-Host " [ERROR] Could not write to Package.appxmanifest: $($_.Exception.Message)" -ForegroundColor Red
Write-Host " The file may be read-only or locked by another process." -ForegroundColor Yellow
Write-Host " Press any key to exit..." -ForegroundColor Gray
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
exit 1
}
}
else {
Write-Host ""
Write-Host " [INFO] No changes were needed for Package.appxmanifest" -ForegroundColor Cyan
}
Write-Host ""
# Update .csproj file
Write-Host "[2/2] Updating TemplateCmdPalExtension.csproj..." -ForegroundColor Cyan
try {
$csprojContent = Get-Content $csprojPath -Raw -ErrorAction Stop
}
catch {
Write-Host " [ERROR] Could not read .csproj file: $($_.Exception.Message)" -ForegroundColor Red
Write-Host " The file may be locked by another process." -ForegroundColor Yellow
Write-Host " Press any key to exit..." -ForegroundColor Gray
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
exit 1
}
# Backup original content
$csprojBackup = $csprojContent
$csprojUpdateCount = 0
# Check if Store properties are commented or uncommented
Write-Host " Checking Store property configuration..." -NoNewline -ForegroundColor Gray
$storePropsCommentedPattern = '<!--\s*<AppxPackageIdentityName>YOUR_PACKAGE_IDENTITY_NAME_HERE</AppxPackageIdentityName>\s*<AppxPackagePublisher>YOUR_PACKAGE_IDENTITY_PUBLISHER_HERE</AppxPackagePublisher>\s*<AppxPackageVersion>[^<]*</AppxPackageVersion>\s*-->'
if ($csprojContent -match $storePropsCommentedPattern) {
Write-Host " [COMMENTED]" -ForegroundColor Yellow
Write-Host " Uncommenting and updating Store properties..." -ForegroundColor Gray
# Uncomment and update the Store-specific properties
$replacement = "<AppxPackageIdentityName>$packageIdentityName</AppxPackageIdentityName>`n <AppxPackagePublisher>$packageIdentityPublisher</AppxPackagePublisher>`n <AppxPackageVersion>0.0.1.0</AppxPackageVersion>"
$csprojContent = $csprojContent -replace $storePropsCommentedPattern, $replacement
$csprojUpdateCount++
Write-Host " [OK] Store properties uncommented and updated" -ForegroundColor Green
}
else {
Write-Host " [UNCOMMENTED]" -ForegroundColor Green
Write-Host " Updating existing Store property values..." -ForegroundColor Gray
# Try updating already-uncommented properties
$identityNamePattern = '(?<=<AppxPackageIdentityName>)[^<]*(?=</AppxPackageIdentityName>)'
if ($csprojContent -match $identityNamePattern) {
$oldValue = $Matches[0]
if ($oldValue -ne $packageIdentityName) {
$csprojContent = $csprojContent -replace $identityNamePattern, $packageIdentityName
$csprojUpdateCount++
Write-Host " Updated AppxPackageIdentityName" -ForegroundColor Green
}
}
$publisherPattern = '(?<=<AppxPackagePublisher>)[^<]*(?=</AppxPackagePublisher>)'
if ($csprojContent -match $publisherPattern) {
$oldValue = $Matches[0]
if ($oldValue -ne $packageIdentityPublisher) {
$csprojContent = $csprojContent -replace $publisherPattern, $packageIdentityPublisher
$csprojUpdateCount++
Write-Host " Updated AppxPackagePublisher" -ForegroundColor Green
}
}
}
# Uncomment the PrepareAssets Target section (using (?s) for multi-line matching)
Write-Host " Checking PrepareAssets Target..." -NoNewline -ForegroundColor Gray
$targetPattern = '(?s)<!--\s*(<Target Name="PrepareAssets".*?</Target>)\s*-->'
if ($csprojContent -match $targetPattern) {
Write-Host " [COMMENTED]" -ForegroundColor Yellow
Write-Host " Uncommenting PrepareAssets Target..." -ForegroundColor Gray
$targetReplacement = '$1'
$targetUpdated = $csprojContent -replace $targetPattern, $targetReplacement
if ($targetUpdated -ne $csprojContent) {
$csprojContent = $targetUpdated
$csprojUpdateCount++
Write-Host " [OK] PrepareAssets Target uncommented" -ForegroundColor Green
}
else {
Write-Host " [WARNING] Could not uncomment PrepareAssets Target" -ForegroundColor Yellow
}
}
else {
Write-Host " [ALREADY UNCOMMENTED]" -ForegroundColor Green
}
# Write the updated csproj
if ($csprojContent -ne $csprojBackup) {
try {
Set-Content -Path $csprojPath -Value $csprojContent -NoNewline -ErrorAction Stop
Write-Host ""
Write-Host " [SUCCESS] TemplateCmdPalExtension.csproj updated ($csprojUpdateCount changes)" -ForegroundColor Green
}
catch {
Write-Host ""
Write-Host " [ERROR] Could not write to .csproj file: $($_.Exception.Message)" -ForegroundColor Red
Write-Host " The file may be read-only or locked by another process." -ForegroundColor Yellow
Write-Host " Press any key to exit..." -ForegroundColor Gray
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
exit 1
}
}
else {
Write-Host ""
Write-Host " [INFO] No changes were needed for TemplateCmdPalExtension.csproj" -ForegroundColor Cyan
}
Write-Host ""
# Display summary
Write-Host "=================================================================" -ForegroundColor Cyan
Write-Host " CONFIGURATION SUMMARY" -ForegroundColor White
Write-Host "=================================================================" -ForegroundColor Cyan
Write-Host " Package Identity Name:" -ForegroundColor Gray
Write-Host " $packageIdentityName" -ForegroundColor White
Write-Host ""
Write-Host " Publisher:" -ForegroundColor Gray
# Truncate publisher if too long
$publisherDisplay = if ($packageIdentityPublisher.Length -gt 80) {
$packageIdentityPublisher.Substring(0, 77) + "..."
} else {
$packageIdentityPublisher
}
Write-Host " $publisherDisplay" -ForegroundColor White
Write-Host ""
Write-Host " Display Name:" -ForegroundColor Gray
Write-Host " $displayName" -ForegroundColor White
Write-Host ""
Write-Host " Publisher Display Name:" -ForegroundColor Gray
Write-Host " $publisherDisplayName" -ForegroundColor White
Write-Host "=================================================================" -ForegroundColor Cyan
Write-Host ""
# Display modified files
Write-Host "Modified Files:" -ForegroundColor Yellow
$manifestRelative = $manifestPath -replace [regex]::Escape($projectRoot), "."
$csprojRelative = $csprojPath -replace [regex]::Escape($projectRoot), "."
Write-Host "$manifestRelative" -ForegroundColor Green
Write-Host "$csprojRelative" -ForegroundColor Green
Write-Host ""
if ($backupDir) {
Write-Host "Backup Location:" -ForegroundColor Yellow
$backupRelative = $backupDir -replace [regex]::Escape($projectRoot), "."
Write-Host " $backupRelative" -ForegroundColor Gray
Write-Host ""
}
# Asset status
if ($missingAssets.Count -gt 0) {
Write-Host "=================================================================" -ForegroundColor Red
Write-Host " ACTION REQUIRED: Missing Assets" -ForegroundColor Yellow
Write-Host "=================================================================" -ForegroundColor Red
Write-Host " $($missingAssets.Count) required asset(s) are missing. Add them before publishing:" -ForegroundColor White
Write-Host ""
foreach ($asset in $missingAssets) {
Write-Host " * $($asset.Name) ($($asset.Size))" -ForegroundColor White
}
Write-Host "=================================================================" -ForegroundColor Red
Write-Host ""
Write-Host "Asset Creation Tips:" -ForegroundColor Cyan
Write-Host " * Use PNG format with transparency where appropriate" -ForegroundColor Gray
Write-Host " * Follow Microsoft Store asset guidelines" -ForegroundColor Gray
Write-Host " * Reference: https://learn.microsoft.com/windows/apps/design/style/app-icons-and-logos" -ForegroundColor DarkCyan
Write-Host ""
}
else {
Write-Host " [OK] All required assets are present" -ForegroundColor Green
Write-Host ""
}
# Final success message with conditional messaging
Write-Host "=================================================================" -ForegroundColor Green
if ($missingAssets.Count -gt 0) {
Write-Host " Setup Complete - Action Required" -ForegroundColor Yellow
Write-Host "=================================================================" -ForegroundColor Yellow
Write-Host ""
Write-Host " Your project has been configured for Microsoft Store publishing." -ForegroundColor White
Write-Host " However, you need to add $($missingAssets.Count) missing asset(s) before publishing." -ForegroundColor Yellow
}
else {
Write-Host " Setup Completed Successfully!" -ForegroundColor Green
Write-Host "=================================================================" -ForegroundColor Green
Write-Host ""
Write-Host " Your extension is ready for Microsoft Store publishing!" -ForegroundColor White
Write-Host " All configuration and assets are in place." -ForegroundColor Green
}
Write-Host ""
Write-Host "Next Steps:" -ForegroundColor Cyan
Write-Host " 1. Build MSIX bundles by running:" -ForegroundColor Gray
Write-Host " .\build-msix-bundles.ps1" -ForegroundColor White
Write-Host ""
Write-Host " 2. Upload the bundle to Microsoft Store Partner Center" -ForegroundColor Gray
Write-Host " (Located in Publication\ folder after build)" -ForegroundColor DarkGray
Write-Host ""
Write-Host " 3. Follow submission instructions at:" -ForegroundColor Gray
Write-Host " https://learn.microsoft.com/windows/powertoys/command-palette/publish-extension" -ForegroundColor DarkCyan
Write-Host ""
Write-Host "Press any key to exit..." -ForegroundColor Gray
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")

View File

@@ -1,512 +0,0 @@
# One-Time WinGet Publication Setup Script for CmdPal Extension
# This script collects information and updates files needed for WinGet publication via EXE installer
# Version: 1.0
#Requires -Version 5.1
# Enable strict mode for better error detection
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
Write-Host "================================================================" -ForegroundColor Cyan
Write-Host " WinGet Publication Setup (EXE Installer)" -ForegroundColor Cyan
Write-Host " CmdPal Extension Publisher" -ForegroundColor Cyan
Write-Host "================================================================" -ForegroundColor Cyan
Write-Host ""
# Path to the project files
$publicationRoot = $PSScriptRoot
$projectRoot = Split-Path -Parent $publicationRoot
$projectName = Split-Path -Leaf $projectRoot
$wingetResourcesPath = Join-Path $PSScriptRoot "winget-resources"
Write-Host "Validating project structure..." -ForegroundColor Cyan
Write-Host " Publication Root: $publicationRoot" -ForegroundColor Gray
Write-Host " Project Root: $projectRoot" -ForegroundColor Gray
Write-Host " Project Name: $projectName" -ForegroundColor Gray
Write-Host ""
# Verify required files exist
$csprojPath = Join-Path $projectRoot "$projectName.csproj"
$manifestPath = Join-Path $projectRoot "Package.appxmanifest"
$extensionCsPath = Join-Path $projectRoot "$projectName.cs"
$buildExePath = Join-Path $wingetResourcesPath "build-exe.ps1"
$setupTemplatePath = Join-Path $wingetResourcesPath "setup-template.iss"
$releaseYmlPath = Join-Path $wingetResourcesPath "release-extension.yml"
if (-not (Test-Path $csprojPath)) {
Write-Host "ERROR: Could not find .csproj file at: $csprojPath" -ForegroundColor Red
Write-Host "Press any key to exit..." -ForegroundColor Gray
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
exit 1
}
if (-not (Test-Path $manifestPath)) {
Write-Host "ERROR: Could not find Package.appxmanifest at: $manifestPath" -ForegroundColor Red
Write-Host "Press any key to exit..." -ForegroundColor Gray
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
exit 1
}
if (-not (Test-Path $buildExePath)) {
Write-Host "ERROR: Could not find build-exe.ps1 at: $buildExePath" -ForegroundColor Red
Write-Host "Press any key to exit..." -ForegroundColor Gray
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
exit 1
}
if (-not (Test-Path $setupTemplatePath)) {
Write-Host "ERROR: Could not find setup-template.iss at: $setupTemplatePath" -ForegroundColor Red
Write-Host "Press any key to exit..." -ForegroundColor Gray
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
exit 1
}
if (-not (Test-Path $releaseYmlPath)) {
Write-Host "ERROR: Could not find release-extension.yml at: $releaseYmlPath" -ForegroundColor Red
Write-Host "Press any key to exit..." -ForegroundColor Gray
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
exit 1
}
Write-Host " [OK] All required files found" -ForegroundColor Green
Write-Host ""
# Create backup directory
$backupDir = Join-Path $wingetResourcesPath "Backups"
if (-not (Test-Path $backupDir)) {
try {
New-Item -Path $backupDir -ItemType Directory -Force | Out-Null
Write-Host "Created backup directory: $backupDir" -ForegroundColor Gray
}
catch {
Write-Host "WARNING: Could not create backup directory. Proceeding without backups." -ForegroundColor Yellow
$backupDir = $null
}
}
# Create timestamped backups
if ($backupDir) {
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
try {
Copy-Item $buildExePath -Destination (Join-Path $backupDir "build-exe.ps1.$timestamp.bak") -Force
Copy-Item $setupTemplatePath -Destination (Join-Path $backupDir "setup-template.iss.$timestamp.bak") -Force
Copy-Item $releaseYmlPath -Destination (Join-Path $backupDir "release-extension.yml.$timestamp.bak") -Force
Write-Host "Backups created: $timestamp" -ForegroundColor Gray
Write-Host ""
}
catch {
Write-Host "WARNING: Could not create backup files. Proceeding anyway." -ForegroundColor Yellow
Write-Host ""
}
}
# Read existing project information
Write-Host "Reading project information..." -ForegroundColor Cyan
try {
[xml]$manifest = Get-Content $manifestPath -ErrorAction Stop
$packageName = $manifest.Package.Identity.Name
$packageVersion = $manifest.Package.Identity.Version
$displayName = $manifest.Package.Properties.DisplayName
$publisherDisplayName = $manifest.Package.Properties.PublisherDisplayName
Write-Host " Current Package Name: $packageName" -ForegroundColor White
Write-Host " Current Version: $packageVersion" -ForegroundColor White
Write-Host " Current Display Name: $displayName" -ForegroundColor White
Write-Host " Current Publisher: $publisherDisplayName" -ForegroundColor White
Write-Host ""
}
catch {
Write-Host "ERROR: Could not read Package.appxmanifest: $($_.Exception.Message)" -ForegroundColor Red
Write-Host "Press any key to exit..." -ForegroundColor Gray
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
exit 1
}
# Extract GUID/CLSID from extension class
Write-Host "Reading extension GUID..." -ForegroundColor Cyan
try {
$extensionCsContent = Get-Content $extensionCsPath -Raw -ErrorAction Stop
if ($extensionCsContent -match '\[Guid\("([A-F0-9-]+)"\)\]') {
$extensionGuid = $Matches[1]
Write-Host " Extension GUID: $extensionGuid" -ForegroundColor White
Write-Host ""
}
else {
Write-Host "ERROR: Could not find GUID in $projectName.cs" -ForegroundColor Red
Write-Host "Press any key to exit..." -ForegroundColor Gray
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
exit 1
}
}
catch {
Write-Host "ERROR: Could not read $projectName.cs: $($_.Exception.Message)" -ForegroundColor Red
Write-Host "Press any key to exit..." -ForegroundColor Gray
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
exit 1
}
Write-Host "This script will configure your extension for WinGet publication using EXE installer." -ForegroundColor White
Write-Host ""
Write-Host "The following information will be collected:" -ForegroundColor White
Write-Host " - GitHub Repository URL (for releases)" -ForegroundColor Gray
Write-Host " - Developer/Publisher Name" -ForegroundColor Gray
Write-Host ""
Write-Host "The script will update:" -ForegroundColor Yellow
Write-Host " - build-exe.ps1 (build script)" -ForegroundColor Gray
Write-Host " - setup-template.iss (Inno Setup installer script)" -ForegroundColor Gray
Write-Host " - release-extension.yml (GitHub Actions workflow)" -ForegroundColor Gray
Write-Host ""
# Function to validate URL
function Test-GitHubUrl {
param([string]$url)
if ([string]::IsNullOrWhiteSpace($url)) { return $false }
if ($url -notmatch '^https://github\.com/[a-zA-Z0-9_-]+/[a-zA-Z0-9_.-]+/?$') { return $false }
return $true
}
# Function to validate developer name
function Test-DeveloperName {
param([string]$name)
if ([string]::IsNullOrWhiteSpace($name)) { return $false }
if ($name.Length -lt 1 -or $name.Length -gt 256) { return $false }
return $true
}
# Prompt to continue
Write-Host "Do you want to continue? (Y/N): " -ForegroundColor Yellow -NoNewline
$continue = Read-Host
if ($continue -notmatch '^[Yy]') {
Write-Host ""
Write-Host "Setup cancelled by user." -ForegroundColor Yellow
Write-Host "Press any key to exit..." -ForegroundColor Gray
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
exit 0
}
Write-Host ""
# Collect GitHub Repository URL
Write-Host "================================================================" -ForegroundColor Cyan
Write-Host " Step 1: GitHub Repository URL" -ForegroundColor Cyan
Write-Host "================================================================" -ForegroundColor Cyan
Write-Host ""
Write-Host "Enter your GitHub repository URL:" -ForegroundColor Yellow
Write-Host " This is where your extension's releases will be published." -ForegroundColor Gray
Write-Host ""
Write-Host " Format: https://github.com/username/repository" -ForegroundColor DarkGray
Write-Host " Example: https://github.com/johndoe/MyAwesomeExtension" -ForegroundColor DarkGray
Write-Host ""
$githubRepoUrl = ""
$maxAttempts = 3
$attempt = 0
do {
$attempt++
Write-Host "GitHub Repository URL" -NoNewline -ForegroundColor Yellow
if ($attempt -gt 1) {
Write-Host " (Attempt $attempt of $maxAttempts)" -NoNewline -ForegroundColor Red
}
Write-Host ": " -NoNewline -ForegroundColor Yellow
$githubRepoUrl = Read-Host
if ([string]::IsNullOrWhiteSpace($githubRepoUrl)) {
Write-Host " [ERROR] GitHub Repository URL cannot be empty." -ForegroundColor Red
Write-Host ""
}
elseif (-not (Test-GitHubUrl $githubRepoUrl)) {
Write-Host " [ERROR] Invalid GitHub URL format." -ForegroundColor Red
Write-Host " Please use format: https://github.com/username/repository" -ForegroundColor Yellow
Write-Host ""
}
else {
Write-Host " [OK] GitHub Repository URL accepted: $githubRepoUrl" -ForegroundColor Green
Write-Host ""
break
}
if ($attempt -ge $maxAttempts) {
Write-Host ""
Write-Host "Maximum attempts reached. Please try again with a valid GitHub URL." -ForegroundColor Red
Write-Host "Press any key to exit..." -ForegroundColor Gray
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
exit 1
}
} while ($true)
# Collect Developer Name
Write-Host "================================================================" -ForegroundColor Cyan
Write-Host " Step 2: Developer/Publisher Name" -ForegroundColor Cyan
Write-Host "================================================================" -ForegroundColor Cyan
Write-Host ""
Write-Host "Enter your developer or publisher name:" -ForegroundColor Yellow
Write-Host " This will appear in the EXE installer as the publisher." -ForegroundColor Gray
Write-Host ""
Write-Host " IMPORTANT: If you published to Microsoft Store, this should match" -ForegroundColor Yellow
Write-Host " the PublisherDisplayName from your Store configuration." -ForegroundColor Yellow
Write-Host ""
Write-Host " Example: John Doe" -ForegroundColor DarkGray
Write-Host " Example: Contoso Software" -ForegroundColor DarkGray
Write-Host ""
Write-Host " Current value from manifest: $publisherDisplayName" -ForegroundColor Cyan
Write-Host ""
$developerName = ""
$attempt = 0
do {
$attempt++
Write-Host "Developer Name" -NoNewline -ForegroundColor Yellow
if ($attempt -gt 1) {
Write-Host " (Attempt $attempt of $maxAttempts)" -NoNewline -ForegroundColor Red
}
Write-Host " [press Enter to use default]: " -NoNewline -ForegroundColor Yellow
$input = Read-Host
if ([string]::IsNullOrWhiteSpace($input)) {
$developerName = $publisherDisplayName
Write-Host " [OK] Using default: $developerName" -ForegroundColor Green
Write-Host ""
break
}
elseif (-not (Test-DeveloperName $input)) {
Write-Host " [ERROR] Invalid developer name." -ForegroundColor Red
Write-Host ""
}
else {
$developerName = $input
Write-Host " [OK] Developer name accepted: $developerName" -ForegroundColor Green
Write-Host ""
break
}
if ($attempt -ge $maxAttempts) {
Write-Host ""
Write-Host "Maximum attempts reached. Using default: $publisherDisplayName" -ForegroundColor Yellow
$developerName = $publisherDisplayName
Write-Host ""
break
}
} while ($true)
# Update files
Write-Host "================================================================" -ForegroundColor Cyan
Write-Host " Updating Configuration Files..." -ForegroundColor Cyan
Write-Host "================================================================" -ForegroundColor Cyan
Write-Host ""
# Update build-exe.ps1
Write-Host "[1/3] Updating build-exe.ps1..." -ForegroundColor Cyan
try {
$buildExeContent = Get-Content $buildExePath -Raw -ErrorAction Stop
# Update ExtensionName default value
$buildExeContent = $buildExeContent -replace '\[string\]\$ExtensionName = "UPDATE"', "[string]`$ExtensionName = `"$projectName`""
# Update Version default value
$buildExeContent = $buildExeContent -replace '\[string\]\$Version = "UPDATE"', "[string]`$Version = `"$packageVersion`""
Set-Content -Path $buildExePath -Value $buildExeContent -NoNewline -ErrorAction Stop
Write-Host " [SUCCESS] build-exe.ps1 updated" -ForegroundColor Green
Write-Host ""
}
catch {
Write-Host " [ERROR] Could not update build-exe.ps1: $($_.Exception.Message)" -ForegroundColor Red
Write-Host ""
}
# Update setup-template.iss
Write-Host "[2/3] Updating setup-template.iss..." -ForegroundColor Cyan
try {
$setupTemplateContent = Get-Content $setupTemplatePath -Raw -ErrorAction Stop
# Update version
$setupTemplateContent = $setupTemplateContent -replace '#define AppVersion ".*"', "#define AppVersion `"$packageVersion`""
# Update AppId GUID
$setupTemplateContent = $setupTemplateContent -replace 'AppId=\{\{GUID-HERE\}\}', "AppId={{$extensionGuid}}"
# Update AppName (DISPLAY_NAME)
$setupTemplateContent = $setupTemplateContent -replace 'AppName=DISPLAY_NAME', "AppName=$displayName"
# Update AppPublisher (DEVELOPER_NAME)
$setupTemplateContent = $setupTemplateContent -replace 'AppPublisher=DEVELOPER_NAME', "AppPublisher=$developerName"
# Update DefaultDirName (EXTENSION_NAME)
$setupTemplateContent = $setupTemplateContent -replace 'DefaultDirName=\{autopf\}\\EXTENSION_NAME', "DefaultDirName={autopf}\$projectName"
# Update OutputBaseFilename (EXTENSION_NAME)
$setupTemplateContent = $setupTemplateContent -replace 'OutputBaseFilename=EXTENSION_NAME-Setup', "OutputBaseFilename=$projectName-Setup"
# Update Icon name (DISPLAY_NAME)
$setupTemplateContent = $setupTemplateContent -replace 'Name: "\{group\}\\DISPLAY_NAME"', "Name: `"{group}\$displayName`""
# Update Icon filename (EXTENSION_NAME)
$setupTemplateContent = $setupTemplateContent -replace 'Filename: "\{app\}\\EXTENSION_NAME\.exe"', "Filename: `"{app}\$projectName.exe`""
# Update Registry CLSID entries
$setupTemplateContent = $setupTemplateContent -replace 'CLSID\\CLSID-HERE', "CLSID\{{$extensionGuid}}"
$setupTemplateContent = $setupTemplateContent -replace '\{\{CLSID-HERE\}\}', "{{$extensionGuid}}"
# Update Registry ValueData (EXTENSION_NAME)
$setupTemplateContent = $setupTemplateContent -replace 'ValueData: "EXTENSION_NAME"', "ValueData: `"$projectName`""
# Update LocalServer32 ValueData
$setupTemplateContent = $setupTemplateContent -replace 'ValueData: "\{app\}\\EXTENSION_NAME\.exe', "ValueData: `"{app}\$projectName.exe"
Set-Content -Path $setupTemplatePath -Value $setupTemplateContent -NoNewline -ErrorAction Stop
Write-Host " [SUCCESS] setup-template.iss updated" -ForegroundColor Green
Write-Host ""
}
catch {
Write-Host " [ERROR] Could not update setup-template.iss: $($_.Exception.Message)" -ForegroundColor Red
Write-Host ""
}
# Update release-extension.yml
Write-Host "[3/3] Updating release-extension.yml..." -ForegroundColor Cyan
try {
$releaseYmlContent = Get-Content $releaseYmlPath -Raw -ErrorAction Stop
# Update workflow name
$releaseYmlContent = $releaseYmlContent -replace 'name: CmdPal Extension - Build EXE Installer', "name: $displayName - Build EXE Installer"
# Update environment variables with actual values
$releaseYmlContent = $releaseYmlContent -replace "DISPLAY_NAME: \$\{\{ vars\.DISPLAY_NAME \|\| 'DISPLAY_NAME' \}\}", "DISPLAY_NAME: `${{ vars.DISPLAY_NAME || '$displayName' }}"
$releaseYmlContent = $releaseYmlContent -replace "EXTENSION_NAME: \$\{\{ vars\.EXTENSION_NAME \|\| 'EXTENSION_NAME' \}\}", "EXTENSION_NAME: `${{ vars.EXTENSION_NAME || '$projectName' }}"
$releaseYmlContent = $releaseYmlContent -replace "FOLDER_NAME: \$\{\{ vars\.FOLDER_NAME \|\| 'FOLDER_NAME' \}\}", "FOLDER_NAME: `${{ vars.FOLDER_NAME || '$projectName' }}"
$releaseYmlContent = $releaseYmlContent -replace "GITHUB_REPO_URL: \$\{\{ vars\.GITHUB_REPO_URL \|\| 'GITHUB_REPO_URL' \}\}", "GITHUB_REPO_URL: `${{ vars.GITHUB_REPO_URL || '$githubRepoUrl' }}"
Set-Content -Path $releaseYmlPath -Value $releaseYmlContent -NoNewline -ErrorAction Stop
Write-Host " [SUCCESS] release-extension.yml updated" -ForegroundColor Green
Write-Host ""
}
catch {
Write-Host " [ERROR] Could not update release-extension.yml: $($_.Exception.Message)" -ForegroundColor Red
Write-Host ""
}
# Display summary
Write-Host "================================================================" -ForegroundColor Green
Write-Host " CONFIGURATION SUMMARY" -ForegroundColor White
Write-Host "================================================================" -ForegroundColor Green
Write-Host " Extension Name:" -ForegroundColor Gray
Write-Host " $projectName" -ForegroundColor White
Write-Host ""
Write-Host " Display Name:" -ForegroundColor Gray
Write-Host " $displayName" -ForegroundColor White
Write-Host ""
Write-Host " Version:" -ForegroundColor Gray
Write-Host " $packageVersion" -ForegroundColor White
Write-Host ""
Write-Host " Developer:" -ForegroundColor Gray
Write-Host " $developerName" -ForegroundColor White
Write-Host ""
Write-Host " Extension GUID:" -ForegroundColor Gray
Write-Host " $extensionGuid" -ForegroundColor White
Write-Host ""
Write-Host " GitHub Repository:" -ForegroundColor Gray
Write-Host " $githubRepoUrl" -ForegroundColor White
Write-Host "================================================================" -ForegroundColor Green
Write-Host ""
# Display modified files
Write-Host "Updated Files:" -ForegroundColor Yellow
$buildExeRelative = $buildExePath -replace [regex]::Escape($projectRoot + "\"), ""
$setupTemplateRelative = $setupTemplatePath -replace [regex]::Escape($projectRoot + "\"), ""
$releaseYmlRelative = $releaseYmlPath -replace [regex]::Escape($projectRoot + "\"), ""
Write-Host " [OK] $buildExeRelative" -ForegroundColor Green
Write-Host " [OK] $setupTemplateRelative" -ForegroundColor Green
Write-Host " [OK] $releaseYmlRelative" -ForegroundColor Green
Write-Host ""
if ($backupDir) {
Write-Host "Backup Location:" -ForegroundColor Yellow
$backupRelative = $backupDir -replace [regex]::Escape($projectRoot + "\"), ""
Write-Host " $backupRelative" -ForegroundColor Gray
Write-Host ""
}
# Move files to correct locations
Write-Host "================================================================" -ForegroundColor Cyan
Write-Host " Moving Files to Correct Locations" -ForegroundColor Cyan
Write-Host "================================================================" -ForegroundColor Cyan
Write-Host ""
Write-Host "The following file will be moved:" -ForegroundColor Yellow
Write-Host " - release-extension.yml → .github/workflows/ (2 levels up)" -ForegroundColor Gray
Write-Host ""
Write-Host "The following files will remain in winget-resources:" -ForegroundColor Yellow
Write-Host " - build-exe.ps1" -ForegroundColor Gray
Write-Host " - setup-template.iss" -ForegroundColor Gray
Write-Host ""
# Calculate destination paths
# From: TemplateCmdPalExtension/Publication/winget-resources/
# release-extension.yml → TemplateCmdPalExtension/.github/workflows/ (2 levels up from Publication)
# GitHub workflows directory (2 levels up from Publication)
$solutionRoot = Split-Path -Parent $projectRoot
$githubWorkflowsDir = Join-Path $solutionRoot ".github\workflows"
$releaseYmlDestination = Join-Path $githubWorkflowsDir "release-extension.yml"
Write-Host "Destination:" -ForegroundColor Yellow
Write-Host " release-extension.yml → $releaseYmlDestination" -ForegroundColor Gray
Write-Host ""
# Move release-extension.yml
Write-Host "Moving release-extension.yml..." -ForegroundColor Cyan
try {
if (-not (Test-Path $githubWorkflowsDir)) {
Write-Host " Creating .github/workflows directory..." -ForegroundColor Gray
New-Item -Path $githubWorkflowsDir -ItemType Directory -Force | Out-Null
}
if (Test-Path $releaseYmlDestination) {
Write-Host " [WARNING] Destination file exists, overwriting..." -ForegroundColor Yellow
}
Move-Item $releaseYmlPath -Destination $releaseYmlDestination -Force -ErrorAction Stop
Write-Host " [SUCCESS] Moved to: $releaseYmlDestination" -ForegroundColor Green
}
catch {
Write-Host " [ERROR] Could not move release-extension.yml: $($_.Exception.Message)" -ForegroundColor Red
}
Write-Host ""
# Verify file was moved
Write-Host "Verifying file..." -ForegroundColor Cyan
if (Test-Path $releaseYmlDestination) {
Write-Host " [OK] release-extension.yml exists at destination" -ForegroundColor Green
}
else {
Write-Host " [ERROR] release-extension.yml NOT found at destination" -ForegroundColor Red
}
Write-Host ""
# Final instructions
Write-Host "================================================================" -ForegroundColor Green
Write-Host " Setup Completed Successfully!" -ForegroundColor Green
Write-Host "================================================================" -ForegroundColor Green
Write-Host ""
Write-Host "Files have been configured:" -ForegroundColor Yellow
Write-Host " Updated (in winget-resources):" -ForegroundColor Cyan
Write-Host " $buildExePath" -ForegroundColor White
Write-Host " $setupTemplatePath" -ForegroundColor White
Write-Host ""
Write-Host " Moved to GitHub workflows:" -ForegroundColor Cyan
Write-Host " $releaseYmlDestination" -ForegroundColor White
Write-Host ""
Write-Host "Next Steps:" -ForegroundColor Cyan
Write-Host " 1. Review the configured files to ensure correctness" -ForegroundColor Gray
Write-Host " 2. Add and commit files and push to Github" -ForegroundColor Gray
Write-Host " 3. Follow instructions at https://learn.microsoft.com//windows/powertoys/command-palette/publish-extension" -ForegroundColor Gray
Write-Host ""
Write-Host "Press any key to exit..." -ForegroundColor Gray
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")

View File

@@ -1,114 +0,0 @@
# TEMPLATE: PowerShell Build Script for Command Palette Extensions
#
# To use this template for a new extension:
# 1. Copy this file to your extension's project folder as "build-exe.ps1"
# 2. Update in param():
# - EXTENSION_NAME with your extension name (e.g., CmdPalMyExtension)
# - VERSION with your extension version (e.g., 0.0.1.0)
param(
[string]$ExtensionName = "UPDATE", # Change to your extension name
[string]$Configuration = "Release",
[string]$Version = "UPDATE", # Change to your version
[string[]]$Platforms = @("x64", "arm64")
)
$ErrorActionPreference = "Stop"
Write-Host "Building $ExtensionName EXE installer..." -ForegroundColor Green
Write-Host "Version: $Version" -ForegroundColor Yellow
Write-Host "Platforms: $($Platforms -join ', ')" -ForegroundColor Yellow
# Get the project directory (two levels up from winget-resources)
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$ProjectDir = Split-Path -Parent (Split-Path -Parent $ScriptDir)
$ProjectFile = "$ProjectDir\$ExtensionName.csproj"
Write-Host "Script directory: $ScriptDir" -ForegroundColor Cyan
Write-Host "Project directory: $ProjectDir" -ForegroundColor Cyan
# Clean previous builds
Write-Host "Cleaning previous builds..." -ForegroundColor Yellow
if (Test-Path "$ProjectDir\bin") {
Remove-Item -Path "$ProjectDir\bin" -Recurse -Force -ErrorAction SilentlyContinue
}
if (Test-Path "$ProjectDir\obj") {
Remove-Item -Path "$ProjectDir\obj" -Recurse -Force -ErrorAction SilentlyContinue
}
# Restore packages
Write-Host "Restoring NuGet packages..." -ForegroundColor Yellow
dotnet restore $ProjectFile
# Build for each platform
foreach ($Platform in $Platforms) {
Write-Host "`n=== Building $Platform ===" -ForegroundColor Cyan
# Build and publish
Write-Host "Building and publishing $Platform application..." -ForegroundColor Yellow
dotnet publish $ProjectFile `
--configuration $Configuration `
--runtime "win-$Platform" `
--self-contained true `
--output "$ProjectDir\bin\$Configuration\win-$Platform\publish"
if ($LASTEXITCODE -ne 0) {
Write-Warning "Build failed for $Platform with exit code: $LASTEXITCODE"
continue
}
# Check if files were published
$publishDir = "$ProjectDir\bin\$Configuration\win-$Platform\publish"
$fileCount = (Get-ChildItem -Path $publishDir -Recurse -File).Count
Write-Host "✅ Published $fileCount files to $publishDir" -ForegroundColor Green
# Create platform-specific setup script
Write-Host "Creating installer script for $Platform..." -ForegroundColor Yellow
$setupTemplate = Get-Content "$ScriptDir\setup-template.iss" -Raw
# Update version
$setupScript = $setupTemplate -replace '#define AppVersion ".*"', "#define AppVersion `"$Version`""
# Update output filename to include platform suffix
$setupScript = $setupScript -replace 'OutputBaseFilename=(.*?)\{#AppVersion\}', "OutputBaseFilename=`$1{#AppVersion}-$Platform"
# Update source path for the platform
$setupScript = $setupScript -replace 'Source: "bin\\Release\\win-x64\\publish', "Source: `"bin\Release\win-$Platform\publish"
# Add architecture settings after [Setup] section
if ($Platform -eq "arm64") {
$setupScript = $setupScript -replace '(\[Setup\][^\[]*)(MinVersion=)', "`$1ArchitecturesAllowed=arm64`r`nArchitecturesInstallIn64BitMode=arm64`r`n`$2"
} else {
$setupScript = $setupScript -replace '(\[Setup\][^\[]*)(MinVersion=)', "`$1ArchitecturesAllowed=x64compatible`r`nArchitecturesInstallIn64BitMode=x64compatible`r`n`$2"
}
$setupScript | Out-File -FilePath "$ProjectDir\setup-$Platform.iss" -Encoding UTF8
# Create installer with Inno Setup
Write-Host "Creating $Platform installer with Inno Setup..." -ForegroundColor Yellow
$InnoSetupPath = "${env:ProgramFiles(x86)}\Inno Setup 6\iscc.exe"
if (-not (Test-Path $InnoSetupPath)) {
$InnoSetupPath = "${env:ProgramFiles}\Inno Setup 6\iscc.exe"
}
if (Test-Path $InnoSetupPath) {
& $InnoSetupPath "$ProjectDir\setup-$Platform.iss"
if ($LASTEXITCODE -eq 0) {
$installer = Get-ChildItem "$ProjectDir\bin\$Configuration\installer\*-$Platform.exe" -ErrorAction SilentlyContinue | Select-Object -First 1
if ($installer) {
$sizeMB = [math]::Round($installer.Length / 1MB, 2)
Write-Host "✅ Created $Platform installer: $($installer.Name) ($sizeMB MB)" -ForegroundColor Green
} else {
Write-Warning "Installer file not found for $Platform"
}
} else {
Write-Warning "Inno Setup failed for $Platform with exit code: $LASTEXITCODE"
}
} else {
Write-Warning "Inno Setup not found at expected locations"
}
}
Write-Host "`n🎉 Build completed successfully!" -ForegroundColor Green

View File

@@ -1,127 +0,0 @@
# TEMPLATE: Extension EXE Installer Build and Release Workflow
#
# To use this template for a new extension:
# 1. Copy this file to a new workflow file (e.g., release-extension-exe.yml)
# 2. Update Global constants with your data:
# - GITHUB_REPO_URL with your GitHub repository URL (e.g., https://github.com/yourusername/YourRepository)
# - DISPLAY_NAME with your display name (e.g., My Extension)
# - EXTENSION_NAME with your extension name (e.g., CmdPalMyExtension)
# - FOLDER_NAME with your project folder name (e.g., CmdPalMyExtension)
name: CmdPal Extension - Build EXE Installer
on:
workflow_dispatch:
inputs:
version:
description: 'Version number (leave empty to auto-detect)'
required: false
type: string
release_notes:
description: 'What is new in this version'
required: false
default: 'New release with latest updates and improvements.'
type: string
# Global constants: UPDATE THESE, example; DISPLAY_NAME: ${{ vars.DISPLAY_NAME || 'CmdPal Name' }}
env:
DISPLAY_NAME: ${{ vars.DISPLAY_NAME || 'DISPLAY_NAME' }}
EXTENSION_NAME: ${{ vars.EXTENSION_NAME || 'EXTENSION_NAME' }}
FOLDER_NAME: ${{ vars.FOLDER_NAME || 'FOLDER_NAME' }}
GITHUB_REPO_URL: ${{ vars.GITHUB_REPO_URL || 'GITHUB_REPO_URL' }}
jobs:
build:
runs-on: windows-2022
permissions:
contents: write
actions: read
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup .NET 9
uses: actions/setup-dotnet@v4
with:
dotnet-version: '9.0.x'
- name: Install Inno Setup
run: choco install innosetup -y --no-progress
shell: pwsh
- name: Get version from project
id: version
run: |
if ("${{ github.event.inputs.version }}" -ne "") {
$version = "${{ github.event.inputs.version }}"
} else {
$projectFile = "${{ env.FOLDER_NAME }}/${{ env.EXTENSION_NAME }}.csproj"
$xml = [xml](Get-Content $projectFile)
$version = $xml.Project.PropertyGroup.AppxPackageVersion | Select-Object -First 1
if (-not $version) { throw "Version not found in project file" }
}
echo "VERSION=$version" >> $env:GITHUB_OUTPUT
Write-Host "Using version: $version"
shell: pwsh
- name: Build EXE installers (x64 and ARM64)
run: |
Set-Location "${{ env.FOLDER_NAME }}/Publication/winget-resources"
.\build-exe.ps1 -Version "${{ steps.version.outputs.VERSION }}" -Platforms @("x64", "arm64")
shell: pwsh
- name: Upload x64 installer artifact
uses: actions/upload-artifact@v4
with:
name: ${{ env.EXTENSION_NAME }}-x64-installer
path: ${{ env.FOLDER_NAME }}/bin/Release/installer/*-x64.exe
if-no-files-found: error
- name: Upload ARM64 installer artifact
uses: actions/upload-artifact@v4
with:
name: ${{ env.EXTENSION_NAME }}-arm64-installer
path: ${{ env.FOLDER_NAME }}/bin/Release/installer/*-arm64.exe
if-no-files-found: warn
- name: Create GitHub Release
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ env.EXTENSION_NAME }}-v${{ steps.version.outputs.VERSION }}
name: "${{ env.DISPLAY_NAME }} v${{ steps.version.outputs.VERSION }}"
body: |
## 🎯 ${{ env.DISPLAY_NAME }} ${{ steps.version.outputs.VERSION }}
## What's New
${{ github.event.inputs.release_notes }}
## 📦 Installation
Download the installer for your system architecture:
- **x64 (Intel/AMD)**: `${{ env.DISPLAY_NAME }}-Setup-${{ steps.version.outputs.VERSION }}-x64.exe`
- **ARM64 (Windows on ARM)**: `${{ env.DISPLAY_NAME }}-Setup-${{ steps.version.outputs.VERSION }}-arm64.exe`
1. Download the appropriate installer from the Assets section below
2. Run the installer with administrator privileges
3. The extension will be registered and available in Command Palette
## 🔗 More Information
Repository: ${{ env.GITHUB_REPO_URL }}
files: ${{ env.FOLDER_NAME }}/bin/Release/installer/*.exe
draft: false
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build summary
run: |
Write-Host "🎉 ${{ env.DISPLAY_NAME }} Release Complete!" -ForegroundColor Green
Write-Host "Version: ${{ steps.version.outputs.VERSION }}" -ForegroundColor Yellow
Write-Host "📁 Installer uploaded to GitHub Release" -ForegroundColor Green
shell: pwsh

View File

@@ -1,36 +0,0 @@
; TEMPLATE: Inno Setup Script for Command Palette Extensions
;
; To use this template for a new extension:
; 1. Copy this file to your extension's project folder as "setup-template.iss"
; 2. Replace EXTENSION_NAME with your extension name (e.g., CmdPalMyExtension)
; 3. Replace DISPLAY_NAME with your extension's display name (e.g., My Extension)
; 4. Replace DEVELOPER_NAME with your name (e.g., Your Name Here)
; 5. Replace CLSID-HERE with extensions CLSID
; 6. Update the default version to match your project file
#define AppVersion "0.0.1.0"
[Setup]
AppId={{GUID-HERE}}
AppName=DISPLAY_NAME
AppVersion={#AppVersion}
AppPublisher=DEVELOPER_NAME
DefaultDirName={autopf}\EXTENSION_NAME
OutputDir=bin\Release\installer
OutputBaseFilename=EXTENSION_NAME-Setup-{#AppVersion}
Compression=lzma
SolidCompression=yes
MinVersion=10.0.19041
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
[Files]
Source: "bin\Release\win-x64\publish\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs
[Icons]
Name: "{group}\DISPLAY_NAME"; Filename: "{app}\EXTENSION_NAME.exe"
[Registry]
Root: HKCU; Subkey: "SOFTWARE\Classes\CLSID\{{CLSID-HERE}}"; ValueData: "EXTENSION_NAME"
Root: HKCU; Subkey: "SOFTWARE\Classes\CLSID\{{CLSID-HERE}}\LocalServer32"; ValueData: "{app}\EXTENSION_NAME.exe -RegisterProcessAsComServer"

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<RootNamespace>TemplateCmdPalExtension</RootNamespace>
@@ -10,58 +10,20 @@
<SupportedOSPlatformVersion>10.0.19041.0</SupportedOSPlatformVersion>
<RuntimeIdentifiers>win-x64;win-arm64</RuntimeIdentifiers>
<PublishProfile>win-$(Platform).pubxml</PublishProfile>
<EnableMsixTooling>true</EnableMsixTooling>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Content Include="Assets\**\*.png" />
<Content Include="Assets\SplashScreen.scale-200.png" />
<Content Include="Assets\LockScreenLogo.scale-200.png" />
<Content Include="Assets\Square150x150Logo.scale-200.png" />
<Content Include="Assets\Square44x44Logo.scale-200.png" />
<Content Include="Assets\Square44x44Logo.targetsize-24_altform-unplated.png" />
<Content Include="Assets\StoreLogo.png" />
<Content Include="Assets\Wide310x150Logo.scale-200.png" />
</ItemGroup>
<PropertyGroup>
<!-- FOR PUBLISHING TO WINGET -->
<!-- When you're ready to publish your extension to winget,
comment out PublishProfile and uncomment WindowsPackageType tag to create
OR USE THE GITHUB ACTION TO DO THIS FOR YOU-->
<PublishProfile>win-$(Platform).pubxml</PublishProfile>
<!--<WindowsPackageType>None</WindowsPackageType> -->
<!-- FOR PUBLISHING TO MICROSOFT STORE 1 of 2-->
<!-- When you're ready to publish your extension to Microsoft Store, you'll need to change that are below
AppxPackageIdentityName = replace with Microsoft Store's Package/Identity/Name
AppxPackagePublisher = replace with Microsoft Store's Package/Identity/Publisher
-->
<!-- <AppxPackageIdentityName>YOUR_PACKAGE_IDENTITY_NAME_HERE</AppxPackageIdentityName>
<AppxPackagePublisher>YOUR_PACKAGE_IDENTITY_PUBLISHER_HERE</AppxPackagePublisher>
<AppxPackageVersion>0.0.1.0</AppxPackageVersion> -->
</PropertyGroup>
<!-- FOR PUBLISHING TO MICROSOFT STORE 2 of 2-->
<!-- When you're ready to publish your extension to Microsoft Store, uncomment the
Target tag and confirm the images exist -->
<!-- <Target Name="PrepareAssets" BeforeTargets="BeforeBuild">
<Copy SourceFiles="$(MSBuildProjectDirectory)\Assets\Square150x150Logo.scale-200.png"
DestinationFiles="$(MSBuildProjectDirectory)\Assets\Square150x150Logo.png"
SkipUnchangedFiles="true" />
<Copy SourceFiles="$(MSBuildProjectDirectory)\Assets\Square44x44Logo.scale-200.png"
DestinationFiles="$(MSBuildProjectDirectory)\Assets\SmallTile.png"
SkipUnchangedFiles="true" />
<Copy SourceFiles="$(MSBuildProjectDirectory)\Assets\Wide310x150Logo.scale-200.png"
DestinationFiles="$(MSBuildProjectDirectory)\Assets\Wide310x150Logo.png"
SkipUnchangedFiles="true" />
<Copy SourceFiles="$(MSBuildProjectDirectory)\Assets\SplashScreen.scale-200.png"
DestinationFiles="$(MSBuildProjectDirectory)\Assets\SplashScreen.png"
SkipUnchangedFiles="true" />
<Copy SourceFiles="$(MSBuildProjectDirectory)\Assets\Square150x150Logo.scale-200.png"
DestinationFiles="$(MSBuildProjectDirectory)\Assets\LargeTile.png"
SkipUnchangedFiles="true" />
<Copy SourceFiles="$(MSBuildProjectDirectory)\Assets\StoreLogo.scale-100.png"
DestinationFiles="$(MSBuildProjectDirectory)\Assets\StoreLogo.png"
SkipUnchangedFiles="true" />
</Target> -->
<ItemGroup>
<Manifest Include="$(ApplicationManifest)" />

View File

@@ -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,
};
}
}

View File

@@ -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)

View File

@@ -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;
}
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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 Microsoft.CommandPalette.Extensions;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Data;
namespace Microsoft.CmdPal.UI;
public partial class DetailsSizeToGridLengthConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is ContentSize size)
{
// This converter calculates the Star width for the LIST.
//
// The input 'size' (ContentSize) represents the TARGET WIDTH desired for the DETAILS PANEL.
//
// To ensure the Details Panel achieves its target size (e.g. ContentSize.Large),
// we must shrink the List and let the Details fill the available space.
// (e.g., A larger target size for Details results in a smaller Star value for the List).
var starValue = size switch
{
ContentSize.Small => 3.0,
ContentSize.Medium => 2.0,
ContentSize.Large => 1.0,
_ => 3.0,
};
return new GridLength(starValue, GridUnitType.Star);
}
return new GridLength(3.0, GridUnitType.Star);
}
public object ConvertBack(object value, Type targetType, object parameter, string language) => throw new NotImplementedException();
}

View File

@@ -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,

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -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,

View File

@@ -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);
}
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -63,7 +63,4 @@ CreateWindowEx
WNDCLASSEXW
RegisterClassEx
GetStockObject
GetModuleHandle
GetWindowThreadProcessId
AttachThreadInput
GetModuleHandle

View File

@@ -26,7 +26,6 @@
EmptyValue="Collapsed"
NotEmptyValue="Visible" />
<cmdpalUI:DetailsSizeToGridLengthConverter x:Key="SizeToWidthConverter" />
<cmdpalUI:MessageStateToSeverityConverter x:Key="MessageStateToSeverityConverter" />
<cmdpalUI:DetailsDataTemplateSelector
@@ -371,7 +370,7 @@
<Grid x:Name="ContentGrid" Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="{x:Bind ViewModel.Details.Size, Mode=OneWay, Converter={StaticResource SizeToWidthConverter}}" />
<ColumnDefinition Width="3*" />
<ColumnDefinition x:Name="DetailsColumn" Width="Auto" />
</Grid.ColumnDefinitions>

View File

@@ -105,7 +105,6 @@ public sealed partial class SettingsWindow : WindowEx,
"Extensions" => typeof(ExtensionsPage),
_ => null,
};
if (pageType is not null)
{
NavFrame.Navigate(pageType);

View File

@@ -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";

View File

@@ -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())
{

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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())
{

View File

@@ -5,6 +5,7 @@
using System;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Microsoft.UI.Xaml;
namespace SamplePagesExtension;
@@ -22,34 +23,14 @@ internal sealed partial class SampleListPageWithDetails : ListPage
return [
new ListItem(new NoOpCommand())
{
Title = "Details on ListItems (Small)",
Title = "This page demonstrates Details on ListItems",
Details = new Details()
{
Title = "This item has default details size",
Title = "List Item 1",
Body = "Each of these items can have a `Body` formatted with **Markdown**",
},
},
new ListItem(new NoOpCommand())
{
Title = "Details on ListItems (Medium)",
Details = new Details()
{
Title = "This item has medium details size",
Body = "Each of these items can have a `Body` formatted with **Markdown**",
Size = ContentSize.Medium,
},
},
new ListItem(new NoOpCommand())
{
Title = "Details on ListItems (Large)",
Details = new Details()
{
Title = "This item has large details size",
Body = "Each of these items can have a `Body` formatted with **Markdown**",
Size = ContentSize.Large,
},
},
new ListItem(new NoOpCommand())
{
Title = "This one has a subtitle too",
Subtitle = "Example Subtitle",
@@ -89,13 +70,11 @@ internal sealed partial class SampleListPageWithDetails : ListPage
new ListItem(new NoOpCommand())
{
Title = "This one has metadata",
Subtitle = "And Large Details panel",
Tags = [],
Details = new Details()
{
Title = "Metadata Example",
Body = "Each of the sections below is some sample metadata",
Size = ContentSize.Large,
Metadata = [
new DetailsElement()
{

View File

@@ -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
];
}
}

View File

@@ -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",
},
];
}
}

View File

@@ -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;
}
}

View File

@@ -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())

View File

@@ -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;
}
}

View File

@@ -1,11 +1,10 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Windows.Foundation.Collections;
namespace Microsoft.CommandPalette.Extensions.Toolkit;
public partial class Details : BaseObservable, IDetails, IExtendedAttributesProvider
public partial class Details : BaseObservable, IDetails
{
public virtual IIconInfo HeroImage
{
@@ -54,21 +53,4 @@ public partial class Details : BaseObservable, IDetails, IExtendedAttributesProv
}
= [];
public virtual ContentSize Size
{
get;
set
{
field = value;
OnPropertyChanged(nameof(Size));
}
}
= ContentSize.Small;
public IDictionary<string, object>? GetProperties() => new ValueSet()
{
{ "Size", (int)Size },
};
}

View File

@@ -27,6 +27,6 @@ public partial class FontIconData : IconData, IExtendedAttributesProvider
public IDictionary<string, object>? GetProperties() => new ValueSet()
{
{ WellKnownExtensionAttributes.FontFamily, FontFamily },
{ "FontFamily", FontFamily },
};
}

View File

@@ -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>

View File

@@ -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();
}

View File

@@ -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 { }
}
}

View File

@@ -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";
}

View File

@@ -160,15 +160,6 @@ namespace Microsoft.CommandPalette.Extensions
[uuid("6a6dd345-37a3-4a1e-914d-4f658a4d583d")]
[contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)]
interface IDetailsData {}
[contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)]
enum ContentSize
{
Small = 0,
Medium = 1,
Large = 2,
};
[contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)]
interface IDetailsElement {
String Key { get; };

View File

@@ -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>

View File

@@ -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>

View File

@@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using FancyZonesEditor.Models;
@@ -50,15 +49,18 @@ namespace UITests_FancyZones
[TestInitialize]
public void TestInitialize()
{
Session.KillAllProcessesByName("PowerToys");
// ClearOpenWindows
ClearOpenWindows();
AppZoneHistory.DeleteFile();
FancyZonesEditorHelper.Files.Restore();
SetupCustomLayouts();
// kill all processes related to FancyZones Editor to ensure a clean state
Session.KillAllProcessesByName("PowerToys.FancyZonesEditor");
RestartScopeExe("Hosts");
Thread.Sleep(2000);
AppZoneHistory.DeleteFile();
this.RestartScopeExe();
FancyZonesEditorHelper.Files.Restore();
// Set a custom layout with 1 subzones and clear app zone history
SetupCustomLayouts();
// Get the current mouse button setting
nonPrimaryMouseButton = SystemInformation.MouseButtonsSwapped ? "Left" : "Right";
@@ -70,175 +72,6 @@ namespace UITests_FancyZones
LaunchFancyZones();
}
/// <summary>
/// Test toggling zones using a non-primary mouse click during window dragging.
/// <list type="bullet">
/// <item>
/// <description>Verifies that clicking a non-primary mouse button deactivates zones while dragging a window.</description>
/// </item>
/// </list>
/// </summary>
[TestMethod("FancyZones.Settings.TestToggleZonesWithNonPrimaryMouseClick")]
[TestCategory("FancyZones_Dragging #3")]
public void TestToggleZonesWithNonPrimaryMouseClick()
{
string testCaseName = nameof(TestToggleZonesWithNonPrimaryMouseClick);
var windowRect = Session.GetMainWindowRect();
int startX = windowRect.Left + 70;
int startY = windowRect.Top + 25;
int endX = startX + 300;
int endY = startY + 300;
var (initialColor, withMouseColor) = RunDragInteractions(
preAction: () =>
{
Session.MoveMouseTo(startX, startY);
Session.PerformMouseAction(MouseActionType.LeftDown);
Session.MoveMouseTo(endX, endY);
},
postAction: () =>
{
// press non-primary mouse button to toggle zones
Session.PerformMouseAction(
nonPrimaryMouseButton == "Right" ? MouseActionType.RightClick : MouseActionType.LeftClick);
},
releaseAction: () =>
{
Session.PerformMouseAction(MouseActionType.LeftUp);
},
testCaseName: testCaseName);
// check the zone color is deactivated
Assert.AreNotEqual(highlightColor, withMouseColor, $"[{testCaseName}] Zone deactivation failed.");
// check the zone color is activated
Assert.AreEqual(highlightColor, initialColor, $"[{testCaseName}] Zone activation failed.");
}
/// <summary>
/// Test both use Shift and non primary mouse off settings.
/// <list type="bullet">
/// <item>
/// <description>Verifies that pressing the Shift key deactivates zones during a window drag-and-hold action.</description>
/// </item>
/// </list>
/// </summary>
[TestMethod("FancyZones.Settings.TestShowZonesWhenShiftAndMouseOff")]
[TestCategory("FancyZones_Dragging #4")]
public void TestShowZonesWhenShiftAndMouseOff()
{
string testCaseName = nameof(TestShowZonesWhenShiftAndMouseOff);
var windowRect = Session.GetMainWindowRect();
int startX = windowRect.Left + 70;
int startY = windowRect.Top + 25;
int endX = startX + 300;
int endY = startY + 300;
var (initialColor, withShiftColor) = RunDragInteractions(
preAction: () =>
{
Session.MoveMouseTo(startX, startY);
Session.PerformMouseAction(MouseActionType.LeftDown);
Session.MoveMouseTo(endX, endY);
},
postAction: () =>
{
// press Shift Key to deactivate zones
Session.PressKey(Key.Shift);
Task.Delay(1000).Wait();
},
releaseAction: () =>
{
Session.PerformMouseAction(MouseActionType.LeftUp);
Session.ReleaseKey(Key.Shift);
},
testCaseName: testCaseName);
Assert.AreEqual(highlightColor, initialColor, $"[{testCaseName}] Zone activation failed.");
Assert.AreNotEqual(highlightColor, withShiftColor, $"[{testCaseName}] Zone deactivation failed.");
}
/// <summary>
/// Test zone visibility when both Shift key and mouse settings are involved.
/// <list type="bullet">
/// <item>
/// <description>Verifies that zones are activated when Shift is pressed during drag, and deactivated by a non-primary mouse click.</description>
/// </item>
/// </list>
/// </summary>
[TestMethod("FancyZones.Settings.TestShowZonesWhenShiftAndMouseOn")]
[TestCategory("FancyZones_Dragging #5")]
public void TestShowZonesWhenShiftAndMouseOn()
{
string testCaseName = nameof(TestShowZonesWhenShiftAndMouseOn);
var windowRect = Session.GetMainWindowRect();
int startX = windowRect.Left + 70;
int startY = windowRect.Top + 25;
int endX = startX + 300;
int endY = startY + 300;
var (initialColor, withShiftColor) = RunDragInteractions(
preAction: () =>
{
Session.MoveMouseTo(startX, startY);
Session.PerformMouseAction(MouseActionType.LeftDown);
Session.MoveMouseTo(endX, endY);
},
postAction: () =>
{
Session.PressKey(Key.Shift);
},
releaseAction: () =>
{
},
testCaseName: testCaseName);
Assert.AreEqual(highlightColor, withShiftColor, $"[{testCaseName}] show zone failed.");
Session.PerformMouseAction(
nonPrimaryMouseButton == "Right" ? MouseActionType.RightClick : MouseActionType.LeftClick);
string zoneColorWithMouse = GetOutWindowPixelColor(30);
Assert.AreEqual(initialColor, zoneColorWithMouse, $"[{nameof(TestShowZonesWhenShiftAndMouseOff)}] Zone deactivate failed.");
Session.ReleaseKey(Key.Shift);
Session.PerformMouseAction(MouseActionType.LeftUp);
}
/// <summary>
/// Test that a window becomes transparent during dragging when the transparent window setting is enabled.
/// <list type="bullet">
/// <item>
/// <description>Verifies that the window appears transparent while being dragged.</description>
/// </item>
/// </list>
/// </summary>
[TestMethod("FancyZones.Settings.TestMakeDraggedWindowTransparentOn")]
[TestCategory("FancyZones_Dragging #8")]
public void TestMakeDraggedWindowTransparentOn()
{
var pixel = GetPixelWhenMakeDraggedWindow();
Assert.AreNotEqual(pixel.PixelInWindow, pixel.TransPixel, $"[{nameof(TestMakeDraggedWindowTransparentOn)}] Window transparency failed.");
}
/// <summary>
/// Test that a window remains opaque during dragging when the transparent window setting is disabled.
/// <list type="bullet">
/// <item>
/// <description>Verifies that the window is not transparent while being dragged.</description>
/// </item>
/// </list>
/// </summary>
[TestMethod("FancyZones.Settings.TestMakeDraggedWindowTransparentOff")]
[TestCategory("FancyZones_Dragging #8")]
public void TestMakeDraggedWindowTransparentOff()
{
var pixel = GetPixelWhenMakeDraggedWindow();
Assert.AreEqual(pixel.PixelInWindow, pixel.TransPixel, $"[{nameof(TestMakeDraggedWindowTransparentOff)}] Window without transparency failed.");
}
/// <summary>
/// Test Use Shift key to activate zones while dragging a window in FancyZones Zone Behaviour Settings
/// <list type="bullet">
@@ -252,19 +85,13 @@ namespace UITests_FancyZones
public void TestShowZonesOnShiftDuringDrag()
{
string testCaseName = nameof(TestShowZonesOnShiftDuringDrag);
var windowRect = Session.GetMainWindowRect();
int startX = windowRect.Left + 70;
int startY = windowRect.Top + 25;
int endX = startX + 300;
int endY = startY + 300;
Pane dragElement = Find<Pane>(By.Name("Non Client Input Sink Window")); // element to drag
var offSet = ZoneSwitchHelper.GetOffset(dragElement, quarterX, quarterY);
var (initialColor, withShiftColor) = RunDragInteractions(
preAction: () =>
{
Session.MoveMouseTo(startX, startY);
Session.PerformMouseAction(MouseActionType.LeftDown);
Session.MoveMouseTo(endX, endY);
dragElement.DragAndHold(offSet.Dx, offSet.Dy);
},
postAction: () =>
{
@@ -274,16 +101,22 @@ namespace UITests_FancyZones
releaseAction: () =>
{
Session.ReleaseKey(Key.Shift);
Task.Delay(1000).Wait(); // Optional: Wait for a moment to ensure window switch
Task.Delay(5000).Wait(); // Optional: Wait for a moment to ensure window switch
},
testCaseName: testCaseName);
string zoneColorWithoutShift = GetOutWindowPixelColor(30);
Assert.AreNotEqual(initialColor, withShiftColor, $"[{testCaseName}] Zone color did not change; zone activation failed.");
Assert.AreEqual(highlightColor, withShiftColor, $"[{testCaseName}] Zone color did not match the highlight color; activation failed.");
Assert.AreNotEqual(initialColor, withShiftColor, $"[{testCaseName}] Zone display failed.");
Assert.IsTrue(
withShiftColor == inactivateColor || withShiftColor == highlightColor,
$"[{testCaseName}] Zone display failed: withShiftColor was {withShiftColor}, expected {inactivateColor} or {highlightColor}.");
Assert.AreEqual(inactivateColor, withShiftColor, $"[{testCaseName}] Zone display failed.");
Session.PerformMouseAction(MouseActionType.LeftUp);
Assert.AreEqual(zoneColorWithoutShift, initialColor, $"[{testCaseName}] Zone deactivated failed.");
dragElement.ReleaseDrag();
Clean();
}
/// <summary>
@@ -300,30 +133,24 @@ namespace UITests_FancyZones
{
string testCaseName = nameof(TestShowZonesOnDragDuringShift);
var windowRect = Session.GetMainWindowRect();
int startX = windowRect.Left + 70;
int startY = windowRect.Top + 25;
int endX = startX + 300;
int endY = startY + 300;
var dragElement = Find<Pane>(By.Name("Non Client Input Sink Window"));
var offSet = ZoneSwitchHelper.GetOffset(dragElement, quarterX, quarterY);
var (initialColor, withDragColor) = RunDragInteractions(
preAction: () =>
{
dragElement.Drag(offSet.Dx, offSet.Dy);
Session.PressKey(Key.Shift);
Task.Delay(100).Wait();
},
postAction: () =>
{
Session.MoveMouseTo(startX, startY);
Session.PerformMouseAction(MouseActionType.LeftDown);
Session.MoveMouseTo(endX, endY);
Task.Delay(1000).Wait();
dragElement.DragAndHold(0, 0);
Task.Delay(5000).Wait();
},
releaseAction: () =>
{
Session.PerformMouseAction(MouseActionType.LeftUp);
dragElement.ReleaseDrag();
Session.ReleaseKey(Key.Shift);
Task.Delay(100).Wait();
},
testCaseName: testCaseName);
@@ -334,6 +161,178 @@ namespace UITests_FancyZones
string appZoneHistoryJson = AppZoneHistory.GetData();
string? zoneNumber = ZoneSwitchHelper.GetZoneIndexSetByAppName(powertoysWindowName, appZoneHistoryJson);
Assert.IsNull(zoneNumber, $"[{testCaseName}] AppZoneHistory layout was unexpectedly set.");
Clean();
}
/// <summary>
/// Test toggling zones using a non-primary mouse click during window dragging.
/// <list type="bullet">
/// <item>
/// <description>Verifies that clicking a non-primary mouse button deactivates zones while dragging a window.</description>
/// </item>
/// </list>
/// </summary>
[TestMethod("FancyZones.Settings.TestToggleZonesWithNonPrimaryMouseClick")]
[TestCategory("FancyZones_Dragging #3")]
public void TestToggleZonesWithNonPrimaryMouseClick()
{
string testCaseName = nameof(TestToggleZonesWithNonPrimaryMouseClick);
var dragElement = Find<Pane>(By.Name("Non Client Input Sink Window"));
var offSet = ZoneSwitchHelper.GetOffset(dragElement, quarterX, quarterY);
var (initialColor, withMouseColor) = RunDragInteractions(
preAction: () =>
{
// activate zone
dragElement.DragAndHold(offSet.Dx, offSet.Dy);
},
postAction: () =>
{
// press non-primary mouse button to toggle zones
Session.PerformMouseAction(
nonPrimaryMouseButton == "Right" ? MouseActionType.RightClick : MouseActionType.LeftClick);
},
releaseAction: () =>
{
dragElement.ReleaseDrag();
},
testCaseName: testCaseName);
// check the zone color is deactivated
Assert.AreNotEqual(highlightColor, withMouseColor, $"[{testCaseName}] Zone deactivation failed.");
// check the zone color is activated
Assert.AreEqual(highlightColor, initialColor, $"[{testCaseName}] Zone activation failed.");
Clean();
}
/// <summary>
/// Test both use Shift and non primary mouse off settings.
/// <list type="bullet">
/// <item>
/// <description>Verifies that pressing the Shift key deactivates zones during a window drag-and-hold action.</description>
/// </item>
/// </list>
/// </summary>
[TestMethod("FancyZones.Settings.TestShowZonesWhenShiftAndMouseOff")]
[TestCategory("FancyZones_Dragging #4")]
public void TestShowZonesWhenShiftAndMouseOff()
{
string testCaseName = nameof(TestShowZonesWhenShiftAndMouseOff);
Pane dragElement = Find<Pane>(By.Name("Non Client Input Sink Window"));
var offSet = ZoneSwitchHelper.GetOffset(dragElement, quarterX, quarterY);
var (initialColor, withShiftColor) = RunDragInteractions(
preAction: () =>
{
// activate zone
dragElement.DragAndHold(offSet.Dx, offSet.Dy);
},
postAction: () =>
{
// press Shift Key to deactivate zones
Session.PressKey(Key.Shift);
Task.Delay(500).Wait();
},
releaseAction: () =>
{
dragElement.ReleaseDrag();
Session.ReleaseKey(Key.Shift);
},
testCaseName: testCaseName);
Assert.AreEqual(highlightColor, initialColor, $"[{testCaseName}] Zone activation failed.");
Assert.AreNotEqual(highlightColor, withShiftColor, $"[{testCaseName}] Zone deactivation failed.");
Clean();
}
/// <summary>
/// Test zone visibility when both Shift key and mouse settings are involved.
/// <list type="bullet">
/// <item>
/// <description>Verifies that zones are activated when Shift is pressed during drag, and deactivated by a non-primary mouse click.</description>
/// </item>
/// </list>
/// </summary>
[TestMethod("FancyZones.Settings.TestShowZonesWhenShiftAndMouseOn")]
[TestCategory("FancyZones_Dragging #5")]
public void TestShowZonesWhenShiftAndMouseOn()
{
string testCaseName = nameof(TestShowZonesWhenShiftAndMouseOn);
var dragElement = Find<Pane>(By.Name("Non Client Input Sink Window"));
var offSet = ZoneSwitchHelper.GetOffset(dragElement, quarterX, quarterY);
var (initialColor, withShiftColor) = RunDragInteractions(
preAction: () =>
{
dragElement.DragAndHold(offSet.Dx, offSet.Dy);
},
postAction: () =>
{
Session.PressKey(Key.Shift);
},
releaseAction: () =>
{
},
testCaseName: testCaseName);
Assert.AreEqual(inactivateColor, withShiftColor, $"[{testCaseName}] show zone failed.");
Session.PerformMouseAction(
nonPrimaryMouseButton == "Right" ? MouseActionType.RightClick : MouseActionType.LeftClick);
string zoneColorWithMouse = GetOutWindowPixelColor(30);
Assert.AreEqual(initialColor, zoneColorWithMouse, $"[{nameof(TestShowZonesWhenShiftAndMouseOff)}] Zone deactivate failed.");
Session.ReleaseKey(Key.Shift);
dragElement.ReleaseDrag();
Clean();
}
/// <summary>
/// Test that a window becomes transparent during dragging when the transparent window setting is enabled.
/// <list type="bullet">
/// <item>
/// <description>Verifies that the window appears transparent while being dragged.</description>
/// </item>
/// </list>
/// </summary>
[TestMethod("FancyZones.Settings.TestMakeDraggedWindowTransparentOn")]
[TestCategory("FancyZones_Dragging #8")]
public void TestMakeDraggedWindowTransparentOn()
{
var pixel = GetPixelWhenMakeDraggedWindow();
Assert.AreNotEqual(pixel.PixelInWindow, pixel.TransPixel, $"[{nameof(TestMakeDraggedWindowTransparentOn)}] Window transparency failed.");
Clean();
}
/// <summary>
/// Test that a window remains opaque during dragging when the transparent window setting is disabled.
/// <list type="bullet">
/// <item>
/// <description>Verifies that the window is not transparent while being dragged.</description>
/// </item>
/// </list>
/// </summary>
[TestMethod("FancyZones.Settings.TestMakeDraggedWindowTransparentOff")]
[TestCategory("FancyZones_Dragging #8")]
public void TestMakeDraggedWindowTransparentOff()
{
var pixel = GetPixelWhenMakeDraggedWindow();
Assert.AreEqual(pixel.PixelInWindow, pixel.TransPixel, $"[{nameof(TestMakeDraggedWindowTransparentOff)}] Window without transparency failed.");
Clean();
}
private void Clean()
{
// clean app zone history file
AppZoneHistory.DeleteFile();
}
// Helper method to ensure the desktop has no open windows by clicking the "Show Desktop" button
@@ -353,7 +352,7 @@ namespace UITests_FancyZones
desktopButtonName = "Show Desktop";
}
this.Find<Microsoft.PowerToys.UITest.Button>(By.Name(desktopButtonName), 5000, true).Click(false, 500, 1000);
this.Find<Microsoft.PowerToys.UITest.Button>(By.Name(desktopButtonName), 5000, true).Click(false, 500, 2000);
}
// Setup custom layout with 1 subzones
@@ -383,11 +382,6 @@ namespace UITests_FancyZones
this.Scroll(6, "Down"); // Pull the settings page up to make sure the settings are visible
ZoneBehaviourSettings(TestContext.TestName);
// Go back and forth to make sure settings applied
this.Find<NavigationViewItem>("Workspaces").Click();
Task.Delay(200).Wait();
this.Find<NavigationViewItem>("FancyZones").Click();
this.Find<Microsoft.PowerToys.UITest.Button>(By.AccessibilityId("LaunchLayoutEditorButton")).Click(false, 500, 10000);
this.Session.Attach(PowerToysModule.FancyZone);
@@ -441,26 +435,22 @@ namespace UITests_FancyZones
// Get the mouse color of the pixel when make dragged window
private (string PixelInWindow, string TransPixel) GetPixelWhenMakeDraggedWindow()
{
var windowRect = Session.GetMainWindowRect();
int startX = windowRect.Left + 70;
int startY = windowRect.Top + 25;
int endX = startX + 100;
int endY = startY + 100;
var dragElement = Find<Pane>(By.Name("Non Client Input Sink Window"));
Session.MoveMouseTo(startX, startY);
// maximize the window to make sure get pixel color more accurate
dragElement.DoubleClick();
// Session.PerformMouseAction(MouseActionType.LeftDoubleClick);
var offSet = ZoneSwitchHelper.GetOffset(dragElement, quarterX, quarterY);
Session.PressKey(Key.Shift);
Session.PerformMouseAction(MouseActionType.LeftDown);
Session.MoveMouseTo(endX, endY);
dragElement.DragAndHold(offSet.Dx, offSet.Dy);
Task.Delay(1000).Wait(); // Optional: Wait for a moment to ensure the window is in position
Tuple<int, int> pos = GetMousePosition();
string pixelInWindow = this.GetPixelColorString(pos.Item1, pos.Item2);
Session.ReleaseKey(Key.Shift);
Task.Delay(1000).Wait();
Task.Delay(1000).Wait(); // Optional: Wait for a moment to ensure the window is in position
string transPixel = this.GetPixelColorString(pos.Item1, pos.Item2);
dragElement.ReleaseDrag();
Session.PerformMouseAction(MouseActionType.LeftUp);
return (pixelInWindow, transPixel);
}

View File

@@ -271,7 +271,7 @@ namespace UITests_FancyZones
};
FancyZonesEditorHelper.Files.AppliedLayoutsIOHelper.WriteData(appliedLayouts.Serialize(appliedLayoutsWrapper));
RestartScopeExe("Hosts");
this.RestartScopeExe();
}
[TestMethod("FancyZones.Settings.TestApplyHotKey")]
@@ -598,12 +598,10 @@ namespace UITests_FancyZones
this.TryReaction();
int tries = 24;
Pull(tries, "down"); // Pull the setting page up to make sure the setting is visible
this.Find<ToggleSwitch>("FancyZonesQuickLayoutSwitch").Toggle(flag);
this.Find<ToggleSwitch>("Enable quick layout switch").Toggle(flag);
// Go back and forth to make sure settings applied
this.Find<NavigationViewItem>("Workspaces").Click();
Task.Delay(200).Wait();
this.Find<NavigationViewItem>("FancyZones").Click();
tries = 24;
Pull(tries, "up");
}
private void TryReaction()

View File

@@ -34,7 +34,7 @@ namespace UITests_FancyZones
Session.KillAllProcessesByName("PowerToys.FancyZonesEditor");
AppZoneHistory.DeleteFile();
RestartScopeExe("Hosts");
this.RestartScopeExe();
FancyZonesEditorHelper.Files.Restore();
// Set a custom layout with 1 subzones and clear app zone history
@@ -137,7 +137,7 @@ namespace UITests_FancyZones
Task.Delay(500).Wait(); // Optional: Wait for a moment to ensure window switch
activeWindowTitle = ZoneSwitchHelper.GetActiveWindowTitle();
Assert.AreEqual(postWindow, activeWindowTitle);
Assert.AreNotEqual(preWindow, activeWindowTitle);
Clean(); // close the windows
}
@@ -151,23 +151,9 @@ namespace UITests_FancyZones
var rect = Session.GetMainWindowRect();
var (targetX, targetY) = ZoneSwitchHelper.GetScreenMargins(rect, 4);
var offSet = ZoneSwitchHelper.GetOffset(hostsView, targetX, targetY);
// Snap first window (Hosts) to left zone using shift+drag with direct mouse movement
var hostsRect = hostsView.Rect ?? throw new InvalidOperationException("Failed to get hosts window rect");
int hostsStartX = hostsRect.Left + 70;
int hostsStartY = hostsRect.Top + 25;
// For a 2-column layout, left zone is at approximately 1/4 of screen width
int hostsEndX = rect.Left + (3 * (rect.Right - rect.Left) / 4);
int hostsEndY = rect.Top + ((rect.Bottom - rect.Top) / 2);
Session.MoveMouseTo(hostsStartX, hostsStartY);
Session.PerformMouseAction(MouseActionType.LeftDown);
Session.PressKey(Key.Shift);
Session.MoveMouseTo(hostsEndX, hostsEndY);
Session.PerformMouseAction(MouseActionType.LeftUp);
Session.ReleaseKey(Key.Shift);
Task.Delay(500).Wait(); // Wait for snap to complete
DragWithShift(hostsView, offSet);
string preWindow = ZoneSwitchHelper.GetActiveWindowTitle();
@@ -177,26 +163,11 @@ namespace UITests_FancyZones
Pane settingsView = Find<Pane>(By.Name("Non Client Input Sink Window"));
settingsView.DoubleClick(); // maximize the window
var windowRect = Session.GetMainWindowRect();
var settingsRect = settingsView.Rect ?? throw new InvalidOperationException("Failed to get settings window rect");
int settingsStartX = settingsRect.Left + 70;
int settingsStartY = settingsRect.Top + 25;
// For a 2-column layout, right zone is at approximately 3/4 of screen width
int settingsEndX = windowRect.Left + (3 * (windowRect.Right - windowRect.Left) / 4);
int settingsEndY = windowRect.Top + ((windowRect.Bottom - windowRect.Top) / 2);
Session.MoveMouseTo(settingsStartX, settingsStartY);
Session.PerformMouseAction(MouseActionType.LeftDown);
Session.PressKey(Key.Shift);
Session.MoveMouseTo(settingsEndX, settingsEndY);
Session.PerformMouseAction(MouseActionType.LeftUp);
Session.ReleaseKey(Key.Shift);
Task.Delay(500).Wait(); // Wait for snap to complete
DragWithShift(settingsView, offSet);
string appZoneHistoryJson = AppZoneHistory.GetData();
string? zoneIndexOfFileWindow = ZoneSwitchHelper.GetZoneIndexSetByAppName("PowerToys.Hosts.exe", appZoneHistoryJson);
string? zoneIndexOfFileWindow = ZoneSwitchHelper.GetZoneIndexSetByAppName("PowerToys.Hosts.exe", appZoneHistoryJson); // explorer.exe
string? zoneIndexOfPowertoys = ZoneSwitchHelper.GetZoneIndexSetByAppName("PowerToys.Settings.exe", appZoneHistoryJson);
// check the AppZoneHistory layout is set and in the same zone
@@ -205,6 +176,16 @@ namespace UITests_FancyZones
return (preWindow, powertoysWindowName);
}
private void DragWithShift(Pane settingsView, (int Dx, int Dy) offSet)
{
Session.PressKey(Key.Shift);
settingsView.DragAndHold(offSet.Dx, offSet.Dy);
Task.Delay(1000).Wait(); // Wait for drag to start (optional)
settingsView.ReleaseDrag();
Task.Delay(1000).Wait(); // Wait after drag (optional)
Session.ReleaseKey(Key.Shift);
}
private static readonly CustomLayouts.CustomLayoutListWrapper CustomLayoutsList = new CustomLayouts.CustomLayoutListWrapper
{
CustomLayouts = new List<CustomLayouts.CustomLayoutWrapper>
@@ -272,14 +253,11 @@ namespace UITests_FancyZones
this.Scroll(9, "Down"); // Pull the setting page up to make sure the setting is visible
bool switchWindowEnable = TestContext.TestName == "TestSwitchShortCutDisable" ? false : true;
this.Find<ToggleSwitch>("FancyZonesWindowSwitchingToggle").Toggle(switchWindowEnable);
this.Find<ToggleSwitch>("Switch between windows in the current zone").Toggle(switchWindowEnable);
// Go back and forth to make sure settings applied
this.Find<NavigationViewItem>("Workspaces").Click();
Task.Delay(200).Wait();
this.Find<NavigationViewItem>("FancyZones").Click();
this.Find<Button>("Open layout editor").Click(false, 500, 5000);
Task.Delay(500).Wait(); // Wait for the setting to be applied
this.Scroll(9, "Up"); // Pull the setting page down to make sure the setting is visible
this.Find<Button>("Launch layout editor").Click(false, 500, 5000);
this.Session.Attach(PowerToysModule.FancyZone);
// pipeline machine may have an unstable delays, causing the custom layout to be unavailable as we set. then A retry is required.
@@ -295,7 +273,7 @@ namespace UITests_FancyZones
this.Find<Microsoft.PowerToys.UITest.Button>("Close").Click();
this.Session.Attach(PowerToysModule.PowerToysSettings);
SetupCustomLayouts();
this.Find<Microsoft.PowerToys.UITest.Button>("Open layout editor").Click(false, 5000, 5000);
this.Find<Microsoft.PowerToys.UITest.Button>("Launch layout editor").Click(false, 5000, 5000);
this.Session.Attach(PowerToysModule.FancyZone);
// customLayoutData = FancyZonesEditorHelper.Files.CustomLayoutsIOHelper.GetData();
@@ -323,11 +301,11 @@ namespace UITests_FancyZones
Task.Delay(1000).Wait();
this.Find<ToggleSwitch>("Enable Hosts File Editor").Toggle(true);
this.Find<ToggleSwitch>("Open as administrator").Toggle(launchAsAdmin);
this.Find<ToggleSwitch>("Launch as administrator").Toggle(launchAsAdmin);
this.Find<ToggleSwitch>("Show a warning at startup").Toggle(showWarning);
// launch Hosts File Editor
this.Find<Button>("Open Hosts File Editor").Click();
this.Find<Button>("Launch Hosts File Editor").Click();
Task.Delay(5000).Wait();
}

View File

@@ -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);
}
}

View File

@@ -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" />

View File

@@ -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()
{
}
}
}

View File

@@ -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();
}
}

View File

@@ -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);

View File

@@ -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>

View File

@@ -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>

View File

@@ -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();
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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; }
}
}

View File

@@ -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);
}
}

Some files were not shown because too many files have changed in this diff Show More