Skip to content

Commit

Permalink
[Peek] Add cancellation token OnFilePropertyChanged (#22643)
Browse files Browse the repository at this point in the history
* Cancel file loading before opening another file

* Add omitted cancellation checks

* Catch task cancelled exception; Add more cancellation checkpoints

* Add cancellation checkpoint beofre GetBitmapFromHBitmapAsync

* Correct typo

* Update to pass cancellation token individually to each async methods

* Add lost cancellationToken source

* Add cancellation token to PngPreviewer

Co-authored-by: Yawen Hou <[email protected]>
  • Loading branch information
Sytta and Sytta authored Dec 9, 2022
1 parent 496220f commit b075c00
Show file tree
Hide file tree
Showing 7 changed files with 113 additions and 96 deletions.
45 changes: 35 additions & 10 deletions src/modules/peek/Peek.FilePreviewer/FilePreview.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -42,6 +43,8 @@ public sealed partial class FilePreview : UserControl
[ObservableProperty]
private string imageInfoTooltip = ResourceLoader.GetForViewIndependentUse().GetString("PreviewTooltip_Blank");

private CancellationTokenSource _cancellationTokenSource = new ();

public FilePreview()
{
InitializeComponent();
Expand All @@ -54,8 +57,12 @@ private async void Previewer_PropertyChanged(object? sender, System.ComponentMod
{
if (Previewer?.State == PreviewState.Error)
{
// Cancel previous loading task
_cancellationTokenSource.Cancel();
_cancellationTokenSource = new ();

Previewer = previewerFactory.CreateDefaultPreviewer(File);
await UpdatePreviewAsync();
await UpdatePreviewAsync(_cancellationTokenSource.Token);
}
}
}
Expand Down Expand Up @@ -89,6 +96,10 @@ public Visibility IsPreviewVisible(IPreviewer? previewer, PreviewState? state)

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)
Expand All @@ -101,20 +112,31 @@ private async Task OnFilePropertyChanged()
}

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);

cancellationToken.ThrowIfCancellationRequested();
await UpdateImageTooltipAsync(cancellationToken);
}
catch (OperationCanceledException)
{
// TODO: Log task cancelled exception?
}
}

await UpdateImageTooltipAsync();
}

partial void OnPreviewerChanging(IPreviewer? value)
Expand All @@ -139,7 +161,7 @@ private void PreviewBrowser_NavigationCompleted(WebView2 sender, Microsoft.Web.W
}
}

private async Task UpdateImageTooltipAsync()
private async Task UpdateImageTooltipAsync(CancellationToken cancellationToken)
{
if (File == null)
{
Expand All @@ -152,6 +174,7 @@ private async Task UpdateImageTooltipAsync()
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);
Expand All @@ -160,10 +183,12 @@ private async Task UpdateImageTooltipAsync()
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);
Expand Down
5 changes: 3 additions & 2 deletions src/modules/peek/Peek.FilePreviewer/Previewers/IPreviewer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ namespace Peek.FilePreviewer.Previewers
{
using System;
using System.ComponentModel;
using System.Threading;
using System.Threading.Tasks;
using Windows.Foundation;

Expand All @@ -15,9 +16,9 @@ public interface IPreviewer : INotifyPropertyChanged

public static bool IsFileTypeSupported(string fileExt) => throw new NotImplementedException();

public Task<Size> GetPreviewSizeAsync();
public Task<Size> GetPreviewSizeAsync(CancellationToken cancellationToken);

Task LoadPreviewAsync();
Task LoadPreviewAsync(CancellationToken cancellationToken);
}

public enum PreviewState
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,18 +49,14 @@ public ImagePreviewer(File file)

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<Size> GetPreviewSizeAsync()
public async Task<Size> GetPreviewSizeAsync(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var propertyImageSize = await PropertyHelper.GetImageSize(File.Path);
if (propertyImageSize != Size.Empty)
{
Expand All @@ -70,13 +66,14 @@ public async Task<Size> GetPreviewSizeAsync()
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);

Expand All @@ -97,15 +94,11 @@ private void OnPropertyChanged(object? sender, System.ComponentModel.PropertyCha
}
}

