mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-10 21:41:51 +02:00
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:
4
.github/actions/spell-check/expect.txt
vendored
4
.github/actions/spell-check/expect.txt
vendored
@@ -130,6 +130,7 @@ bezelled
|
|||||||
bhid
|
bhid
|
||||||
BIF
|
BIF
|
||||||
bigbar
|
bigbar
|
||||||
|
BIGGERSIZEOK
|
||||||
bigobj
|
bigobj
|
||||||
binlog
|
binlog
|
||||||
binres
|
binres
|
||||||
@@ -311,6 +312,7 @@ CRECT
|
|||||||
CRH
|
CRH
|
||||||
critsec
|
critsec
|
||||||
cropandlock
|
cropandlock
|
||||||
|
CROPTOSQUARE
|
||||||
Crossdevice
|
Crossdevice
|
||||||
csdevkit
|
csdevkit
|
||||||
CSearch
|
CSearch
|
||||||
@@ -761,6 +763,7 @@ IAI
|
|||||||
icf
|
icf
|
||||||
ICONERROR
|
ICONERROR
|
||||||
ICONLOCATION
|
ICONLOCATION
|
||||||
|
ICONONLY
|
||||||
IDCANCEL
|
IDCANCEL
|
||||||
IDD
|
IDD
|
||||||
idk
|
idk
|
||||||
@@ -1672,6 +1675,7 @@ sigdn
|
|||||||
Signedness
|
Signedness
|
||||||
SIGNINGSCENARIO
|
SIGNINGSCENARIO
|
||||||
signtool
|
signtool
|
||||||
|
SIIGBF
|
||||||
SINGLEKEY
|
SINGLEKEY
|
||||||
sipolicy
|
sipolicy
|
||||||
SIZEBOX
|
SIZEBOX
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ public sealed class AppItem
|
|||||||
|
|
||||||
public string? FullExecutablePath { get; set; }
|
public string? FullExecutablePath { get; set; }
|
||||||
|
|
||||||
|
public string? JumboIconPath { get; set; }
|
||||||
|
|
||||||
public AppItem()
|
public AppItem()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ using System.Threading.Tasks;
|
|||||||
using ManagedCommon;
|
using ManagedCommon;
|
||||||
using Microsoft.CmdPal.Core.Common.Helpers;
|
using Microsoft.CmdPal.Core.Common.Helpers;
|
||||||
using Microsoft.CmdPal.Ext.Apps.Commands;
|
using Microsoft.CmdPal.Ext.Apps.Commands;
|
||||||
|
using Microsoft.CmdPal.Ext.Apps.Helpers;
|
||||||
using Microsoft.CommandPalette.Extensions;
|
using Microsoft.CommandPalette.Extensions;
|
||||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||||
|
|
||||||
@@ -17,6 +18,7 @@ public sealed partial class AppListItem : ListItem
|
|||||||
{
|
{
|
||||||
private readonly AppCommand _appCommand;
|
private readonly AppCommand _appCommand;
|
||||||
private readonly AppItem _app;
|
private readonly AppItem _app;
|
||||||
|
|
||||||
private readonly Lazy<Task<IconInfo?>> _iconLoadTask;
|
private readonly Lazy<Task<IconInfo?>> _iconLoadTask;
|
||||||
private readonly Lazy<Task<Details>> _detailsLoadTask;
|
private readonly Lazy<Task<Details>> _detailsLoadTask;
|
||||||
|
|
||||||
@@ -66,7 +68,7 @@ public sealed partial class AppListItem : ListItem
|
|||||||
MoreCommands = AddPinCommands(_app.Commands!, isPinned);
|
MoreCommands = AddPinCommands(_app.Commands!, isPinned);
|
||||||
|
|
||||||
_detailsLoadTask = new Lazy<Task<Details>>(BuildDetails);
|
_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()
|
private async Task LoadDetailsAsync()
|
||||||
@@ -85,7 +87,7 @@ public sealed partial class AppListItem : ListItem
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Icon = _appCommand.Icon = await _iconLoadTask.Value ?? Icons.GenericAppIcon;
|
Icon = _appCommand.Icon = CoalesceIcon(await _iconLoadTask.Value);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
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()
|
private async Task<Details> BuildDetails()
|
||||||
{
|
{
|
||||||
// Build metadata, with app type, path, etc.
|
// 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] 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] 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] IcoPath", Data = new DetailsLink() { Text = _app.IcoPath } });
|
||||||
|
metadata.Add(new DetailsElement() { Key = "[DEBUG] JumboIconPath", Data = new DetailsLink() { Text = _app.JumboIconPath ?? "(null)" } });
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Icon
|
// Icon
|
||||||
IconInfo? heroImage = null;
|
IconInfo? heroImage = null;
|
||||||
if (_app.IsPackaged)
|
if (_app.IsPackaged)
|
||||||
{
|
{
|
||||||
heroImage = new IconInfo(_app.IcoPath);
|
heroImage = new IconInfo(_app.JumboIconPath ?? _app.IcoPath);
|
||||||
}
|
}
|
||||||
else
|
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.
|
// do nothing if we fail to load an icon.
|
||||||
// Logging it would be too NOISY, there's really no need.
|
// 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);
|
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);
|
heroImage = await TryLoadThumbnail(_app.ExePath, jumbo: true, logOnFailure: false);
|
||||||
}
|
}
|
||||||
@@ -133,8 +175,8 @@ public sealed partial class AppListItem : ListItem
|
|||||||
return new Details()
|
return new Details()
|
||||||
{
|
{
|
||||||
Title = this.Title,
|
Title = this.Title,
|
||||||
HeroImage = heroImage ?? this.Icon ?? Icons.GenericAppIcon,
|
HeroImage = CoalesceIcon(CoalesceIcon(heroImage, this.Icon as IconInfo)),
|
||||||
Metadata = metadata.ToArray(),
|
Metadata = [..metadata],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,7 +196,7 @@ public sealed partial class AppListItem : ListItem
|
|||||||
icon = await TryLoadThumbnail(_app.IcoPath, jumbo: false, logOnFailure: true);
|
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);
|
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)
|
private async Task<IconInfo?> TryLoadThumbnail(string path, bool jumbo, bool logOnFailure)
|
||||||
{
|
{
|
||||||
try
|
return await Task.Run(async () =>
|
||||||
{
|
{
|
||||||
var stream = await ThumbnailHelper.GetThumbnail(path, jumbo);
|
try
|
||||||
if (stream is not null)
|
|
||||||
{
|
{
|
||||||
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)
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
if (logOnFailure)
|
|
||||||
{
|
{
|
||||||
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -18,3 +18,9 @@ ShellLink
|
|||||||
IPersistFile
|
IPersistFile
|
||||||
CoTaskMemFree
|
CoTaskMemFree
|
||||||
IUnknown
|
IUnknown
|
||||||
|
IShellItemImageFactory
|
||||||
|
DeleteObject
|
||||||
|
GetDIBits
|
||||||
|
GetDC
|
||||||
|
ReleaseDC
|
||||||
|
SIIGBF
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ using System.IO.Abstractions;
|
|||||||
using System.Xml;
|
using System.Xml;
|
||||||
using ManagedCommon;
|
using ManagedCommon;
|
||||||
using Microsoft.CmdPal.Ext.Apps.Commands;
|
using Microsoft.CmdPal.Ext.Apps.Commands;
|
||||||
|
using Microsoft.CmdPal.Ext.Apps.Helpers;
|
||||||
using Microsoft.CmdPal.Ext.Apps.Properties;
|
using Microsoft.CmdPal.Ext.Apps.Properties;
|
||||||
using Microsoft.CmdPal.Ext.Apps.Utils;
|
using Microsoft.CmdPal.Ext.Apps.Utils;
|
||||||
using Microsoft.CommandPalette.Extensions;
|
using Microsoft.CommandPalette.Extensions;
|
||||||
@@ -23,8 +24,10 @@ namespace Microsoft.CmdPal.Ext.Apps.Programs;
|
|||||||
[Serializable]
|
[Serializable]
|
||||||
public class UWPApplication : IUWPApplication
|
public class UWPApplication : IUWPApplication
|
||||||
{
|
{
|
||||||
|
private const int ListIconSize = 20;
|
||||||
|
private const int JumboIconSize = 64;
|
||||||
|
|
||||||
private static readonly IFileSystem FileSystem = new FileSystem();
|
private static readonly IFileSystem FileSystem = new FileSystem();
|
||||||
private static readonly IPath Path = FileSystem.Path;
|
|
||||||
private static readonly IFile File = FileSystem.File;
|
private static readonly IFile File = FileSystem.File;
|
||||||
|
|
||||||
public string AppListEntry { get; set; } = string.Empty;
|
public string AppListEntry { get; set; } = string.Empty;
|
||||||
@@ -56,13 +59,15 @@ public class UWPApplication : IUWPApplication
|
|||||||
|
|
||||||
public LogoType LogoType { get; set; }
|
public LogoType LogoType { get; set; }
|
||||||
|
|
||||||
|
public string JumboLogoPath { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public LogoType JumboLogoType { get; set; }
|
||||||
|
|
||||||
public UWP Package { get; set; }
|
public UWP Package { get; set; }
|
||||||
|
|
||||||
private string logoUri;
|
private string _logoUri;
|
||||||
|
|
||||||
private const string ContrastWhite = "contrast-white";
|
private string _jumboLogoUri;
|
||||||
|
|
||||||
private const string ContrastBlack = "contrast-black";
|
|
||||||
|
|
||||||
// Function to set the subtitle based on the Type of application
|
// Function to set the subtitle based on the Type of application
|
||||||
public static string Type()
|
public static string Type()
|
||||||
@@ -154,7 +159,8 @@ public class UWPApplication : IUWPApplication
|
|||||||
|
|
||||||
DisplayName = ResourceFromPri(package.FullName, DisplayName);
|
DisplayName = ResourceFromPri(package.FullName, DisplayName);
|
||||||
Description = ResourceFromPri(package.FullName, Description);
|
Description = ResourceFromPri(package.FullName, Description);
|
||||||
logoUri = LogoUriFromManifest(manifestApp);
|
_logoUri = LogoUriFromManifest(manifestApp);
|
||||||
|
_jumboLogoUri = LogoUriFromManifest(manifestApp, jumbo: true);
|
||||||
|
|
||||||
Enabled = true;
|
Enabled = true;
|
||||||
CanRunElevated = IfApplicationCanRunElevated();
|
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.Windows10, "Square44x44Logo" },
|
||||||
{ PackageVersion.Windows81, "Square30x30Logo" },
|
{ PackageVersion.Windows81, "Square30x30Logo" },
|
||||||
{ PackageVersion.Windows8, "SmallLogo" },
|
{ 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);
|
var hr = app->GetStringValue(key, out var logoUriFromAppPtr);
|
||||||
return ComFreeHelper.GetStringAndFree(hr, logoUriFromAppPtr);
|
return ComFreeHelper.GetStringAndFree(hr, logoUriFromAppPtr);
|
||||||
@@ -302,257 +316,55 @@ public class UWPApplication : IUWPApplication
|
|||||||
|
|
||||||
public void UpdateLogoPath(Theme theme)
|
public void UpdateLogoPath(Theme theme)
|
||||||
{
|
{
|
||||||
LogoPathFromUri(logoUri, theme);
|
// Update small logo
|
||||||
}
|
var logo = AppxIconLoader.LogoPathFromUri(_logoUri, theme, ListIconSize, Package);
|
||||||
|
if (logo.IsFound)
|
||||||
// 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>>
|
|
||||||
{
|
{
|
||||||
{ PackageVersion.Windows10, new List<int> { 100, 125, 150, 200, 400 } },
|
LogoPath = logo.LogoPath!;
|
||||||
{ PackageVersion.Windows81, new List<int> { 100, 120, 140, 160, 180 } },
|
LogoType = logo.LogoType;
|
||||||
{ 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);
|
|
||||||
}
|
}
|
||||||
else
|
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;
|
LogoPath = string.Empty;
|
||||||
LogoType = LogoType.Error;
|
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()
|
public AppItem ToAppItem()
|
||||||
{
|
{
|
||||||
var app = this;
|
var app = this;
|
||||||
var iconPath = app.LogoType != LogoType.Error ? app.LogoPath : string.Empty;
|
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,
|
Name = app.Name,
|
||||||
Subtitle = app.Description,
|
Subtitle = app.Description,
|
||||||
Type = UWPApplication.Type(),
|
Type = UWPApplication.Type(),
|
||||||
IcoPath = iconPath,
|
IcoPath = iconPath,
|
||||||
|
JumboIconPath = jumboIconPath,
|
||||||
DirPath = app.Location,
|
DirPath = app.Location,
|
||||||
UserModelId = app.UserModelId,
|
UserModelId = app.UserModelId,
|
||||||
IsPackaged = true,
|
IsPackaged = true,
|
||||||
@@ -563,116 +375,6 @@ public class UWPApplication : IUWPApplication
|
|||||||
return item;
|
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()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return $"{DisplayName}: {Description}";
|
return $"{DisplayName}: {Description}";
|
||||||
|
|||||||
Reference in New Issue
Block a user