CmdPal: Improve loading of application icons (uwp and jumbo icons) - part 2 (#44973)

<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

This PR improves icons for app items:
- Refactors icon detection and selection from the AppX manifest out of
`UWPApplication`
- Prefer *unplated* UWP app logos so icons no longer appear smaller than
expected
- Adds an icon loader based on `IShellItemImageFactory` to correctly
load large icons
- Jumbo icons loaded from shortcuts are now crisp
- Jumbo icons loaded from shortcuts are no longer scaled down
- Refactors detail loading in `AppListItem` to prevent potential
deadlocks
- Makes PWA icons more crisp
- Fixes fallback item (now it gets used not only when the icon is null,
but also when it's empty).

<table>

<thead>
<tr>
<th></th>
<th>Old</th>
<th>New</th>
</tr>
</thead>

<tr>
<td>1</td>
<td>
<img width="830" height="495" alt="image"
src="https://github.com/user-attachments/assets/bc9875bd-6a8b-4a3d-88e1-07a655a5a5cd"
/>
</td>
<td>
<img width="750" height="533" alt="image"
src="https://github.com/user-attachments/assets/a82ed464-b925-4d0c-95c4-6c04859e886e"
/>
</td>
</tr>

<tr>
<td>2</td>
<td>
<img width="814" height="233" alt="image"
src="https://github.com/user-attachments/assets/d560d3c0-ffc5-4178-a610-4e3b3c7107c8"
/>
</td>
<td>
<img width="760" height="299" alt="image"
src="https://github.com/user-attachments/assets/f29c825e-324f-46f1-b6bb-6edcf286fc9a"
/>

</td>
</tr>


<tr>
<td>3</td>
<td>
<img width="813" height="262" alt="image"
src="https://github.com/user-attachments/assets/d94f724d-ec26-48c8-bb8a-1b10f6a0f7eb"
/>
</td>
<td>
<img width="762" height="260" alt="image"
src="https://github.com/user-attachments/assets/76c5debb-baac-417e-8aba-9cec198e742c"
/>
</td>
</tr>

<tr>
<td>4</td>
<td>
<img width="819" height="250" alt="image"
src="https://github.com/user-attachments/assets/5f16d714-56d8-42f2-ad8b-1c2be6570e5c"
/>
</td>
<td>
<img width="747" height="244" alt="image"
src="https://github.com/user-attachments/assets/485c72cf-ef39-4c05-afdd-877f0a47f51a"
/>
</td>
</tr>


<tr>
<td>5</td>
<td>
<img width="815" height="327" alt="image"
src="https://github.com/user-attachments/assets/4108e36a-5950-43c9-bdff-6a9f58dadcf6"
/>
</td>
<td>
<img width="762" height="272" alt="image"
src="https://github.com/user-attachments/assets/804a3159-a165-4a48-87f6-15849f5f4516"
/>
</td>
</tr>

<tr>
<td>6</td>
<td>
<img width="809" height="257" alt="image"
src="https://github.com/user-attachments/assets/93ad8241-1d75-415f-b08c-4161c0905e41"
/>
</td>
<td>
<img width="756" height="231" alt="image"
src="https://github.com/user-attachments/assets/a0c9bb44-7151-438d-a811-82d5e2080f44"
/>
</td>
</tr>

<tr>
<td></td>
<td>
</td>
<td>
</td>
</tr>

</table>

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] Closes: #44970
- [x] Closes: #43320
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
This commit is contained in:
Jiří Polášek
2026-02-02 18:53:40 +01:00
committed by GitHub
parent 4d1f92199c
commit 18c6d6b0f3
8 changed files with 601 additions and 371 deletions

View File

@@ -33,6 +33,8 @@ public sealed class AppItem
public string? FullExecutablePath { get; set; }
public string? JumboIconPath { get; set; }
public AppItem()
{
}

View File

@@ -8,6 +8,7 @@ using System.Threading.Tasks;
using ManagedCommon;
using Microsoft.CmdPal.Core.Common.Helpers;
using Microsoft.CmdPal.Ext.Apps.Commands;
using Microsoft.CmdPal.Ext.Apps.Helpers;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -17,6 +18,7 @@ public sealed partial class AppListItem : ListItem
{
private readonly AppCommand _appCommand;
private readonly AppItem _app;
private readonly Lazy<Task<IconInfo?>> _iconLoadTask;
private readonly Lazy<Task<Details>> _detailsLoadTask;
@@ -66,7 +68,7 @@ public sealed partial class AppListItem : ListItem
MoreCommands = AddPinCommands(_app.Commands!, isPinned);
_detailsLoadTask = new Lazy<Task<Details>>(BuildDetails);
_iconLoadTask = new Lazy<Task<IconInfo?>>(async () => await FetchIcon(useThumbnails));
_iconLoadTask = new Lazy<Task<IconInfo?>>(async () => await FetchIcon(useThumbnails).ConfigureAwait(false));
}
private async Task LoadDetailsAsync()
@@ -85,7 +87,7 @@ public sealed partial class AppListItem : ListItem
{
try
{
Icon = _appCommand.Icon = await _iconLoadTask.Value ?? Icons.GenericAppIcon;
Icon = _appCommand.Icon = CoalesceIcon(await _iconLoadTask.Value);
}
catch (Exception ex)
{
@@ -93,6 +95,21 @@ public sealed partial class AppListItem : ListItem
}
}
private static IconInfo CoalesceIcon(IconInfo? value)
{
return CoalesceIcon(value, Icons.GenericAppIcon)!;
}
private static IconInfo? CoalesceIcon(IconInfo? value, IconInfo? replacement)
{
return IconIsNullOrEmpty(value) ? replacement : value;
}
private static bool IconIsNullOrEmpty(IconInfo? value)
{
return value == null || (string.IsNullOrEmpty(value.Light?.Icon) && value.Light?.Data is null) || (string.IsNullOrEmpty(value.Dark?.Icon) && value.Dark?.Data is null);
}
private async Task<Details> BuildDetails()
{
// Build metadata, with app type, path, etc.
@@ -107,24 +124,49 @@ public sealed partial class AppListItem : ListItem
metadata.Add(new DetailsElement() { Key = "[DEBUG] AppIdentifier", Data = new DetailsLink() { Text = _app.AppIdentifier } });
metadata.Add(new DetailsElement() { Key = "[DEBUG] ExePath", Data = new DetailsLink() { Text = _app.ExePath } });
metadata.Add(new DetailsElement() { Key = "[DEBUG] IcoPath", Data = new DetailsLink() { Text = _app.IcoPath } });
metadata.Add(new DetailsElement() { Key = "[DEBUG] JumboIconPath", Data = new DetailsLink() { Text = _app.JumboIconPath ?? "(null)" } });
#endif
// Icon
IconInfo? heroImage = null;
if (_app.IsPackaged)
{
heroImage = new IconInfo(_app.IcoPath);
heroImage = new IconInfo(_app.JumboIconPath ?? _app.IcoPath);
}
else
{
// Get the icon from the system
if (!string.IsNullOrEmpty(_app.JumboIconPath))
{
var randomAccessStream = await IconExtractor.GetIconStreamAsync(_app.JumboIconPath, 64);
if (randomAccessStream != null)
{
heroImage = IconInfo.FromStream(randomAccessStream);
}
}
if (IconIsNullOrEmpty(heroImage) && !string.IsNullOrEmpty(_app.IcoPath))
{
var randomAccessStream = await IconExtractor.GetIconStreamAsync(_app.IcoPath, 64);
if (randomAccessStream != null)
{
heroImage = IconInfo.FromStream(randomAccessStream);
}
}
// do nothing if we fail to load an icon.
// Logging it would be too NOISY, there's really no need.
if (!string.IsNullOrEmpty(_app.IcoPath))
if (IconIsNullOrEmpty(heroImage) && !string.IsNullOrEmpty(_app.JumboIconPath))
{
heroImage = await TryLoadThumbnail(_app.JumboIconPath, jumbo: true, logOnFailure: false);
}
if (IconIsNullOrEmpty(heroImage) && !string.IsNullOrEmpty(_app.IcoPath))
{
heroImage = await TryLoadThumbnail(_app.IcoPath, jumbo: true, logOnFailure: false);
}
if (heroImage == null && !string.IsNullOrEmpty(_app.ExePath))
if (IconIsNullOrEmpty(heroImage) && !string.IsNullOrEmpty(_app.ExePath))
{
heroImage = await TryLoadThumbnail(_app.ExePath, jumbo: true, logOnFailure: false);
}
@@ -133,8 +175,8 @@ public sealed partial class AppListItem : ListItem
return new Details()
{
Title = this.Title,
HeroImage = heroImage ?? this.Icon ?? Icons.GenericAppIcon,
Metadata = metadata.ToArray(),
HeroImage = CoalesceIcon(CoalesceIcon(heroImage, this.Icon as IconInfo)),
Metadata = [..metadata],
};
}
@@ -154,7 +196,7 @@ public sealed partial class AppListItem : ListItem
icon = await TryLoadThumbnail(_app.IcoPath, jumbo: false, logOnFailure: true);
}
if (icon == null && !string.IsNullOrEmpty(_app.ExePath))
if (IconIsNullOrEmpty(icon) && !string.IsNullOrEmpty(_app.ExePath))
{
icon = await TryLoadThumbnail(_app.ExePath, jumbo: false, logOnFailure: true);
}
@@ -196,22 +238,25 @@ public sealed partial class AppListItem : ListItem
private async Task<IconInfo?> TryLoadThumbnail(string path, bool jumbo, bool logOnFailure)
{
try
return await Task.Run(async () =>
{
var stream = await ThumbnailHelper.GetThumbnail(path, jumbo);
if (stream is not null)
try
{
return IconInfo.FromStream(stream);
var stream = await ThumbnailHelper.GetThumbnail(path, jumbo).ConfigureAwait(false);
if (stream is not null)
{
return IconInfo.FromStream(stream);
}
}
}
catch (Exception ex)
{
if (logOnFailure)
catch (Exception ex)
{
Logger.LogDebug($"Failed to load icon {path} for {AppIdentifier}:\n{ex}");
if (logOnFailure)
{
Logger.LogDebug($"Failed to load icon {path} for {AppIdentifier}:\n{ex}");
}
}
}
return null;
return null;
}).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,295 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.IO;
using Microsoft.CmdPal.Ext.Apps.Programs;
using Microsoft.CmdPal.Ext.Apps.Utils;
namespace Microsoft.CmdPal.Ext.Apps.Helpers;
internal static class AppxIconLoader
{
private const string ContrastWhite = "contrast-white";
private const string ContrastBlack = "contrast-black";
private static readonly Dictionary<UWP.PackageVersion, List<int>> _scaleFactors = new()
{
{ UWP.PackageVersion.Windows10, [100, 125, 150, 200, 400] },
{ UWP.PackageVersion.Windows81, [100, 120, 140, 160, 180] },
{ UWP.PackageVersion.Windows8, [100] },
};
private static readonly List<int> TargetSizes = [16, 24, 30, 36, 44, 60, 72, 96, 128, 180, 256];
private static IconSearchResult GetScaleIcons(
string path,
string colorscheme,
UWP.PackageVersion packageVersion,
bool highContrast = false)
{
var extension = Path.GetExtension(path);
if (extension is null)
{
return IconSearchResult.NotFound();
}
var end = path.Length - extension.Length;
var prefix = path[..end];
if (!_scaleFactors.TryGetValue(packageVersion, out var factors))
{
return IconSearchResult.NotFound();
}
var logoType = highContrast ? LogoType.HighContrast : LogoType.Colored;
// Check from highest scale factor to lowest for best quality
for (var i = factors.Count - 1; i >= 0; i--)
{
var factor = factors[i];
string[] pathsToTry = highContrast
?
[
$"{prefix}.scale-{factor}_{colorscheme}{extension}",
$"{prefix}.{colorscheme}_scale-{factor}{extension}",
]
:
[
$"{prefix}.scale-{factor}{extension}",
];
foreach (var p in pathsToTry)
{
if (File.Exists(p))
{
return IconSearchResult.FoundScaled(p, logoType);
}
}
}
// Check base path (100% scale) as last resort
if (!highContrast && File.Exists(path))
{
return IconSearchResult.FoundScaled(path, logoType);
}
return IconSearchResult.NotFound();
}
private static IconSearchResult GetTargetSizeIcon(
string path,
string colorscheme,
bool highContrast = false,
int appIconSize = 36,
double maxSizeCoefficient = 8.0)
{
var extension = Path.GetExtension(path);
if (extension is null)
{
return IconSearchResult.NotFound();
}
var end = path.Length - extension.Length;
var prefix = path[..end];
var pathSizePairs = new List<(string Path, int Size)>();
foreach (var size in TargetSizes)
{
if (highContrast)
{
pathSizePairs.Add(($"{prefix}.targetsize-{size}_{colorscheme}{extension}", size));
pathSizePairs.Add(($"{prefix}.{colorscheme}_targetsize-{size}{extension}", size));
}
else
{
pathSizePairs.Add(($"{prefix}.targetsize-{size}_altform-unplated{extension}", size));
pathSizePairs.Add(($"{prefix}.targetsize-{size}{extension}", size));
}
}
var maxAllowedSize = (int)(appIconSize * maxSizeCoefficient);
var logoType = highContrast ? LogoType.HighContrast : LogoType.Colored;
string? bestLargerPath = null;
var bestLargerSize = int.MaxValue;
string? bestSmallerPath = null;
var bestSmallerSize = 0;
foreach (var (p, size) in pathSizePairs)
{
if (!File.Exists(p))
{
continue;
}
if (size >= appIconSize && size <= maxAllowedSize)
{
if (size < bestLargerSize)
{
bestLargerSize = size;
bestLargerPath = p;
}
}
else if (size < appIconSize)
{
if (size > bestSmallerSize)
{
bestSmallerSize = size;
bestSmallerPath = p;
}
}
}
if (bestLargerPath is not null)
{
return IconSearchResult.FoundTargetSize(bestLargerPath, logoType, bestLargerSize);
}
if (bestSmallerPath is not null)
{
return IconSearchResult.FoundTargetSize(bestSmallerPath, logoType, bestSmallerSize);
}
return IconSearchResult.NotFound();
}
private static IconSearchResult GetColoredIcon(
string path,
string colorscheme,
int iconSize,
UWP package)
{
// First priority: targetsize icons (we know the exact size)
var targetResult = GetTargetSizeIcon(path, colorscheme, highContrast: false, appIconSize: iconSize);
if (targetResult.MeetsMinimumSize(iconSize))
{
return targetResult;
}
var hcTargetResult = GetTargetSizeIcon(path, colorscheme, highContrast: true, appIconSize: iconSize);
if (hcTargetResult.MeetsMinimumSize(iconSize))
{
return hcTargetResult;
}
// Second priority: scale icons (size unknown, but higher scale = likely better)
var scaleResult = GetScaleIcons(path, colorscheme, package.Version, highContrast: false);
if (scaleResult.IsFound)
{
return scaleResult;
}
var hcScaleResult = GetScaleIcons(path, colorscheme, package.Version, highContrast: true);
if (hcScaleResult.IsFound)
{
return hcScaleResult;
}
// Last resort: return undersized targetsize if we found one
if (targetResult.IsFound)
{
return targetResult;
}
if (hcTargetResult.IsFound)
{
return hcTargetResult;
}
return IconSearchResult.NotFound();
}
private static IconSearchResult SetHighContrastIcon(
string path,
string colorscheme,
int iconSize,
UWP package)
{
// First priority: HC targetsize icons (we know the exact size)
var hcTargetResult = GetTargetSizeIcon(path, colorscheme, highContrast: true, appIconSize: iconSize);
if (hcTargetResult.MeetsMinimumSize(iconSize))
{
return hcTargetResult;
}
var targetResult = GetTargetSizeIcon(path, colorscheme, highContrast: false, appIconSize: iconSize);
if (targetResult.MeetsMinimumSize(iconSize))
{
return targetResult;
}
// Second priority: scale icons
var hcScaleResult = GetScaleIcons(path, colorscheme, package.Version, highContrast: true);
if (hcScaleResult.IsFound)
{
return hcScaleResult;
}
var scaleResult = GetScaleIcons(path, colorscheme, package.Version, highContrast: false);
if (scaleResult.IsFound)
{
return scaleResult;
}
// Last resort: undersized targetsize
if (hcTargetResult.IsFound)
{
return hcTargetResult;
}
if (targetResult.IsFound)
{
return targetResult;
}
return IconSearchResult.NotFound();
}
/// <summary>
/// Loads an icon from a UWP package, attempting to find the best match for the requested size.
/// </summary>
/// <param name="uri">The relative URI to the logo asset.</param>
/// <param name="theme">The current theme.</param>
/// <param name="iconSize">The requested icon size in pixels.</param>
/// <param name="package">The UWP package.</param>
/// <returns>
/// An IconSearchResult. Use <see cref="IconSearchResult.MeetsMinimumSize"/> to check if
/// the icon is confirmed to be large enough, or <see cref="IconSearchResult.IsTargetSizeIcon"/>
/// to determine if the size is known.
/// </returns>
internal static IconSearchResult LogoPathFromUri(
string uri,
Theme theme,
int iconSize,
UWP package)
{
var path = Path.Combine(package.Location, uri);
var logo = Probe(theme, path, iconSize, package);
if (!logo.IsFound && !uri.Contains('\\', StringComparison.Ordinal))
{
path = Path.Combine(package.Location, "Assets", uri);
logo = Probe(theme, path, iconSize, package);
}
return logo;
}
private static IconSearchResult Probe(Theme theme, string path, int iconSize, UWP package)
{
return theme switch
{
Theme.HighContrastBlack or Theme.HighContrastOne or Theme.HighContrastTwo
=> SetHighContrastIcon(path, ContrastBlack, iconSize, package),
Theme.HighContrastWhite
=> SetHighContrastIcon(path, ContrastWhite, iconSize, package),
Theme.Light
=> GetColoredIcon(path, ContrastWhite, iconSize, package),
_
=> GetColoredIcon(path, ContrastBlack, iconSize, package),
};
}
}

View File

@@ -0,0 +1,132 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Threading.Tasks;
using ManagedCommon;
using Windows.Graphics.Imaging;
using Windows.Storage.Streams;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.Graphics.Gdi;
using Windows.Win32.UI.Shell;
namespace Microsoft.CmdPal.Ext.Apps.Helpers;
internal static class IconExtractor
{
public static async Task<IRandomAccessStream?> GetIconStreamAsync(string path, int size)
{
var bitmap = GetIcon(path, size);
if (bitmap == null)
{
return null;
}
var stream = new InMemoryRandomAccessStream();
var encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, stream);
encoder.SetSoftwareBitmap(bitmap);
await encoder.FlushAsync();
stream.Seek(0);
return stream;
}
public static unsafe SoftwareBitmap? GetIcon(string path, int size)
{
IShellItemImageFactory* factory = null;
HBITMAP hBitmap = default;
try
{
fixed (char* pPath = path)
{
var iid = IShellItemImageFactory.IID_Guid;
var hr = PInvoke.SHCreateItemFromParsingName(
pPath,
null,
&iid,
(void**)&factory);
if (hr.Failed || factory == null)
{
return null;
}
}
var requestedSize = new SIZE { cx = size, cy = size };
var hr2 = factory->GetImage(
requestedSize,
SIIGBF.SIIGBF_ICONONLY | SIIGBF.SIIGBF_BIGGERSIZEOK | SIIGBF.SIIGBF_CROPTOSQUARE,
&hBitmap);
if (hr2.Failed || hBitmap.IsNull)
{
return null;
}
return CreateSoftwareBitmap(hBitmap, size);
}
catch (Exception ex)
{
Logger.LogError($"Failed to load icon from path='{path}',size={size}", ex);
return null;
}
finally
{
if (!hBitmap.IsNull)
{
PInvoke.DeleteObject(hBitmap);
}
if (factory != null)
{
factory->Release();
}
}
}
private static unsafe SoftwareBitmap CreateSoftwareBitmap(HBITMAP hBitmap, int size)
{
var pixels = new byte[size * size * 4];
var bmi = new BITMAPINFO
{
bmiHeader = new BITMAPINFOHEADER
{
biSize = (uint)sizeof(BITMAPINFOHEADER),
biWidth = size,
biHeight = -size,
biPlanes = 1,
biBitCount = 32,
biCompression = 0,
},
};
var hdc = PInvoke.GetDC(default);
try
{
fixed (byte* pPixels = pixels)
{
_ = PInvoke.GetDIBits(
hdc,
hBitmap,
0,
(uint)size,
pPixels,
&bmi,
DIB_USAGE.DIB_RGB_COLORS);
}
}
finally
{
_ = PInvoke.ReleaseDC(default, hdc);
}
var bitmap = new SoftwareBitmap(BitmapPixelFormat.Bgra8, size, size, BitmapAlphaMode.Premultiplied);
bitmap.CopyFromBuffer(pixels.AsBuffer());
return bitmap;
}
}

