diff --git a/src/modules/peek/Peek.FilePreviewer/FilePreview.xaml.cs b/src/modules/peek/Peek.FilePreviewer/FilePreview.xaml.cs index c95714b389..328ddce23f 100644 --- a/src/modules/peek/Peek.FilePreviewer/FilePreview.xaml.cs +++ b/src/modules/peek/Peek.FilePreviewer/FilePreview.xaml.cs @@ -6,6 +6,7 @@ namespace Peek.FilePreviewer { using System; using System.Text; + using System.Threading; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using Microsoft.UI.Xaml; @@ -42,6 +43,8 @@ namespace Peek.FilePreviewer [ObservableProperty] private string imageInfoTooltip = ResourceLoader.GetForViewIndependentUse().GetString("PreviewTooltip_Blank"); + private CancellationTokenSource _cancellationTokenSource = new (); + public FilePreview() { InitializeComponent(); @@ -54,8 +57,12 @@ namespace Peek.FilePreviewer { if (Previewer?.State == PreviewState.Error) { + // Cancel previous loading task + _cancellationTokenSource.Cancel(); + _cancellationTokenSource = new (); + Previewer = previewerFactory.CreateDefaultPreviewer(File); - await UpdatePreviewAsync(); + await UpdatePreviewAsync(_cancellationTokenSource.Token); } } } @@ -89,6 +96,10 @@ namespace Peek.FilePreviewer private async Task OnFilePropertyChanged() { + // Cancel previous loading task + _cancellationTokenSource.Cancel(); + _cancellationTokenSource = new (); + // TODO: track and cancel existing async preview tasks // https://github.com/microsoft/PowerToys/issues/22480 if (File == null) @@ -101,20 +112,31 @@ namespace Peek.FilePreviewer } Previewer = previewerFactory.Create(File); - await UpdatePreviewAsync(); + + await UpdatePreviewAsync(_cancellationTokenSource.Token); } - private async Task UpdatePreviewAsync() + private async Task UpdatePreviewAsync(CancellationToken cancellationToken) { if (Previewer != null) { - var size = await Previewer.GetPreviewSizeAsync(); - SizeFormat windowSizeFormat = UnsupportedFilePreviewer != null ? SizeFormat.Percentage : SizeFormat.Pixels; - PreviewSizeChanged?.Invoke(this, new PreviewSizeChangedArgs(size, windowSizeFormat)); - await Previewer.LoadPreviewAsync(); - } + try + { + cancellationToken.ThrowIfCancellationRequested(); + var size = await Previewer.GetPreviewSizeAsync(cancellationToken); + SizeFormat windowSizeFormat = UnsupportedFilePreviewer != null ? SizeFormat.Percentage : SizeFormat.Pixels; + PreviewSizeChanged?.Invoke(this, new PreviewSizeChangedArgs(size, windowSizeFormat)); + cancellationToken.ThrowIfCancellationRequested(); + await Previewer.LoadPreviewAsync(cancellationToken); - await UpdateImageTooltipAsync(); + cancellationToken.ThrowIfCancellationRequested(); + await UpdateImageTooltipAsync(cancellationToken); + } + catch (OperationCanceledException) + { + // TODO: Log task cancelled exception? + } + } } partial void OnPreviewerChanging(IPreviewer? value) @@ -139,7 +161,7 @@ namespace Peek.FilePreviewer } } - private async Task UpdateImageTooltipAsync() + private async Task UpdateImageTooltipAsync(CancellationToken cancellationToken) { if (File == null) { @@ -152,6 +174,7 @@ namespace Peek.FilePreviewer string fileNameFormatted = ReadableStringHelper.FormatResourceString("PreviewTooltip_FileName", File.FileName); sb.Append(fileNameFormatted); + cancellationToken.ThrowIfCancellationRequested(); string fileType = await PropertyHelper.GetFileType(File.Path); string fileTypeFormatted = string.IsNullOrEmpty(fileType) ? string.Empty : "\n" + ReadableStringHelper.FormatResourceString("PreviewTooltip_FileType", fileType); sb.Append(fileTypeFormatted); @@ -160,10 +183,12 @@ namespace Peek.FilePreviewer string dateModifiedFormatted = string.IsNullOrEmpty(dateModified) ? string.Empty : "\n" + ReadableStringHelper.FormatResourceString("PreviewTooltip_DateModified", dateModified); sb.Append(dateModifiedFormatted); + cancellationToken.ThrowIfCancellationRequested(); Size dimensions = await PropertyHelper.GetImageSize(File.Path); string dimensionsFormatted = dimensions.IsEmpty ? string.Empty : "\n" + ReadableStringHelper.FormatResourceString("PreviewTooltip_Dimensions", dimensions.Width, dimensions.Height); sb.Append(dimensionsFormatted); + cancellationToken.ThrowIfCancellationRequested(); ulong bytes = await PropertyHelper.GetFileSizeInBytes(File.Path); string fileSize = ReadableStringHelper.BytesToReadableString(bytes); string fileSizeFormatted = string.IsNullOrEmpty(fileSize) ? string.Empty : "\n" + ReadableStringHelper.FormatResourceString("PreviewTooltip_FileSize", fileSize); diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/IPreviewer.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/IPreviewer.cs index 473cd0a7aa..1765d6282d 100644 --- a/src/modules/peek/Peek.FilePreviewer/Previewers/IPreviewer.cs +++ b/src/modules/peek/Peek.FilePreviewer/Previewers/IPreviewer.cs @@ -6,6 +6,7 @@ namespace Peek.FilePreviewer.Previewers { using System; using System.ComponentModel; + using System.Threading; using System.Threading.Tasks; using Windows.Foundation; @@ -15,9 +16,9 @@ namespace Peek.FilePreviewer.Previewers public static bool IsFileTypeSupported(string fileExt) => throw new NotImplementedException(); - public Task GetPreviewSizeAsync(); + public Task GetPreviewSizeAsync(CancellationToken cancellationToken); - Task LoadPreviewAsync(); + Task LoadPreviewAsync(CancellationToken cancellationToken); } public enum PreviewState diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/ImagePreviewer/ImagePreviewer.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/ImagePreviewer/ImagePreviewer.cs index a6b4f0b091..3a7c928b8c 100644 --- a/src/modules/peek/Peek.FilePreviewer/Previewers/ImagePreviewer/ImagePreviewer.cs +++ b/src/modules/peek/Peek.FilePreviewer/Previewers/ImagePreviewer/ImagePreviewer.cs @@ -49,18 +49,14 @@ namespace Peek.FilePreviewer.Previewers private bool IsFullImageLoaded => FullQualityImageTask?.Status == TaskStatus.RanToCompletion; - private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); - - private CancellationToken CancellationToken => _cancellationTokenSource.Token; - public void Dispose() { - _cancellationTokenSource.Dispose(); GC.SuppressFinalize(this); } - public async Task GetPreviewSizeAsync() + public async Task GetPreviewSizeAsync(CancellationToken cancellationToken) { + cancellationToken.ThrowIfCancellationRequested(); var propertyImageSize = await PropertyHelper.GetImageSize(File.Path); if (propertyImageSize != Size.Empty) { @@ -70,13 +66,14 @@ namespace Peek.FilePreviewer.Previewers return await WICHelper.GetImageSize(File.Path); } - public async Task LoadPreviewAsync() + public async Task LoadPreviewAsync(CancellationToken cancellationToken) { State = PreviewState.Loading; - LowQualityThumbnailTask = LoadLowQualityThumbnailAsync(); - HighQualityThumbnailTask = LoadHighQualityThumbnailAsync(); - FullQualityImageTask = LoadFullQualityImageAsync(); + LowQualityThumbnailTask = LoadLowQualityThumbnailAsync(cancellationToken); + HighQualityThumbnailTask = LoadHighQualityThumbnailAsync(cancellationToken); + FullQualityImageTask = LoadFullQualityImageAsync(cancellationToken); + cancellationToken.ThrowIfCancellationRequested(); await Task.WhenAll(LowQualityThumbnailTask, HighQualityThumbnailTask, FullQualityImageTask); @@ -97,15 +94,11 @@ namespace Peek.FilePreviewer.Previewers } } - private Task LoadLowQualityThumbnailAsync() + private Task LoadLowQualityThumbnailAsync(CancellationToken cancellationToken) { return TaskExtension.RunSafe(async () => { - if (CancellationToken.IsCancellationRequested) - { - _cancellationTokenSource = new CancellationTokenSource(); - return; - } + cancellationToken.ThrowIfCancellationRequested(); if (!IsFullImageLoaded && !IsHighQualityThumbnailLoaded) { @@ -117,24 +110,23 @@ namespace Peek.FilePreviewer.Previewers throw new ArgumentNullException(nameof(hbitmap)); } + cancellationToken.ThrowIfCancellationRequested(); + await Dispatcher.RunOnUiThread(async () => { - var thumbnailBitmap = await GetBitmapFromHBitmapAsync(hbitmap); + cancellationToken.ThrowIfCancellationRequested(); + var thumbnailBitmap = await GetBitmapFromHBitmapAsync(hbitmap, cancellationToken); Preview = thumbnailBitmap; }); } }); } - private Task LoadHighQualityThumbnailAsync() + private Task LoadHighQualityThumbnailAsync(CancellationToken cancellationToken) { return TaskExtension.RunSafe(async () => { - if (CancellationToken.IsCancellationRequested) - { - _cancellationTokenSource = new CancellationTokenSource(); - return; - } + cancellationToken.ThrowIfCancellationRequested(); if (!IsFullImageLoaded) { @@ -146,29 +138,29 @@ namespace Peek.FilePreviewer.Previewers throw new ArgumentNullException(nameof(hbitmap)); } + cancellationToken.ThrowIfCancellationRequested(); + await Dispatcher.RunOnUiThread(async () => { - var thumbnailBitmap = await GetBitmapFromHBitmapAsync(hbitmap); + cancellationToken.ThrowIfCancellationRequested(); + var thumbnailBitmap = await GetBitmapFromHBitmapAsync(hbitmap, cancellationToken); Preview = thumbnailBitmap; }); } }); } - private Task LoadFullQualityImageAsync() + private Task LoadFullQualityImageAsync(CancellationToken cancellationToken) { return TaskExtension.RunSafe(async () => { - if (CancellationToken.IsCancellationRequested) - { - _cancellationTokenSource = new CancellationTokenSource(); - return; - } + cancellationToken.ThrowIfCancellationRequested(); // TODO: Check if this is performant await Dispatcher.RunOnUiThread(async () => { - var bitmap = await GetFullBitmapFromPathAsync(File.Path); + cancellationToken.ThrowIfCancellationRequested(); + var bitmap = await GetFullBitmapFromPathAsync(File.Path, cancellationToken); Preview = bitmap; }); }); @@ -183,27 +175,34 @@ namespace Peek.FilePreviewer.Previewers return hasFailedLoadingLowQualityThumbnail && hasFailedLoadingHighQualityThumbnail && hasFailedLoadingFullQualityImage; } - private static async Task GetFullBitmapFromPathAsync(string path) + private static async Task GetFullBitmapFromPathAsync(string path, CancellationToken cancellationToken) { var bitmap = new BitmapImage(); + + cancellationToken.ThrowIfCancellationRequested(); using (FileStream stream = System.IO.File.OpenRead(path)) { + cancellationToken.ThrowIfCancellationRequested(); await bitmap.SetSourceAsync(stream.AsRandomAccessStream()); } return bitmap; } - private static async Task GetBitmapFromHBitmapAsync(IntPtr hbitmap) + private static async Task GetBitmapFromHBitmapAsync(IntPtr hbitmap, CancellationToken cancellationToken) { try { var bitmap = System.Drawing.Image.FromHbitmap(hbitmap); var bitmapImage = new BitmapImage(); + + cancellationToken.ThrowIfCancellationRequested(); using (var stream = new MemoryStream()) { bitmap.Save(stream, ImageFormat.Bmp); stream.Position = 0; + + cancellationToken.ThrowIfCancellationRequested(); await bitmapImage.SetSourceAsync(stream.AsRandomAccessStream()); } diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/PngPreviewer/PngPreviewer.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/PngPreviewer/PngPreviewer.cs index dbdf943738..9162bc535c 100644 --- a/src/modules/peek/Peek.FilePreviewer/Previewers/PngPreviewer/PngPreviewer.cs +++ b/src/modules/peek/Peek.FilePreviewer/Previewers/PngPreviewer/PngPreviewer.cs @@ -46,19 +46,14 @@ namespace Peek.FilePreviewer.Previewers private Task? FullQualityImageTask { get; set; } - private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); - - private CancellationToken CancellationToken => _cancellationTokenSource.Token; - private bool IsFullImageLoaded => FullQualityImageTask?.Status == TaskStatus.RanToCompletion; public void Dispose() { - _cancellationTokenSource.Dispose(); GC.SuppressFinalize(this); } - public async Task GetPreviewSizeAsync() + public async Task GetPreviewSizeAsync(CancellationToken cancellationToken) { var propertyImageSize = await PropertyHelper.GetImageSize(File.Path); if (propertyImageSize != Size.Empty) @@ -66,16 +61,18 @@ namespace Peek.FilePreviewer.Previewers return propertyImageSize; } + cancellationToken.ThrowIfCancellationRequested(); return await WICHelper.GetImageSize(File.Path); } - public async Task LoadPreviewAsync() + public async Task LoadPreviewAsync(CancellationToken cancellationToken) { State = PreviewState.Loading; - PreviewQualityThumbnailTask = LoadPreviewImageAsync(); - FullQualityImageTask = LoadFullImageAsync(); + PreviewQualityThumbnailTask = LoadPreviewImageAsync(cancellationToken); + FullQualityImageTask = LoadFullImageAsync(cancellationToken); + cancellationToken.ThrowIfCancellationRequested(); await Task.WhenAll(PreviewQualityThumbnailTask, FullQualityImageTask); if (Preview == null) @@ -100,56 +97,54 @@ namespace Peek.FilePreviewer.Previewers } } - private Task LoadPreviewImageAsync() + private Task LoadPreviewImageAsync(CancellationToken cancellationToken) { var thumbnailTCS = new TaskCompletionSource(); return TaskExtension.RunSafe(async () => { - if (CancellationToken.IsCancellationRequested) - { - _cancellationTokenSource = new CancellationTokenSource(); - return; - } + cancellationToken.ThrowIfCancellationRequested(); if (!IsFullImageLoaded) { await Dispatcher.RunOnUiThread(async () => { + cancellationToken.ThrowIfCancellationRequested(); Preview = await ThumbnailHelper.GetThumbnailAsync(File.Path, _png_image_size); }); } }); } - private Task LoadFullImageAsync() + private Task LoadFullImageAsync(CancellationToken cancellationToken) { var thumbnailTCS = new TaskCompletionSource(); return TaskExtension.RunSafe(async () => { - if (CancellationToken.IsCancellationRequested) - { - _cancellationTokenSource = new CancellationTokenSource(); - return; - } - + cancellationToken.ThrowIfCancellationRequested(); await Dispatcher.RunOnUiThread(async () => { WriteableBitmap? bitmap = null; + cancellationToken.ThrowIfCancellationRequested(); var sFile = await StorageFile.GetFileFromPathAsync(File.Path); + + cancellationToken.ThrowIfCancellationRequested(); using (var randomAccessStream = await sFile.OpenStreamForReadAsync()) { // Create an encoder with the desired format + cancellationToken.ThrowIfCancellationRequested(); var decoder = await BitmapDecoder.CreateAsync( BitmapDecoder.PngDecoderId, randomAccessStream.AsRandomAccessStream()); + cancellationToken.ThrowIfCancellationRequested(); var softwareBitmap = await decoder.GetSoftwareBitmapAsync( BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied); // full quality image bitmap = new WriteableBitmap((int)decoder.PixelWidth, (int)decoder.PixelHeight); + cancellationToken.ThrowIfCancellationRequested(); softwareBitmap?.CopyToBuffer(bitmap.PixelBuffer); } diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/PreviewerFactory.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/PreviewerFactory.cs index ab562e592e..b2c06e0062 100644 --- a/src/modules/peek/Peek.FilePreviewer/Previewers/PreviewerFactory.cs +++ b/src/modules/peek/Peek.FilePreviewer/Previewers/PreviewerFactory.cs @@ -4,6 +4,7 @@ namespace Peek.FilePreviewer.Previewers { + using System.Threading; using Peek.Common.Models; public class PreviewerFactory diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/UnsupportedFilePreviewer/UnsupportedFilePreviewer.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/UnsupportedFilePreviewer/UnsupportedFilePreviewer.cs index ac5cbf6a0e..199d63eb7e 100644 --- a/src/modules/peek/Peek.FilePreviewer/Previewers/UnsupportedFilePreviewer/UnsupportedFilePreviewer.cs +++ b/src/modules/peek/Peek.FilePreviewer/Previewers/UnsupportedFilePreviewer/UnsupportedFilePreviewer.cs @@ -70,21 +70,16 @@ namespace Peek.FilePreviewer.Previewers private DispatcherQueue Dispatcher { get; } - private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); - - private CancellationToken CancellationToken => _cancellationTokenSource.Token; - private Task? IconPreviewTask { get; set; } private Task? DisplayInfoTask { get; set; } public void Dispose() { - _cancellationTokenSource.Dispose(); GC.SuppressFinalize(this); } - public Task GetPreviewSizeAsync() + public Task GetPreviewSizeAsync(CancellationToken cancellationToken) { return Task.Run(() => { @@ -92,12 +87,14 @@ namespace Peek.FilePreviewer.Previewers }); } - public async Task LoadPreviewAsync() + public async Task LoadPreviewAsync(CancellationToken cancellationToken) { + cancellationToken.ThrowIfCancellationRequested(); + State = PreviewState.Loading; - IconPreviewTask = LoadIconPreviewAsync(); - DisplayInfoTask = LoadDisplayInfoAsync(); + IconPreviewTask = LoadIconPreviewAsync(cancellationToken); + DisplayInfoTask = LoadDisplayInfoAsync(cancellationToken); await Task.WhenAll(IconPreviewTask, DisplayInfoTask); @@ -107,38 +104,34 @@ namespace Peek.FilePreviewer.Previewers } } - public Task LoadIconPreviewAsync() + public Task LoadIconPreviewAsync(CancellationToken cancellationToken) { return TaskExtension.RunSafe(async () => { - if (CancellationToken.IsCancellationRequested) - { - _cancellationTokenSource = new CancellationTokenSource(); - return; - } + cancellationToken.ThrowIfCancellationRequested(); // TODO: Get icon with transparency IconHelper.GetIcon(Path.GetFullPath(File.Path), out IntPtr hbitmap); + + cancellationToken.ThrowIfCancellationRequested(); await Dispatcher.RunOnUiThread(async () => { - var iconBitmap = await GetBitmapFromHBitmapAsync(hbitmap); + cancellationToken.ThrowIfCancellationRequested(); + var iconBitmap = await GetBitmapFromHBitmapAsync(hbitmap, cancellationToken); IconPreview = iconBitmap; }); }); } - public Task LoadDisplayInfoAsync() + public Task LoadDisplayInfoAsync(CancellationToken cancellationToken) { return TaskExtension.RunSafe(async () => { - if (CancellationToken.IsCancellationRequested) - { - _cancellationTokenSource = new CancellationTokenSource(); - return; - } - // File Properties + cancellationToken.ThrowIfCancellationRequested(); var bytes = await PropertyHelper.GetFileSizeInBytes(File.Path); + + cancellationToken.ThrowIfCancellationRequested(); var type = await PropertyHelper.GetFileType(File.Path); await Dispatcher.RunOnUiThread(() => @@ -169,17 +162,22 @@ namespace Peek.FilePreviewer.Previewers return hasFailedLoadingIconPreview && hasFailedLoadingDisplayInfo; } - // TODO: Move this to a helper file (ImagePrevier uses the same code) - private static async Task GetBitmapFromHBitmapAsync(IntPtr hbitmap) + // TODO: Move this to a helper file (ImagePreviewer uses the same code) + private static async Task GetBitmapFromHBitmapAsync(IntPtr hbitmap, CancellationToken cancellationToken) { try { + cancellationToken.ThrowIfCancellationRequested(); var bitmap = System.Drawing.Image.FromHbitmap(hbitmap); var bitmapImage = new BitmapImage(); + + cancellationToken.ThrowIfCancellationRequested(); using (var stream = new MemoryStream()) { bitmap.Save(stream, ImageFormat.Bmp); stream.Position = 0; + + cancellationToken.ThrowIfCancellationRequested(); await bitmapImage.SetSourceAsync(stream.AsRandomAccessStream()); } diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/WebBrowserPreviewer/WebBrowserPreviewer.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/WebBrowserPreviewer/WebBrowserPreviewer.cs index 7762b60d42..5aabbc16c8 100644 --- a/src/modules/peek/Peek.FilePreviewer/Previewers/WebBrowserPreviewer/WebBrowserPreviewer.cs +++ b/src/modules/peek/Peek.FilePreviewer/Previewers/WebBrowserPreviewer/WebBrowserPreviewer.cs @@ -9,8 +9,6 @@ namespace Peek.FilePreviewer.Previewers using System.Threading; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; - using Microsoft.UI.Dispatching; - using Peek.FilePreviewer.Controls; using Windows.Foundation; using File = Peek.Common.Models.File; @@ -39,14 +37,14 @@ namespace Peek.FilePreviewer.Previewers private File File { get; } - public Task GetPreviewSizeAsync() + public Task GetPreviewSizeAsync(CancellationToken cancellationToken) { // TODO: define how to proper window size on HTML content. var size = new Size(1280, 720); return Task.FromResult(size); } - public Task LoadPreviewAsync() + public Task LoadPreviewAsync(CancellationToken cancellationToken) { State = PreviewState.Loading;