From 8e0eca5c26a889172de5acf473a8a0bec99defe0 Mon Sep 17 00:00:00 2001 From: Jaime Bernardo Date: Fri, 10 Mar 2023 16:31:05 +0000 Subject: [PATCH] [PTRun]Asynchronously load image and thumbnails --- .../ViewModel/ResultViewModel.cs | 56 +++++-- .../Wox.Infrastructure/Image/ImageLoader.cs | 139 +++++++++--------- 2 files changed, 113 insertions(+), 82 deletions(-) diff --git a/src/modules/launcher/PowerLauncher/ViewModel/ResultViewModel.cs b/src/modules/launcher/PowerLauncher/ViewModel/ResultViewModel.cs index 768dcdbe973f..d6949c03215b 100644 --- a/src/modules/launcher/PowerLauncher/ViewModel/ResultViewModel.cs +++ b/src/modules/launcher/PowerLauncher/ViewModel/ResultViewModel.cs @@ -5,6 +5,7 @@ using System; using System.Collections.ObjectModel; using System.Globalization; +using System.Threading.Tasks; using System.Windows.Input; using System.Windows.Media; using PowerLauncher.Helper; @@ -38,6 +39,9 @@ public enum ActivationType private bool _areContextButtonsActive; + private ImageSource _image; + private volatile bool _imageLoaded; + public bool AreContextButtonsActive { get => _areContextButtonsActive; @@ -191,25 +195,51 @@ public ImageSource Image { get { - var imagePath = Result.IcoPath; - if (string.IsNullOrEmpty(imagePath) && Result.Icon != null) + if (!_imageLoaded) { - try - { - return Result.Icon(); - } - catch (Exception e) - { - Log.Exception($"IcoPath is empty and exception when calling Icon() for result <{Result.Title}> of plugin <{Result.PluginDirectory}>", e, GetType()); - imagePath = ImageLoader.ErrorIconPath; - } + _imageLoaded = true; + _ = LoadImageAsync(); } - // will get here either when icoPath has value\icon delegate is null\when had exception in delegate - return ImageLoader.Load(imagePath, _settings.GenerateThumbnailsFromFiles); + return _image; + } + + private set + { + _image = value; + OnPropertyChanged(nameof(Image)); } } + private async Task LoadImageInternalAsync(string imagePath, Result.IconDelegate icon, bool loadFullImage) + { + if (string.IsNullOrEmpty(imagePath) && icon != null) + { + try + { + var image = icon(); + return image; + } + catch (Exception e) + { + Log.Exception( + $"IcoPath is empty and exception when calling Icon() for result <{Result.Title}> of plugin <{Result.PluginDirectory}>", + e, + System.Reflection.MethodBase.GetCurrentMethod().DeclaringType); + imagePath = ImageLoader.ErrorIconPath; + } + } + + return await ImageLoader.LoadAsync(imagePath, _settings.GenerateThumbnailsFromFiles, loadFullImage).ConfigureAwait(false); + } + + private async Task LoadImageAsync() + { + var imagePath = Result.IcoPath; + var iconDelegate = Result.Icon; + Image = await LoadImageInternalAsync(imagePath, iconDelegate, false).ConfigureAwait(false); + } + // Returns false if we've already reached the last item. public bool SelectNextContextButton() { diff --git a/src/modules/launcher/Wox.Infrastructure/Image/ImageLoader.cs b/src/modules/launcher/Wox.Infrastructure/Image/ImageLoader.cs index a52baf3c2c69..767007221906 100644 --- a/src/modules/launcher/Wox.Infrastructure/Image/ImageLoader.cs +++ b/src/modules/launcher/Wox.Infrastructure/Image/ImageLoader.cs @@ -64,12 +64,12 @@ public static void Initialize(Theme theme) UpdateIconPath(theme); Task.Run(() => { - Stopwatch.Normal("ImageLoader.Initialize - Preload images cost", () => + Stopwatch.Normal("ImageLoader.Initialize - Preload images cost", async () => { - ImageCache.Usage.AsParallel().ForAll(x => + foreach (var (path, _) in ImageCache.Usage) { - Load(x.Key, true); - }); + await LoadAsync(path, true); + } }); Log.Info($"Number of preload images is <{ImageCache.Usage.Count}>, Images Number: {ImageCache.CacheSize()}, Unique Items {ImageCache.UniqueImagesInCache()}", MethodBase.GetCurrentMethod().DeclaringType); @@ -120,10 +120,9 @@ private enum ImageType Cache, } - private static ImageResult LoadInternal(string path, bool generateThumbnailsFromFiles, bool loadFullImage = false) + private static async ValueTask LoadInternalAsync(string path, bool generateThumbnailsFromFiles, bool loadFullImage = false) { - ImageSource image; - ImageType type = ImageType.Error; + ImageResult imageResult; try { if (string.IsNullOrEmpty(path)) @@ -144,82 +143,84 @@ private static ImageResult LoadInternal(string path, bool generateThumbnailsFrom return new ImageResult(imageSource, ImageType.Data); } - if (!Path.IsPathRooted(path)) - { - path = Path.Combine(Constant.ProgramDirectory, "Images", Path.GetFileName(path)); - } + imageResult = await Task.Run(() => GetThumbnailResult(ref path, generateThumbnailsFromFiles, loadFullImage)); + } + catch (System.Exception e) + { + Log.Exception($"Failed to get thumbnail for {path}", e, MethodBase.GetCurrentMethod().DeclaringType); + ImageSource image = ImageCache[ErrorIconPath]; + ImageCache[path] = image; + imageResult = new ImageResult(image, ImageType.Error); + } - if (Directory.Exists(path)) - { - /* Directories can also have thumbnails instead of shell icons. - * Generating thumbnails for a bunch of folders while scrolling through - * results from Everything makes a big impact on performance and - * Wox responsibility. - * - Solution: just load the icon - */ - type = ImageType.Folder; - image = WindowsThumbnailProvider.GetThumbnail(path, Constant.ThumbnailSize, Constant.ThumbnailSize, ThumbnailOptions.IconOnly); - } - else if (File.Exists(path)) + return imageResult; + } + + private static ImageResult GetThumbnailResult(ref string path, bool generateThumbnailsFromFiles, bool loadFullImage = false) + { + ImageSource image; + ImageType type = ImageType.Error; + + if (!Path.IsPathRooted(path)) + { + path = Path.Combine(Constant.ProgramDirectory, "Images", Path.GetFileName(path)); + } + + if (Directory.Exists(path)) + { + /* Directories can also have thumbnails instead of shell icons. + * Generating thumbnails for a bunch of folders while scrolling through + * results from Everything makes a big impact on performance and + * Wox responsibility. + * - Solution: just load the icon + */ + type = ImageType.Folder; + image = WindowsThumbnailProvider.GetThumbnail(path, Constant.ThumbnailSize, Constant.ThumbnailSize, ThumbnailOptions.IconOnly); + } + else if (File.Exists(path)) + { + // Using InvariantCulture since this is internal + var extension = Path.GetExtension(path).ToLower(CultureInfo.InvariantCulture); + if (ImageExtensions.Contains(extension)) { - // Using InvariantCulture since this is internal - var extension = Path.GetExtension(path).ToLower(CultureInfo.InvariantCulture); - if (ImageExtensions.Contains(extension)) + type = ImageType.ImageFile; + if (loadFullImage) + { + image = LoadFullImage(path); + } + else { - type = ImageType.ImageFile; - if (loadFullImage) + // PowerToys Run internal images are png, so we make this exception + if (extension == ".png" || generateThumbnailsFromFiles) { - image = LoadFullImage(path); + /* Although the documentation for GetImage on MSDN indicates that + * if a thumbnail is available it will return one, this has proved to not + * be the case in many situations while testing. + * - Solution: explicitly pass the ThumbnailOnly flag + */ + image = WindowsThumbnailProvider.GetThumbnail(path, Constant.ThumbnailSize, Constant.ThumbnailSize, ThumbnailOptions.ThumbnailOnly); } else { - // PowerToys Run internal images are png, so we make this exception - if (extension == ".png" || generateThumbnailsFromFiles) - { - /* Although the documentation for GetImage on MSDN indicates that - * if a thumbnail is available it will return one, this has proved to not - * be the case in many situations while testing. - * - Solution: explicitly pass the ThumbnailOnly flag - */ - image = WindowsThumbnailProvider.GetThumbnail(path, Constant.ThumbnailSize, Constant.ThumbnailSize, ThumbnailOptions.ThumbnailOnly); - } - else - { - image = WindowsThumbnailProvider.GetThumbnail(path, Constant.ThumbnailSize, Constant.ThumbnailSize, ThumbnailOptions.IconOnly); - } + image = WindowsThumbnailProvider.GetThumbnail(path, Constant.ThumbnailSize, Constant.ThumbnailSize, ThumbnailOptions.IconOnly); } } - else if (!generateThumbnailsFromFiles || (extension == ".pdf" && WindowsThumbnailProvider.DoesPdfUseAcrobatAsProvider())) - { - // The PDF thumbnail provider from Adobe Reader and Acrobat Pro lets crash PT Run with an Dispatcher exception. (https://github.com/microsoft/PowerToys/issues/18166) - // To not run into the crash, we only request the icon of PDF files if the PDF thumbnail handler is set to Adobe Reader/Acrobat Pro. - // Also don't get thumbnail if the GenerateThumbnailsFromFiles option is off. - type = ImageType.File; - image = WindowsThumbnailProvider.GetThumbnail(path, Constant.ThumbnailSize, Constant.ThumbnailSize, ThumbnailOptions.IconOnly); - } - else - { - type = ImageType.File; - image = WindowsThumbnailProvider.GetThumbnail(path, Constant.ThumbnailSize, Constant.ThumbnailSize, ThumbnailOptions.RESIZETOFIT); - } } else { - image = ImageCache[ErrorIconPath]; - path = ErrorIconPath; - } - - if (type != ImageType.Error) - { - image.Freeze(); + type = ImageType.File; + image = WindowsThumbnailProvider.GetThumbnail(path, Constant.ThumbnailSize, Constant.ThumbnailSize, ThumbnailOptions.RESIZETOFIT); } } - catch (System.Exception e) + else { - Log.Exception($"Failed to get thumbnail for {path}", e, MethodBase.GetCurrentMethod().DeclaringType); - type = ImageType.Error; image = ImageCache[ErrorIconPath]; - ImageCache[path] = image; + path = ErrorIconPath; + } + + if (type != ImageType.Error) + { + image.Freeze(); } return new ImageResult(image, type); @@ -227,9 +228,9 @@ private static ImageResult LoadInternal(string path, bool generateThumbnailsFrom private const bool _enableImageHash = true; - public static ImageSource Load(string path, bool generateThumbnailsFromFiles, bool loadFullImage = false) + public static async ValueTask LoadAsync(string path, bool generateThumbnailsFromFiles, bool loadFullImage = false) { - var imageResult = LoadInternal(path, generateThumbnailsFromFiles, loadFullImage); + var imageResult = await LoadInternalAsync(path, generateThumbnailsFromFiles, loadFullImage); var img = imageResult.ImageSource; if (imageResult.ImageType != ImageType.Error && imageResult.ImageType != ImageType.Cache)