169 lines
6.0 KiB
C#
169 lines
6.0 KiB
C#
using System.Collections.Concurrent;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Runtime.InteropServices;
|
|
using System.Windows;
|
|
using System.Windows.Data;
|
|
using System.Windows.Interop;
|
|
using System.Windows.Media;
|
|
using System.Windows.Media.Imaging;
|
|
using Explorer.Domain;
|
|
using Explorer.Presentation;
|
|
|
|
namespace Explorer.App;
|
|
|
|
public sealed class ShellIconConverter : IValueConverter
|
|
{
|
|
public object? Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
|
{
|
|
var large = parameter is string p && p.Equals("large", StringComparison.OrdinalIgnoreCase);
|
|
return value switch
|
|
{
|
|
FolderItemViewModel item => ShellIconCache.Get(item.FullPath, item.IsDirectory, large, item.MayHydrateOnRead),
|
|
FileSystemItem fs => ShellIconCache.Get(fs.FullPath, fs.IsDirectory, large, AttributeFlags.MayHydrateOnRead(fs.Attributes)),
|
|
string path => ShellIconCache.Get(path, Directory.Exists(path), large),
|
|
_ => ShellIconCache.Get(null, true, large)
|
|
};
|
|
}
|
|
|
|
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
|
=> throw new NotSupportedException();
|
|
}
|
|
|
|
internal static class ShellIconCache
|
|
{
|
|
private static readonly ConcurrentDictionary<string, ImageSource> Cache = new(StringComparer.OrdinalIgnoreCase);
|
|
|
|
public static ImageSource Get(string? path, bool isDirectory, bool large = false, bool avoidHydration = false)
|
|
{
|
|
var key = (large ? "L:" : "S:") + (avoidHydration ? "A:" : "") + CacheKey(path, isDirectory);
|
|
return Cache.GetOrAdd(key, _ => Load(path, isDirectory, large, avoidHydration));
|
|
}
|
|
|
|
private static string CacheKey(string? path, bool isDirectory)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(path) || path == "This PC")
|
|
{
|
|
return isDirectory ? "dir:generic" : "file:generic";
|
|
}
|
|
|
|
if (path == LocationRoots.RecycleBin)
|
|
{
|
|
return "recycle";
|
|
}
|
|
|
|
if (isDirectory)
|
|
{
|
|
return PathRules.IsDriveRoot(path) || (path.Length <= 3 && path.Contains(':', StringComparison.Ordinal))
|
|
? "drive:" + path.TrimEnd('\\').ToUpperInvariant()
|
|
: "dir:generic";
|
|
}
|
|
|
|
var ext = Path.GetExtension(path);
|
|
if (ext.Equals(".exe", StringComparison.OrdinalIgnoreCase)
|
|
|| ext.Equals(".lnk", StringComparison.OrdinalIgnoreCase)
|
|
|| ext.Equals(".ico", StringComparison.OrdinalIgnoreCase)
|
|
|| ext.Equals(".dll", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return "path:" + path;
|
|
}
|
|
|
|
return "ext:" + (string.IsNullOrEmpty(ext) ? ".file" : ext.ToLowerInvariant());
|
|
}
|
|
|
|
private static ImageSource Load(string? path, bool isDirectory, bool large, bool avoidHydration = false)
|
|
{
|
|
try
|
|
{
|
|
var info = new ShFileInfo();
|
|
uint flags = ShgfiIcon | (large ? ShgfiLargeIcon : ShgfiSmallIcon);
|
|
uint attrs = isDirectory ? FileAttributeDirectory : FileAttributeNormal;
|
|
string psz;
|
|
if (string.IsNullOrWhiteSpace(path) || path == "This PC")
|
|
{
|
|
psz = isDirectory ? "folder" : "file";
|
|
flags |= ShgfiUseFileAttributes;
|
|
}
|
|
else if (path == LocationRoots.RecycleBin)
|
|
{
|
|
psz = @"::{645FF040-5081-101B-9F08-00AA002F954E}";
|
|
}
|
|
else if (isDirectory && PathRules.IsDriveRoot(path))
|
|
{
|
|
psz = PathRules.EnsureDirectoryTrailingSlashIfRoot(path);
|
|
}
|
|
else if (isDirectory)
|
|
{
|
|
psz = path;
|
|
flags |= ShgfiUseFileAttributes;
|
|
}
|
|
else
|
|
{
|
|
psz = path;
|
|
var ext = Path.GetExtension(path);
|
|
if (avoidHydration
|
|
|| !ext.Equals(".exe", StringComparison.OrdinalIgnoreCase)
|
|
&& !ext.Equals(".lnk", StringComparison.OrdinalIgnoreCase)
|
|
&& !ext.Equals(".ico", StringComparison.OrdinalIgnoreCase)
|
|
&& File.Exists(path) == false)
|
|
{
|
|
flags |= ShgfiUseFileAttributes;
|
|
}
|
|
}
|
|
|
|
SHGetFileInfo(psz, attrs, ref info, (uint)Marshal.SizeOf<ShFileInfo>(), flags);
|
|
if (info.hIcon == 0)
|
|
{
|
|
return Fallback();
|
|
}
|
|
|
|
try
|
|
{
|
|
var source = Imaging.CreateBitmapSourceFromHIcon(info.hIcon, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions());
|
|
source.Freeze();
|
|
return source;
|
|
}
|
|
finally
|
|
{
|
|
DestroyIcon(info.hIcon);
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
return Fallback();
|
|
}
|
|
}
|
|
|
|
private static ImageSource Fallback()
|
|
{
|
|
var bmp = BitmapSource.Create(16, 16, 96, 96, PixelFormats.Bgra32, null, new byte[16 * 16 * 4], 16 * 4);
|
|
bmp.Freeze();
|
|
return bmp;
|
|
}
|
|
|
|
private const uint ShgfiIcon = 0x100;
|
|
private const uint ShgfiLargeIcon = 0x0;
|
|
private const uint ShgfiSmallIcon = 0x1;
|
|
private const uint ShgfiUseFileAttributes = 0x10;
|
|
private const uint FileAttributeDirectory = 0x10;
|
|
private const uint FileAttributeNormal = 0x80;
|
|
|
|
[DllImport("shell32.dll", CharSet = CharSet.Unicode)]
|
|
private static extern nint SHGetFileInfo(string pszPath, uint dwFileAttributes, ref ShFileInfo psfi, uint cbFileInfo, uint uFlags);
|
|
|
|
[DllImport("user32.dll", SetLastError = true)]
|
|
private static extern bool DestroyIcon(nint hIcon);
|
|
|
|
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
|
private struct ShFileInfo
|
|
{
|
|
public nint hIcon;
|
|
public int iIcon;
|
|
public uint dwAttributes;
|
|
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
|
|
public string szDisplayName;
|
|
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 80)]
|
|
public string szTypeName;
|
|
}
|
|
}
|