mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-16 11:48:06 +01:00
Adds support for JUMBO thumbnails in the helper (#38539)
Adds a parameter to `Toolkit.ThumbnailHelper.GetThumbnail` to retrieve the largest possible icon from the file. For most use cases, the normal icon size will be good for list items and page icons. But for details, you'll want to use the JUMBO icons, and to retrieve them, we need to get the icon from a different API. As a drive-by, I also have us fetching the highest-res app icon for UWP's rather than the lowest-res icon. Solves #38238 Screenshots: | before | after | | ------ | ----- | |  |  | |  |  |
This commit is contained in:
5
.github/actions/spell-check/expect.txt
vendored
5
.github/actions/spell-check/expect.txt
vendored
@@ -678,6 +678,7 @@ IGo
|
|||||||
iid
|
iid
|
||||||
Iindex
|
Iindex
|
||||||
Ijwhost
|
Ijwhost
|
||||||
|
ILD
|
||||||
IMAGEHLP
|
IMAGEHLP
|
||||||
IMAGERESIZERCONTEXTMENU
|
IMAGERESIZERCONTEXTMENU
|
||||||
IMAGERESIZEREXT
|
IMAGERESIZEREXT
|
||||||
@@ -1453,6 +1454,9 @@ SHELLDLL
|
|||||||
shellex
|
shellex
|
||||||
SHELLEXECUTEINFO
|
SHELLEXECUTEINFO
|
||||||
SHELLEXECUTEINFOW
|
SHELLEXECUTEINFOW
|
||||||
|
SHELLEXTENSION
|
||||||
|
SHELLICONSIZE
|
||||||
|
SHELLNEWVALUE
|
||||||
SHFILEINFO
|
SHFILEINFO
|
||||||
SHFILEOPSTRUCT
|
SHFILEOPSTRUCT
|
||||||
SHGDN
|
SHGDN
|
||||||
@@ -1460,6 +1464,7 @@ SHGDNF
|
|||||||
SHGFI
|
SHGFI
|
||||||
SHGFIICON
|
SHGFIICON
|
||||||
SHGFILARGEICON
|
SHGFILARGEICON
|
||||||
|
SHIL
|
||||||
shinfo
|
shinfo
|
||||||
shlwapi
|
shlwapi
|
||||||
shobjidl
|
shobjidl
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ public sealed partial class AllAppsPage : ListPage
|
|||||||
Stopwatch stopwatch = new();
|
Stopwatch stopwatch = new();
|
||||||
stopwatch.Start();
|
stopwatch.Start();
|
||||||
|
|
||||||
List<AppItem> apps = GetPrograms();
|
var apps = GetPrograms();
|
||||||
|
|
||||||
this.allAppsSection = apps
|
this.allAppsSection = apps
|
||||||
.Select((app) => new AppListItem(app, true))
|
.Select((app) => new AppListItem(app, true))
|
||||||
@@ -73,26 +73,15 @@ public sealed partial class AllAppsPage : ListPage
|
|||||||
|
|
||||||
internal List<AppItem> GetPrograms()
|
internal List<AppItem> GetPrograms()
|
||||||
{
|
{
|
||||||
IEnumerable<AppItem> uwpResults = AppCache.Instance.Value.UWPs
|
var uwpResults = AppCache.Instance.Value.UWPs
|
||||||
.Where((application) => application.Enabled)
|
.Where((application) => application.Enabled)
|
||||||
.Select(app =>
|
.Select(UwpToAppItem);
|
||||||
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(),
|
|
||||||
});
|
|
||||||
|
|
||||||
IEnumerable<AppItem> win32Results = AppCache.Instance.Value.Win32s
|
var win32Results = AppCache.Instance.Value.Win32s
|
||||||
.Where((application) => application.Enabled && application.Valid)
|
.Where((application) => application.Enabled && application.Valid)
|
||||||
.Select(app =>
|
.Select(app =>
|
||||||
{
|
{
|
||||||
string icoPath = string.IsNullOrEmpty(app.IcoPath) ?
|
var icoPath = string.IsNullOrEmpty(app.IcoPath) ?
|
||||||
(app.AppType == Win32Program.ApplicationType.InternetShortcutApplication ?
|
(app.AppType == Win32Program.ApplicationType.InternetShortcutApplication ?
|
||||||
app.IcoPath :
|
app.IcoPath :
|
||||||
app.FullPath) :
|
app.FullPath) :
|
||||||
@@ -116,4 +105,21 @@ public sealed partial class AllAppsPage : ListPage
|
|||||||
|
|
||||||
return uwpResults.Concat(win32Results).OrderBy(app => app.Name).ToList();
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,13 @@ internal sealed partial class AppListItem : ListItem
|
|||||||
Tags = [_appTag];
|
Tags = [_appTag];
|
||||||
MoreCommands = _app.Commands!.ToArray();
|
MoreCommands = _app.Commands!.ToArray();
|
||||||
|
|
||||||
_details = new Lazy<Details>(() => BuildDetails());
|
_details = new Lazy<Details>(() =>
|
||||||
|
{
|
||||||
|
var t = BuildDetails();
|
||||||
|
t.Wait();
|
||||||
|
return t.Result;
|
||||||
|
});
|
||||||
|
|
||||||
_icon = new Lazy<IconInfo>(() =>
|
_icon = new Lazy<IconInfo>(() =>
|
||||||
{
|
{
|
||||||
var t = FetchIcon(useThumbnails);
|
var t = FetchIcon(useThumbnails);
|
||||||
@@ -41,8 +47,9 @@ internal sealed partial class AppListItem : ListItem
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private Details BuildDetails()
|
private async Task<Details> BuildDetails()
|
||||||
{
|
{
|
||||||
|
// Build metadata, with app type, path, etc.
|
||||||
var metadata = new List<DetailsElement>();
|
var metadata = new List<DetailsElement>();
|
||||||
metadata.Add(new DetailsElement() { Key = "Type", Data = new DetailsTags() { Tags = [new Tag(_app.Type)] } });
|
metadata.Add(new DetailsElement() { Key = "Type", Data = new DetailsTags() { Tags = [new Tag(_app.Type)] } });
|
||||||
if (!_app.IsPackaged)
|
if (!_app.IsPackaged)
|
||||||
@@ -50,10 +57,33 @@ internal sealed partial class AppListItem : ListItem
|
|||||||
metadata.Add(new DetailsElement() { Key = "Path", Data = new DetailsLink() { Text = _app.ExePath } });
|
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()
|
return new Details()
|
||||||
{
|
{
|
||||||
Title = this.Title,
|
Title = this.Title,
|
||||||
HeroImage = this.Icon ?? new IconInfo(string.Empty),
|
HeroImage = heroImage ?? this.Icon ?? new IconInfo(string.Empty),
|
||||||
Metadata = metadata.ToArray(),
|
Metadata = metadata.ToArray(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -64,11 +94,6 @@ internal sealed partial class AppListItem : ListItem
|
|||||||
if (_app.IsPackaged)
|
if (_app.IsPackaged)
|
||||||
{
|
{
|
||||||
icon = new IconInfo(_app.IcoPath);
|
icon = new IconInfo(_app.IcoPath);
|
||||||
if (_details.IsValueCreated)
|
|
||||||
{
|
|
||||||
_details.Value.HeroImage = icon;
|
|
||||||
}
|
|
||||||
|
|
||||||
return icon;
|
return icon;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,11 +119,6 @@ internal sealed partial class AppListItem : ListItem
|
|||||||
icon = new IconInfo(_app.IcoPath);
|
icon = new IconInfo(_app.IcoPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_details.IsValueCreated)
|
|
||||||
{
|
|
||||||
_details.Value.HeroImage = icon;
|
|
||||||
}
|
|
||||||
|
|
||||||
return icon;
|
return icon;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,13 +6,11 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO.Abstractions;
|
using System.IO.Abstractions;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Xml;
|
using System.Xml;
|
||||||
using Microsoft.CmdPal.Ext.Apps.Commands;
|
using Microsoft.CmdPal.Ext.Apps.Commands;
|
||||||
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.Toolkit;
|
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||||
using static Microsoft.CmdPal.Ext.Apps.Utils.Native;
|
using static Microsoft.CmdPal.Ext.Apps.Utils.Native;
|
||||||
using PackageVersion = Microsoft.CmdPal.Ext.Apps.Programs.UWP.PackageVersion;
|
using PackageVersion = Microsoft.CmdPal.Ext.Apps.Programs.UWP.PackageVersion;
|
||||||
@@ -314,7 +312,12 @@ public class UWPApplication : IProgram
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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))
|
if (!string.IsNullOrEmpty(selectedIconPath))
|
||||||
{
|
{
|
||||||
LogoPath = selectedIconPath;
|
LogoPath = selectedIconPath;
|
||||||
|
|||||||
@@ -27,4 +27,10 @@ internal sealed class NativeMethods
|
|||||||
|
|
||||||
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
|
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
|
||||||
internal static extern bool DestroyIcon(IntPtr hIcon);
|
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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,19 +24,12 @@ public class ThumbnailHelper
|
|||||||
".ico",
|
".ico",
|
||||||
];
|
];
|
||||||
|
|
||||||
public static Task<IRandomAccessStream?> GetThumbnail(string path)
|
public static Task<IRandomAccessStream?> GetThumbnail(string path, bool jumbo = false)
|
||||||
{
|
{
|
||||||
var extension = Path.GetExtension(path).ToLower(CultureInfo.InvariantCulture);
|
var extension = Path.GetExtension(path).ToLower(CultureInfo.InvariantCulture);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (ImageExtensions.Contains(extension))
|
return ImageExtensions.Contains(extension) ? GetImageThumbnailAsync(path) : GetFileIconStream(path, jumbo);
|
||||||
{
|
|
||||||
return GetImageThumbnailAsync(path);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return GetFileIconStream(path);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
@@ -45,9 +38,19 @@ public class ThumbnailHelper
|
|||||||
return Task.FromResult<IRandomAccessStream?>(null);
|
return Task.FromResult<IRandomAccessStream?>(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private const uint SHGFIICON = 0x000000100;
|
// these are windows constants and mangling them is goofy
|
||||||
private const uint SHGFILARGEICON = 0x000000000;
|
#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)
|
private static MemoryStream GetMemoryStreamFromIcon(IntPtr hIcon)
|
||||||
{
|
{
|
||||||
var memoryStream = new MemoryStream();
|
var memoryStream = new MemoryStream();
|
||||||
@@ -65,19 +68,40 @@ public class ThumbnailHelper
|
|||||||
return memoryStream;
|
return memoryStream;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<IRandomAccessStream?> GetFileIconStream(string filePath)
|
private static async Task<IRandomAccessStream?> GetFileIconStream(string filePath, bool jumbo)
|
||||||
{
|
{
|
||||||
var shinfo = default(NativeMethods.SHFILEINFO);
|
nint hIcon = 0;
|
||||||
var hr = NativeMethods.SHGetFileInfo(filePath, 0, ref shinfo, (uint)Marshal.SizeOf(shinfo), SHGFIICON | SHGFILARGEICON);
|
|
||||||
|
|
||||||
if (hr == 0 || shinfo.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 (hIcon == 0)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var stream = new InMemoryRandomAccessStream();
|
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 outputStream = stream.GetOutputStreamAt(0);
|
||||||
using (var dataWriter = new DataWriter(outputStream))
|
using (var dataWriter = new DataWriter(outputStream))
|
||||||
{
|
{
|
||||||
@@ -95,4 +119,21 @@ public class ThumbnailHelper
|
|||||||
var thumbnail = await file.GetThumbnailAsync(ThumbnailMode.PicturesView);
|
var thumbnail = await file.GetThumbnailAsync(ThumbnailMode.PicturesView);
|
||||||
return thumbnail;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user