diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index 59ec83012433..7ed61d617394 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -678,6 +678,7 @@ IGo iid Iindex Ijwhost +ILD IMAGEHLP IMAGERESIZERCONTEXTMENU IMAGERESIZEREXT @@ -1453,6 +1454,9 @@ SHELLDLL shellex SHELLEXECUTEINFO SHELLEXECUTEINFOW +SHELLEXTENSION +SHELLICONSIZE +SHELLNEWVALUE SHFILEINFO SHFILEOPSTRUCT SHGDN @@ -1460,6 +1464,7 @@ SHGDNF SHGFI SHGFIICON SHGFILARGEICON +SHIL shinfo shlwapi shobjidl diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AllAppsPage.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AllAppsPage.cs index ed724794fde9..f271d4d5564d 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AllAppsPage.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AllAppsPage.cs @@ -57,7 +57,7 @@ private void BuildListItems() Stopwatch stopwatch = new(); stopwatch.Start(); - List apps = GetPrograms(); + var apps = GetPrograms(); this.allAppsSection = apps .Select((app) => new AppListItem(app, true)) @@ -73,26 +73,15 @@ private void BuildListItems() internal List GetPrograms() { - IEnumerable uwpResults = AppCache.Instance.Value.UWPs + var uwpResults = AppCache.Instance.Value.UWPs .Where((application) => application.Enabled) - .Select(app => - new AppItem() - { - Name = app.Name, - Subtitle = app.Description, - Type = UWPApplication.Type(), - IcoPath = app.LogoType != LogoType.Error ? app.LogoPath : string.Empty, - DirPath = app.Location, - UserModelId = app.UserModelId, - IsPackaged = true, - Commands = app.GetCommands(), - }); + .Select(UwpToAppItem); - IEnumerable win32Results = AppCache.Instance.Value.Win32s + var win32Results = AppCache.Instance.Value.Win32s .Where((application) => application.Enabled && application.Valid) .Select(app => { - string icoPath = string.IsNullOrEmpty(app.IcoPath) ? + var icoPath = string.IsNullOrEmpty(app.IcoPath) ? (app.AppType == Win32Program.ApplicationType.InternetShortcutApplication ? app.IcoPath : app.FullPath) : @@ -116,4 +105,21 @@ internal List GetPrograms() return uwpResults.Concat(win32Results).OrderBy(app => app.Name).ToList(); } + + private AppItem UwpToAppItem(UWPApplication app) + { + var iconPath = app.LogoType != LogoType.Error ? app.LogoPath : string.Empty; + var item = new AppItem() + { + Name = app.Name, + Subtitle = app.Description, + Type = UWPApplication.Type(), + IcoPath = iconPath, + DirPath = app.Location, + UserModelId = app.UserModelId, + IsPackaged = true, + Commands = app.GetCommands(), + }; + return item; + } } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AppListItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AppListItem.cs index 1ab8df76efe9..57c9175d4db9 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AppListItem.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AppListItem.cs @@ -32,7 +32,13 @@ public AppListItem(AppItem app, bool useThumbnails) Tags = [_appTag]; MoreCommands = _app.Commands!.ToArray(); - _details = new Lazy
(() => BuildDetails()); + _details = new Lazy
(() => + { + var t = BuildDetails(); + t.Wait(); + return t.Result; + }); + _icon = new Lazy(() => { var t = FetchIcon(useThumbnails); @@ -41,8 +47,9 @@ public AppListItem(AppItem app, bool useThumbnails) }); } - private Details BuildDetails() + private async Task
BuildDetails() { + // Build metadata, with app type, path, etc. var metadata = new List(); metadata.Add(new DetailsElement() { Key = "Type", Data = new DetailsTags() { Tags = [new Tag(_app.Type)] } }); if (!_app.IsPackaged) @@ -50,10 +57,33 @@ private Details BuildDetails() metadata.Add(new DetailsElement() { Key = "Path", Data = new DetailsLink() { Text = _app.ExePath } }); } + // Icon + IconInfo? heroImage = null; + if (_app.IsPackaged) + { + heroImage = new IconInfo(_app.IcoPath); + } + else + { + try + { + var stream = await ThumbnailHelper.GetThumbnail(_app.ExePath, true); + if (stream != null) + { + heroImage = IconInfo.FromStream(stream); + } + } + catch (Exception) + { + // do nothing if we fail to load an icon. + // Logging it would be too NOISY, there's really no need. + } + } + return new Details() { Title = this.Title, - HeroImage = this.Icon ?? new IconInfo(string.Empty), + HeroImage = heroImage ?? this.Icon ?? new IconInfo(string.Empty), Metadata = metadata.ToArray(), }; } @@ -64,11 +94,6 @@ public async Task FetchIcon(bool useThumbnails) if (_app.IsPackaged) { icon = new IconInfo(_app.IcoPath); - if (_details.IsValueCreated) - { - _details.Value.HeroImage = icon; - } - return icon; } @@ -94,11 +119,6 @@ public async Task FetchIcon(bool useThumbnails) icon = new IconInfo(_app.IcoPath); } - if (_details.IsValueCreated) - { - _details.Value.HeroImage = icon; - } - return icon; } } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/UWPApplication.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/UWPApplication.cs index 6a4cf9448052..7a35400a7a06 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/UWPApplication.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/UWPApplication.cs @@ -6,13 +6,11 @@ using System.Collections.Generic; using System.IO.Abstractions; using System.Linq; -using System.Runtime.InteropServices; using System.Text; using System.Xml; using Microsoft.CmdPal.Ext.Apps.Commands; using Microsoft.CmdPal.Ext.Apps.Properties; using Microsoft.CmdPal.Ext.Apps.Utils; -using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions.Toolkit; using static Microsoft.CmdPal.Ext.Apps.Utils.Native; using PackageVersion = Microsoft.CmdPal.Ext.Apps.Programs.UWP.PackageVersion; @@ -314,7 +312,12 @@ private bool SetScaleIcons(string path, string colorscheme, bool highContrast = } } - var selectedIconPath = paths.FirstOrDefault(File.Exists); + // 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. + var selectedIconPath = paths.LastOrDefault(File.Exists); if (!string.IsNullOrEmpty(selectedIconPath)) { LogoPath = selectedIconPath; diff --git a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/NativeMethods.cs b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/NativeMethods.cs index 73e8b0cd1c1b..99db4566191e 100644 --- a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/NativeMethods.cs +++ b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/NativeMethods.cs @@ -27,4 +27,10 @@ internal struct SHFILEINFO [DllImport("user32.dll", CharSet = CharSet.Unicode)] internal static extern bool DestroyIcon(IntPtr hIcon); + + [DllImport("Shell32.dll", CharSet = CharSet.Unicode)] + internal static extern int SHGetImageList(int iImageList, ref Guid riid, out IntPtr ppv); + + [DllImport("comctl32.dll", SetLastError = true)] + internal static extern int ImageList_GetIcon(IntPtr himl, int i, int flags); } diff --git a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/ThumbnailHelper.cs b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/ThumbnailHelper.cs index e71360bcd4ab..cf7c5c2073bf 100644 --- a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/ThumbnailHelper.cs +++ b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/ThumbnailHelper.cs @@ -24,19 +24,12 @@ public class ThumbnailHelper ".ico", ]; - public static Task GetThumbnail(string path) + public static Task GetThumbnail(string path, bool jumbo = false) { var extension = Path.GetExtension(path).ToLower(CultureInfo.InvariantCulture); try { - if (ImageExtensions.Contains(extension)) - { - return GetImageThumbnailAsync(path); - } - else - { - return GetFileIconStream(path); - } + return ImageExtensions.Contains(extension) ? GetImageThumbnailAsync(path) : GetFileIconStream(path, jumbo); } catch (Exception) { @@ -45,9 +38,19 @@ public class ThumbnailHelper return Task.FromResult(null); } - private const uint SHGFIICON = 0x000000100; - private const uint SHGFILARGEICON = 0x000000000; - + // these are windows constants and mangling them is goofy +#pragma warning disable SA1310 // Field names should not contain underscore +#pragma warning disable SA1306 // Field names should begin with lower-case letter + private const uint SHGFI_ICON = 0x000000100; + private const uint SHGFI_SHELLICONSIZE = 0x000000004; + private const int SHGFI_SYSICONINDEX = 0x000004000; + private const int SHIL_JUMBO = 4; + private const int ILD_TRANSPARENT = 1; +#pragma warning restore SA1306 // Field names should begin with lower-case letter +#pragma warning restore SA1310 // Field names should not contain underscore + + // This will call DestroyIcon on the hIcon passed in. + // Duplicate it if you need it again after this. private static MemoryStream GetMemoryStreamFromIcon(IntPtr hIcon) { var memoryStream = new MemoryStream(); @@ -65,19 +68,40 @@ private static MemoryStream GetMemoryStreamFromIcon(IntPtr hIcon) return memoryStream; } - private static async Task GetFileIconStream(string filePath) + private static async Task GetFileIconStream(string filePath, bool jumbo) { - var shinfo = default(NativeMethods.SHFILEINFO); - var hr = NativeMethods.SHGetFileInfo(filePath, 0, ref shinfo, (uint)Marshal.SizeOf(shinfo), SHGFIICON | SHGFILARGEICON); + nint hIcon = 0; + + // If requested, look up the Jumbo icon + if (jumbo) + { + hIcon = GetLargestIcon(filePath); + } + + // If we didn't want the JUMBO icon, or didn't find it, fall back to + // the normal icon lookup + if (hIcon == 0) + { + var shinfo = default(NativeMethods.SHFILEINFO); + + var hr = NativeMethods.SHGetFileInfo(filePath, 0, ref shinfo, (uint)Marshal.SizeOf(shinfo), SHGFI_ICON | SHGFI_SHELLICONSIZE); + + if (hr == 0 || shinfo.hIcon == 0) + { + return null; + } + + hIcon = shinfo.hIcon; + } - if (hr == 0 || shinfo.hIcon == 0) + if (hIcon == 0) { return null; } var stream = new InMemoryRandomAccessStream(); - using var memoryStream = GetMemoryStreamFromIcon(shinfo.hIcon); + using var memoryStream = GetMemoryStreamFromIcon(hIcon); // this will DestroyIcon hIcon using var outputStream = stream.GetOutputStreamAt(0); using (var dataWriter = new DataWriter(outputStream)) { @@ -95,4 +119,21 @@ private static MemoryStream GetMemoryStreamFromIcon(IntPtr hIcon) var thumbnail = await file.GetThumbnailAsync(ThumbnailMode.PicturesView); return thumbnail; } + + private static nint GetLargestIcon(string path) + { + var shinfo = default(NativeMethods.SHFILEINFO); + NativeMethods.SHGetFileInfo(path, 0, ref shinfo, (uint)Marshal.SizeOf(shinfo), SHGFI_ICON | SHGFI_SYSICONINDEX); + + var hIcon = IntPtr.Zero; + var iID_IImageList = new Guid("46EB5926-582E-4017-9FDF-E8998DAA0950"); + IntPtr imageListPtr; + + if (NativeMethods.SHGetImageList(SHIL_JUMBO, ref iID_IImageList, out imageListPtr) == 0 && imageListPtr != IntPtr.Zero) + { + hIcon = NativeMethods.ImageList_GetIcon(imageListPtr, shinfo.iIcon, ILD_TRANSPARENT); + } + + return hIcon; + } }