View File

@@ -0,0 +1,44 @@
// 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.Ext.Apps.Programs;
namespace Microsoft.CmdPal.Ext.Apps.Helpers;
/// <summary>
/// Result of an icon search operation.
/// </summary>
internal readonly record struct IconSearchResult(
string? LogoPath,
LogoType LogoType,
bool IsTargetSizeIcon,
int? KnownSize = null)
{
/// <summary>
/// Gets a value indicating whether an icon was found.
/// </summary>
public bool IsFound => LogoPath is not null;
/// <summary>
/// Returns true if we can confirm the icon meets the minimum size.
/// Only possible for targetsize icons where the size is encoded in the filename.
/// </summary>
public bool MeetsMinimumSize(int minimumSize) =>
IsTargetSizeIcon && KnownSize >= minimumSize;
/// <summary>
/// Returns true if we know the icon is undersized.
/// Returns false if not found, or if size is unknown (scale-based icons).
/// </summary>
public bool IsKnownUndersized(int minimumSize) =>
IsTargetSizeIcon && KnownSize < minimumSize;
public static IconSearchResult NotFound() => new(null, default, false);
public static IconSearchResult FoundTargetSize(string path, LogoType logoType, int size)
=> new(path, logoType, IsTargetSizeIcon: true, size);
public static IconSearchResult FoundScaled(string path, LogoType logoType)
=> new(path, logoType, IsTargetSizeIcon: false);
}