private Task<bool> LoadLowQualityThumbnailAsync()
private Task<bool> LoadLowQualityThumbnailAsync(CancellationToken cancellationToken)
{
return TaskExtension.RunSafe(async () =>
{
if (CancellationToken.IsCancellationRequested)
{
_cancellationTokenSource = new CancellationTokenSource();
return;
}
cancellationToken.ThrowIfCancellationRequested();

if (!IsFullImageLoaded && !IsHighQualityThumbnailLoaded)
{
Expand All @@ -117,24 +110,23 @@ private Task<bool> LoadLowQualityThumbnailAsync()
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<bool> LoadHighQualityThumbnailAsync()
private Task<bool> LoadHighQualityThumbnailAsync(CancellationToken cancellationToken)
{
return TaskExtension.RunSafe(async () =>
{
if (CancellationToken.IsCancellationRequested)
{
_cancellationTokenSource = new CancellationTokenSource();
return;
}
cancellationToken.ThrowIfCancellationRequested();

if (!IsFullImageLoaded)
{
Expand All @@ -146,29 +138,29 @@ private Task<bool> LoadHighQualityThumbnailAsync()
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<bool> LoadFullQualityImageAsync()
private Task<bool> 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;
});
});
Expand All @@ -183,27 +175,34 @@ private bool HasFailedLoadingPreview()
return hasFailedLoadingLowQualityThumbnail && hasFailedLoadingHighQualityThumbnail && hasFailedLoadingFullQualityImage;
}

private static async Task<BitmapImage> GetFullBitmapFromPathAsync(string path)
private static async Task<BitmapImage> 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<BitmapSource> GetBitmapFromHBitmapAsync(IntPtr hbitmap)
private static async Task<BitmapSource> 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());
}

Expand Down
Loading

2 comments on commit b075c00

@github-actions

This comment was marked as outdated.

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@check-spelling-bot Report

🔴 Please review

See the 📜action log for details.

Unrecognized words (139)
Abbrivation
apidl
ari
arw
BESTEFFORT
BHIDSF
BVal
calpwstr
CARRAY
CElems
Chromakey
cidl
crw
CVal
DANGEROUSLYCOMMITMERELYTODISKCACHE
dcr
dcs
Dct
DDEIf
Dds
DELAYCREATION
drf
Dwma
eip
Exa
exabyte
Excep
EXCEPINFO
EXTRINSICPROPERTIES
EXTRINSICPROPERTIESONLY
FASTPROPERTIESONLY
filetime
HANDLERPROPERTIESONLY
HDR
hicon
hif
HVal
IBitmap
IBlock
IColor
icolumn
IContext
IDecoder
IEncoder
IEnum
IIDI
iiq
IMetadata
IPalette
IQuery
IReader
ISource
ISurface
ithumbnail
jfi
jif
kdc
Keybd
Lcid
LOCKBYTES
LOCKTYPE
LVal
mdc
mef
Mega
mrw
neighborings
NOOPEN
nrw
ONLYIFCURRENT
ONLYONCE
OPENSLOWITEM
openspecs
OPLOCK
ori
overriden
pbgra
PBlob
pcch
pcelt
pcs
pef
PElems
Percision
petabyte
pkey
ppenum
pprop
PREFERQUERYPROPERTIES
PRGBA
PROPERTYNOTFOUND
PROPVARIANT
pscid
psfi
pstatstg
pstm
pui
pvar
raf
retunred
rfid
RGBE
rgelt
rgf
rwl
rwz
sachaple
SAFEARRAY
SCID
Scode
Shcontf
SHELLDETAILS
Shgno
Softcoded
srf
SRGB
STGC
STGTY
Stroe
Strret
tcs
terabyte
titlebar
tlbimp
toogle
UMsg
UOffset
USERDEFINED
UType
VARTYPE
VERSIONED
webbrowser
windowsapp
WMSDK
WReserved
WScan
wsp
WVk
YQuantized
Previously acknowledged words that are now absent brucelindbloom chromaticities companding DCR Eqn ffaa FILETIME HICON ITHUMBNAIL Pbgra PKEY Windowsapp :arrow_right:
To accept ✔️ these unrecognized words as correct and remove the previously acknowledged and now absent words, run the following commands

... in a clone of the [email protected]:microsoft/PowerToys.git repository
on the rpontin/peek-webview2-improvements branch (ℹ️ how do I use this?):

curl -s -S -L 'https://raw.githubusercontent.com/check-spelling/check-spelling/v0.0.21/apply.pl' |
perl - 'https://github.com/microsoft/PowerToys/actions/runs/3659913450/attempts/1'
Available 📚 dictionaries could cover words not in the 📘 dictionary

This includes both expected items (2140) from .github/actions/spell-check/expect.txt and unrecognized words (139)

Dictionary Entries Covers
cspell:win32/src/win32.txt 53509 133
cspell:cpp/src/cpp.txt 30216 129
cspell:python/src/python/python-lib.txt 3873 32
cspell:php/php.txt 2597 17
cspell:node/node.txt 1768 14
cspell:typescript/typescript.txt 1211 12
cspell:python/src/python/python.txt 453 11
cspell:java/java.txt 7642 11
cspell:aws/aws.txt 218 8
cspell:r/src/r.txt 808 7

Consider adding them using (in .github/workflows/spelling2.yml):

      with:
        extra_dictionaries:
          cspell:win32/src/win32.txt
          cspell:cpp/src/cpp.txt
          cspell:python/src/python/python-lib.txt
          cspell:php/php.txt
          cspell:node/node.txt
          cspell:typescript/typescript.txt
          cspell:python/src/python/python.txt
          cspell:java/java.txt
          cspell:aws/aws.txt
          cspell:r/src/r.txt

To stop checking additional dictionaries, add:

      with:
        check_extra_dictionaries: ''
Errors (1)

See the 📜action log for details.

❌ Errors Count
❌ forbidden-pattern 1

See ❌ Event descriptions for more information.

If the flagged items are 🤯 false positives

If items relate to a ...

  • binary file (or some other file you wouldn't want to check at all).

    Please add a file path to the excludes.txt file matching the containing file.

    File paths are Perl 5 Regular Expressions - you can test yours before committing to verify it will match your files.

    ^ refers to the file's path from the root of the repository, so ^README\.md$ would exclude README.md (on whichever branch you're using).

  • well-formed pattern.

    If you can write a pattern that would match it,
    try adding it to the patterns.txt file.

    Patterns are Perl 5 Regular Expressions - you can test yours before committing to verify it will match your lines.

    Note that patterns can't match multiline strings.

Please sign in to comment.