View File

@@ -18,3 +18,9 @@ ShellLink
IPersistFile
CoTaskMemFree
IUnknown
IShellItemImageFactory
DeleteObject
GetDIBits
GetDC
ReleaseDC
SIIGBF

View File

@@ -8,6 +8,7 @@ using System.IO.Abstractions;
using System.Xml;
using ManagedCommon;
using Microsoft.CmdPal.Ext.Apps.Commands;
using Microsoft.CmdPal.Ext.Apps.Helpers;
using Microsoft.CmdPal.Ext.Apps.Properties;
using Microsoft.CmdPal.Ext.Apps.Utils;
using Microsoft.CommandPalette.Extensions;
@@ -23,8 +24,10 @@ namespace Microsoft.CmdPal.Ext.Apps.Programs;
[Serializable]
public class UWPApplication : IUWPApplication
{
private const int ListIconSize = 20;
private const int JumboIconSize = 64;
private static readonly IFileSystem FileSystem = new FileSystem();
private static readonly IPath Path = FileSystem.Path;
private static readonly IFile File = FileSystem.File;
public string AppListEntry { get; set; } = string.Empty;
@@ -56,13 +59,15 @@ public class UWPApplication : IUWPApplication
public LogoType LogoType { get; set; }
public string JumboLogoPath { get; set; } = string.Empty;
public LogoType JumboLogoType { get; set; }
public UWP Package { get; set; }
private string logoUri;
private string _logoUri;
private const string ContrastWhite = "contrast-white";
private const string ContrastBlack = "contrast-black";
private string _jumboLogoUri;
// Function to set the subtitle based on the Type of application
public static string Type()
@@ -154,7 +159,8 @@ public class UWPApplication : IUWPApplication
DisplayName = ResourceFromPri(package.FullName, DisplayName);
Description = ResourceFromPri(package.FullName, Description);
logoUri = LogoUriFromManifest(manifestApp);
_logoUri = LogoUriFromManifest(manifestApp);
_jumboLogoUri = LogoUriFromManifest(manifestApp, jumbo: true);
Enabled = true;
CanRunElevated = IfApplicationCanRunElevated();
@@ -280,16 +286,24 @@ public class UWPApplication : IUWPApplication
}
}
private static readonly Dictionary<PackageVersion, string> _logoKeyFromVersion = new Dictionary<PackageVersion, string>
private static readonly Dictionary<PackageVersion, string> _smallLogoKeyFromVersion = new Dictionary<PackageVersion, string>
{
{ PackageVersion.Windows10, "Square44x44Logo" },
{ PackageVersion.Windows81, "Square30x30Logo" },
{ PackageVersion.Windows8, "SmallLogo" },
};
internal unsafe string LogoUriFromManifest(IAppxManifestApplication* app)
private static readonly Dictionary<PackageVersion, string> _largeLogoKeyFromVersion = new Dictionary<PackageVersion, string>
{
if (_logoKeyFromVersion.TryGetValue(Package.Version, out var key))
{ PackageVersion.Windows10, "Square150x150Logo" },
{ PackageVersion.Windows81, "Square150x150Logo" },
{ PackageVersion.Windows8, "Logo" },
};
internal unsafe string LogoUriFromManifest(IAppxManifestApplication* app, bool jumbo = false)
{
var logoMap = jumbo ? _largeLogoKeyFromVersion : _smallLogoKeyFromVersion;
if (logoMap.TryGetValue(Package.Version, out var key))
{
var hr = app->GetStringValue(key, out var logoUriFromAppPtr);
return ComFreeHelper.GetStringAndFree(hr, logoUriFromAppPtr);
@@ -302,257 +316,55 @@ public class UWPApplication : IUWPApplication
public void UpdateLogoPath(Theme theme)
{
LogoPathFromUri(logoUri, theme);
}
// scale factors on win10: https://learn.microsoft.com/windows/uwp/controls-and-patterns/tiles-and-notifications-app-assets#asset-size-tables,
private static readonly Dictionary<PackageVersion, List<int>> _scaleFactors = new Dictionary<PackageVersion, List<int>>
// Update small logo
var logo = AppxIconLoader.LogoPathFromUri(_logoUri, theme, ListIconSize, Package);
if (logo.IsFound)
{
{ PackageVersion.Windows10, new List<int> { 100, 125, 150, 200, 400 } },
{ PackageVersion.Windows81, new List<int> { 100, 120, 140, 160, 180 } },
{ PackageVersion.Windows8, new List<int> { 100 } },
};
private bool SetScaleIcons(string path, string colorscheme, bool highContrast = false)
{
var extension = Path.GetExtension(path);
if (extension is not null)
{
var end = path.Length - extension.Length;
var prefix = path.Substring(0, end);
var paths = new List<string> { };
if (!highContrast)
{
paths.Add(path);
}
if (_scaleFactors.TryGetValue(Package.Version, out var factors))
{
foreach (var factor in factors)
{
if (highContrast)
{
paths.Add($"{prefix}.scale-{factor}_{colorscheme}{extension}");
paths.Add($"{prefix}.{colorscheme}_scale-{factor}{extension}");
}
else
{
paths.Add($"{prefix}.scale-{factor}{extension}");
}
}
}
// By working from the highest resolution to the lowest, we make
// sure that we use the highest quality possible icon for the app.
//
// FirstOrDefault would result in us using the 1x scaled icon
// always, which is usually too small for our needs.
for (var i = paths.Count - 1; i >= 0; i--)
{
if (File.Exists(paths[i]))
{
LogoPath = paths[i];
if (highContrast)
{
LogoType = LogoType.HighContrast;
}
else
{
LogoType = LogoType.Colored;
}
return true;
}
}
}
return false;
}
private bool SetTargetSizeIcon(string path, string colorscheme, bool highContrast = false)
{
var extension = Path.GetExtension(path);
if (extension is not null)
{
var end = path.Length - extension.Length;
var prefix = path.Substring(0, end);
var paths = new List<string> { };
const int appIconSize = 36;
var targetSizes = new List<int> { 16, 24, 30, 36, 44, 60, 72, 96, 128, 180, 256 };
var pathFactorPairs = new Dictionary<string, int>();
foreach (var factor in targetSizes)
{
if (highContrast)
{
var suffixThemePath = $"{prefix}.targetsize-{factor}_{colorscheme}{extension}";
var prefixThemePath = $"{prefix}.{colorscheme}_targetsize-{factor}{extension}";
paths.Add(suffixThemePath);
paths.Add(prefixThemePath);
pathFactorPairs.Add(suffixThemePath, factor);
pathFactorPairs.Add(prefixThemePath, factor);
}
else
{
var simplePath = $"{prefix}.targetsize-{factor}{extension}";
var altformUnPlatedPath = $"{prefix}.targetsize-{factor}_altform-unplated{extension}";
paths.Add(simplePath);
paths.Add(altformUnPlatedPath);
pathFactorPairs.Add(simplePath, factor);
pathFactorPairs.Add(altformUnPlatedPath, factor);
}
}
// Sort paths by distance to desired app icon size
var selectedIconPath = string.Empty;
var closestDistance = int.MaxValue;
foreach (var p in paths)
{
if (File.Exists(p) && pathFactorPairs.TryGetValue(p, out var factor))
{
var distance = Math.Abs(factor - appIconSize);
if (distance < closestDistance)
{
closestDistance = distance;
selectedIconPath = p;
}
}
}
if (!string.IsNullOrEmpty(selectedIconPath))
{
LogoPath = selectedIconPath;
if (highContrast)
{
LogoType = LogoType.HighContrast;
}
else
{
LogoType = LogoType.Colored;
}
return true;
}
}
return false;
}
private bool SetColoredIcon(string path, string colorscheme)
{
var isSetColoredScaleIcon = SetScaleIcons(path, colorscheme);
if (isSetColoredScaleIcon)
{
return true;
}
var isSetColoredTargetIcon = SetTargetSizeIcon(path, colorscheme);
if (isSetColoredTargetIcon)
{
return true;
}
var isSetHighContrastScaleIcon = SetScaleIcons(path, colorscheme, true);
if (isSetHighContrastScaleIcon)
{
return true;
}
var isSetHighContrastTargetIcon = SetTargetSizeIcon(path, colorscheme, true);
if (isSetHighContrastTargetIcon)
{
return true;
}
return false;
}
private bool SetHighContrastIcon(string path, string colorscheme)
{
var isSetHighContrastScaleIcon = SetScaleIcons(path, colorscheme, true);
if (isSetHighContrastScaleIcon)
{
return true;
}
var isSetHighContrastTargetIcon = SetTargetSizeIcon(path, colorscheme, true);
if (isSetHighContrastTargetIcon)
{
return true;
}
var isSetColoredScaleIcon = SetScaleIcons(path, colorscheme);
if (isSetColoredScaleIcon)
{
return true;
}
var isSetColoredTargetIcon = SetTargetSizeIcon(path, colorscheme);
if (isSetColoredTargetIcon)
{
return true;
}
return false;
}
internal void LogoPathFromUri(string uri, Theme theme)
{
// all https://learn.microsoft.com/windows/uwp/controls-and-patterns/tiles-and-notifications-app-assets
// windows 10 https://msdn.microsoft.com/library/windows/apps/dn934817.aspx
// windows 8.1 https://msdn.microsoft.com/library/windows/apps/hh965372.aspx#target_size
// windows 8 https://msdn.microsoft.com/library/windows/apps/br211475.aspx
string path;
bool isLogoUriSet;
// Using Ordinal since this is used internally with uri
if (uri.Contains('\\', StringComparison.Ordinal))
{
path = Path.Combine(Package.Location, uri);
LogoPath = logo.LogoPath!;
LogoType = logo.LogoType;
}
else
{
// for C:\Windows\MiracastView, etc.
path = Path.Combine(Package.Location, "Assets", uri);
}
switch (theme)
{
case Theme.HighContrastBlack:
case Theme.HighContrastOne:
case Theme.HighContrastTwo:
isLogoUriSet = SetHighContrastIcon(path, ContrastBlack);
break;
case Theme.HighContrastWhite:
isLogoUriSet = SetHighContrastIcon(path, ContrastWhite);
break;
case Theme.Light:
isLogoUriSet = SetColoredIcon(path, ContrastWhite);
break;
default:
isLogoUriSet = SetColoredIcon(path, ContrastBlack);
break;
}
if (!isLogoUriSet)
{
LogoPath = string.Empty;
LogoType = LogoType.Error;
}
// Jumbo logo ... small logo can actually provide better result
var jumboLogo = AppxIconLoader.LogoPathFromUri(_logoUri, theme, JumboIconSize, Package);
if (jumboLogo.IsFound)
{
JumboLogoPath = jumboLogo.LogoPath!;
JumboLogoType = jumboLogo.LogoType;
}
else
{
JumboLogoPath = string.Empty;
JumboLogoType = LogoType.Error;
}
if (!jumboLogo.MeetsMinimumSize(JumboIconSize) || !jumboLogo.IsFound)
{
var jumboLogoAlt = AppxIconLoader.LogoPathFromUri(_jumboLogoUri, theme, JumboIconSize, Package);
if (jumboLogoAlt.IsFound)
{
JumboLogoPath = jumboLogoAlt.LogoPath!;
JumboLogoType = jumboLogoAlt.LogoType;
}
}
}
public AppItem ToAppItem()
{
var app = this;
var iconPath = app.LogoType != LogoType.Error ? app.LogoPath : string.Empty;
var item = new AppItem()
var jumboIconPath = app.JumboLogoType != LogoType.Error ? app.JumboLogoPath : string.Empty;
var item = new AppItem
{
Name = app.Name,
Subtitle = app.Description,
Type = UWPApplication.Type(),
IcoPath = iconPath,
JumboIconPath = jumboIconPath,
DirPath = app.Location,
UserModelId = app.UserModelId,
IsPackaged = true,
@@ -563,116 +375,6 @@ public class UWPApplication : IUWPApplication
return item;
}
/*
public ImageSource Logo()
{
if (LogoType == LogoType.Colored)
{
var logo = ImageFromPath(LogoPath);
var platedImage = PlatedImage(logo);
return platedImage;
}
else
{
return ImageFromPath(LogoPath);
}
}
private const int _dpiScale100 = 96;
private ImageSource PlatedImage(BitmapImage image)
{
if (!string.IsNullOrEmpty(BackgroundColor))
{
string currentBackgroundColor;
if (BackgroundColor == "transparent")
{
// Using InvariantCulture since this is internal
currentBackgroundColor = SystemParameters.WindowGlassBrush.ToString(CultureInfo.InvariantCulture);
}
else
{
currentBackgroundColor = BackgroundColor;
}
var padding = 8;
var width = image.Width + (2 * padding);
var height = image.Height + (2 * padding);
var x = 0;
var y = 0;
var group = new DrawingGroup();
var converted = ColorConverter.ConvertFromString(currentBackgroundColor);
if (converted is not null)
{
var color = (Color)converted;
var brush = new SolidColorBrush(color);
var pen = new Pen(brush, 1);
var backgroundArea = new Rect(0, 0, width, height);
var rectangleGeometry = new RectangleGeometry(backgroundArea, 8, 8);
var rectDrawing = new GeometryDrawing(brush, pen, rectangleGeometry);
group.Children.Add(rectDrawing);
var imageArea = new Rect(x + padding, y + padding, image.Width, image.Height);
var imageDrawing = new ImageDrawing(image, imageArea);
group.Children.Add(imageDrawing);
// http://stackoverflow.com/questions/6676072/get-system-drawing-bitmap-of-a-wpf-area-using-visualbrush
var visual = new DrawingVisual();
var context = visual.RenderOpen();
context.DrawDrawing(group);
context.Close();
var bitmap = new RenderTargetBitmap(
Convert.ToInt32(width),
Convert.ToInt32(height),
_dpiScale100,
_dpiScale100,
PixelFormats.Pbgra32);
bitmap.Render(visual);
return bitmap;
}
else
{
ProgramLogger.Exception($"Unable to convert background string {BackgroundColor} to color for {Package.Location}", new InvalidOperationException(), GetType(), Package.Location);
return new BitmapImage(new Uri(Constant.ErrorIcon));
}
}
else
{
// todo use windows theme as background
return image;
}
}
private BitmapImage ImageFromPath(string path)
{
if (File.Exists(path))
{
var memoryStream = new MemoryStream();
using (var fileStream = File.OpenRead(path))
{
fileStream.CopyTo(memoryStream);
memoryStream.Position = 0;
var image = new BitmapImage();
image.BeginInit();
image.StreamSource = memoryStream;
image.EndInit();
return image;
}
}
else
{
// ProgramLogger.Exception($"Unable to get logo for {UserModelId} from {path} and located in {Package.Location}", new FileNotFoundException(), GetType(), path);
return new BitmapImage(new Uri(ImageLoader.ErrorIconPath));
}
}
*/
public override string ToString()
{
return $"{DisplayName}: {Description}";