Compare commits

...

13 Commits

Author SHA1 Message Date
Esteban Margaron
1e71bbdf43 Fix item idx not being returned 2022-12-08 22:52:53 -08:00
Esteban Margaron
ea1cea7b7e Temp early merge of cancellation pattern pr 2022-12-08 22:37:00 -08:00
Esteban Margaron
5fa7205458 Merge peek branch 2022-12-08 21:44:04 -08:00
Esteban Margaron
853086d243 Refactor out to polymorphic folder item sources 2022-12-08 21:32:18 -08:00
Esteban Margaron
ac17cadce3 Add very rough prototype of new query system 2022-12-08 17:48:06 -08:00
Robson
195f14a3c7 Add full image quality support (#22654) 2022-12-08 17:45:39 -08:00
Yawen Hou
f2a7a5ce1a Update to pass cancellation token individually to each async methods 2022-12-08 19:50:59 -05:00
Yawen Hou
e8ebf73033 Correct typo 2022-12-08 19:24:38 -05:00
Yawen Hou
5540aeb20c Add cancellation checkpoint beofre GetBitmapFromHBitmapAsync 2022-12-08 19:23:44 -05:00
Yawen Hou
ab898a2911 Catch task cancelled exception; Add more cancellation checkpoints 2022-12-08 19:18:41 -05:00
Yawen Hou
32bd48e6de Merge master 2022-12-08 17:35:40 -05:00
Yawen Hou
88c7435f93 Add omitted cancellation checks 2022-12-08 17:17:47 -05:00
Yawen Hou
89437f6749 Cancel file loading before opening another file 2022-12-08 17:14:31 -05:00
15 changed files with 419 additions and 175 deletions

View File

@@ -6,11 +6,11 @@ namespace Peek.FilePreviewer
{
using System;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media.Imaging;
using Peek.Common.Helpers;
using Peek.Common.Models;
using Peek.FilePreviewer.Models;
@@ -42,6 +42,8 @@ namespace Peek.FilePreviewer
[ObservableProperty]
private string imageInfoTooltip = ResourceLoader.GetForViewIndependentUse().GetString("PreviewTooltip_Blank");
private CancellationTokenSource _cancellationTokenSource = new ();
public FilePreview()
{
InitializeComponent();
@@ -54,8 +56,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 +95,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 +111,30 @@ 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);
cancellationToken.ThrowIfCancellationRequested();
await UpdateImageTooltipAsync();
}
catch (OperationCanceledException)
{
// TODO: Log task cancelled exception?
}
}
await UpdateImageTooltipAsync();
}
partial void OnPreviewerChanging(IPreviewer? value)

View File

@@ -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<Size> GetPreviewSizeAsync();
public Task<Size> GetPreviewSizeAsync(CancellationToken cancellationToken);
Task LoadPreviewAsync();
Task LoadPreviewAsync(CancellationToken cancellationToken);
}
public enum PreviewState

View File

@@ -5,6 +5,7 @@
namespace Peek.FilePreviewer.Previewers
{
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Peek.Common.Models;
@@ -16,14 +17,21 @@ namespace Peek.FilePreviewer.Previewers
{
return Task.Run(() =>
{
var propertyStore = PropertyStoreShellApi.GetPropertyStoreFromPath(filePath, PropertyStoreShellApi.PropertyStoreFlags.OPENSLOWITEM);
if (propertyStore != null)
try
{
var width = (int)PropertyStoreShellApi.GetUIntFromPropertyStore(propertyStore, PropertyStoreShellApi.PropertyKey.ImageHorizontalSize);
var height = (int)PropertyStoreShellApi.GetUIntFromPropertyStore(propertyStore, PropertyStoreShellApi.PropertyKey.ImageVerticalSize);
var propertyStore = PropertyStoreShellApi.GetPropertyStoreFromPath(filePath, PropertyStoreShellApi.PropertyStoreFlags.OPENSLOWITEM);
if (propertyStore != null)
{
var width = (int)PropertyStoreShellApi.GetUIntFromPropertyStore(propertyStore, PropertyStoreShellApi.PropertyKey.ImageHorizontalSize);
var height = (int)PropertyStoreShellApi.GetUIntFromPropertyStore(propertyStore, PropertyStoreShellApi.PropertyKey.ImageVerticalSize);
Marshal.ReleaseComObject(propertyStore);
return new Size(width, height);
Marshal.ReleaseComObject(propertyStore);
return new Size(width, height);
}
}
catch (Exception e)
{
Debug.Write("Error getting image dimensions:\n" + e.ToString());
}
return Size.Empty;
@@ -33,12 +41,19 @@ namespace Peek.FilePreviewer.Previewers
public static Task<ulong> GetFileSizeInBytes(string filePath)
{
ulong bytes = 0;
var propertyStore = PropertyStoreShellApi.GetPropertyStoreFromPath(filePath, PropertyStoreShellApi.PropertyStoreFlags.OPENSLOWITEM);
if (propertyStore != null)
try
{
bytes = PropertyStoreShellApi.GetULongFromPropertyStore(propertyStore, PropertyStoreShellApi.PropertyKey.FileSizeBytes);
var propertyStore = PropertyStoreShellApi.GetPropertyStoreFromPath(filePath, PropertyStoreShellApi.PropertyStoreFlags.OPENSLOWITEM);
if (propertyStore != null)
{
bytes = PropertyStoreShellApi.GetULongFromPropertyStore(propertyStore, PropertyStoreShellApi.PropertyKey.FileSizeBytes);
Marshal.ReleaseComObject(propertyStore);
Marshal.ReleaseComObject(propertyStore);
}
}
catch (Exception e)
{
Debug.WriteLine("Error getting folder item size:\n", e.ToString());
}
return Task.FromResult(bytes);
@@ -47,13 +62,20 @@ namespace Peek.FilePreviewer.Previewers
public static Task<string> GetFileType(string filePath)
{
var type = string.Empty;
var propertyStore = PropertyStoreShellApi.GetPropertyStoreFromPath(filePath, PropertyStoreShellApi.PropertyStoreFlags.OPENSLOWITEM);
if (propertyStore != null)
try
{
// TODO: find a way to get user friendly description
type = PropertyStoreShellApi.GetStringFromPropertyStore(propertyStore, PropertyStoreShellApi.PropertyKey.FileType);
var propertyStore = PropertyStoreShellApi.GetPropertyStoreFromPath(filePath, PropertyStoreShellApi.PropertyStoreFlags.OPENSLOWITEM);
if (propertyStore != null)
{
// TODO: find a way to get user friendly description
type = PropertyStoreShellApi.GetStringFromPropertyStore(propertyStore, PropertyStoreShellApi.PropertyKey.FileType);
Marshal.ReleaseComObject(propertyStore);
Marshal.ReleaseComObject(propertyStore);
}
}
catch (Exception e)
{
Debug.WriteLine("Failed to get file type:\n" + e.ToString());
}
return Task.FromResult(type);

View File

@@ -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<Size> GetPreviewSizeAsync()
public async Task<Size> 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<bool> LoadLowQualityThumbnailAsync()
private Task<bool> 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<bool> LoadHighQualityThumbnailAsync()
private Task<bool> 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<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;
});
});
@@ -183,27 +175,34 @@ namespace Peek.FilePreviewer.Previewers
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());
}

View File

@@ -5,18 +5,16 @@
namespace Peek.FilePreviewer.Previewers
{
using System;
using System.Collections.Generic;
using System.Drawing.Imaging;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml.Media.Imaging;
using Peek.Common;
using Peek.Common.Extensions;
using Windows.Foundation;
using Windows.Graphics.Imaging;
using Windows.Storage.Streams;
using Windows.Storage;
using File = Peek.Common.Models.File;
public partial class PngPreviewer : ObservableObject, IBitmapPreviewer
@@ -44,17 +42,23 @@ namespace Peek.FilePreviewer.Previewers
private DispatcherQueue Dispatcher { get; }
private Task<bool>? PreviewQualityThumbnailTask { get; set; }
private Task<bool>? 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<Size> GetPreviewSizeAsync()
public async Task<Size> GetPreviewSizeAsync(CancellationToken cancellationToken)
{
var propertyImageSize = await PropertyHelper.GetImageSize(File.Path);
if (propertyImageSize != Size.Empty)
@@ -65,13 +69,14 @@ namespace Peek.FilePreviewer.Previewers
return await WICHelper.GetImageSize(File.Path);
}
public async Task LoadPreviewAsync()
public async Task LoadPreviewAsync(CancellationToken cancellationToken)
{
State = PreviewState.Loading;
var previewTask = LoadPreviewImageAsync();
PreviewQualityThumbnailTask = LoadPreviewImageAsync();
FullQualityImageTask = LoadFullImageAsync();
await Task.WhenAll(previewTask);
await Task.WhenAll(PreviewQualityThumbnailTask, FullQualityImageTask);
if (Preview == null)
{
@@ -95,10 +100,10 @@ namespace Peek.FilePreviewer.Previewers
}
}
private Task LoadPreviewImageAsync()
private Task<bool> LoadPreviewImageAsync()
{
var thumbnailTCS = new TaskCompletionSource();
Dispatcher.TryEnqueue(async () =>
return TaskExtension.RunSafe(async () =>
{
if (CancellationToken.IsCancellationRequested)
{
@@ -106,12 +111,51 @@ namespace Peek.FilePreviewer.Previewers
return;
}
Preview = await ThumbnailHelper.GetThumbnailAsync(File.Path, _png_image_size);
thumbnailTCS.SetResult();
if (!IsFullImageLoaded)
{
await Dispatcher.RunOnUiThread(async () =>
{
Preview = await ThumbnailHelper.GetThumbnailAsync(File.Path, _png_image_size);
});
}
});
}
return thumbnailTCS.Task;
private Task<bool> LoadFullImageAsync()
{
var thumbnailTCS = new TaskCompletionSource();
return TaskExtension.RunSafe(async () =>
{
if (CancellationToken.IsCancellationRequested)
{
_cancellationTokenSource = new CancellationTokenSource();
return;
}
await Dispatcher.RunOnUiThread(async () =>
{
WriteableBitmap? bitmap = null;
var sFile = await StorageFile.GetFileFromPathAsync(File.Path);
using (var randomAccessStream = await sFile.OpenStreamForReadAsync())
{
// Create an encoder with the desired format
var decoder = await BitmapDecoder.CreateAsync(
BitmapDecoder.PngDecoderId,
randomAccessStream.AsRandomAccessStream());
var softwareBitmap = await decoder.GetSoftwareBitmapAsync(
BitmapPixelFormat.Bgra8,
BitmapAlphaMode.Premultiplied);
// full quality image
bitmap = new WriteableBitmap((int)decoder.PixelWidth, (int)decoder.PixelHeight);
softwareBitmap?.CopyToBuffer(bitmap.PixelBuffer);
}
Preview = bitmap;
});
});
}
}
}

View File

@@ -4,6 +4,7 @@
namespace Peek.FilePreviewer.Previewers
{
using System.Threading;
using Peek.Common.Models;
public class PreviewerFactory

View File

@@ -70,21 +70,16 @@ namespace Peek.FilePreviewer.Previewers
private DispatcherQueue Dispatcher { get; }
private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
private CancellationToken CancellationToken => _cancellationTokenSource.Token;
private Task<bool>? IconPreviewTask { get; set; }
private Task<bool>? DisplayInfoTask { get; set; }
public void Dispose()
{
_cancellationTokenSource.Dispose();
GC.SuppressFinalize(this);
}
public Task<Size> GetPreviewSizeAsync()
public Task<Size> 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<bool> LoadIconPreviewAsync()
public Task<bool> 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<bool> LoadDisplayInfoAsync()
public Task<bool> 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<BitmapSource> GetBitmapFromHBitmapAsync(IntPtr hbitmap)
// TODO: Move this to a helper file (ImagePreviewer uses the same code)
private static async Task<BitmapSource> 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());
}

View File

@@ -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<Size> GetPreviewSizeAsync()
public Task<Size> 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;

View File

@@ -0,0 +1,25 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Peek.UI.FolderItemSources
{
using System.Threading.Tasks;
using Peek.Common.Models;
using Shell32;
public interface IFolderItemsSource
{
// Result is null if no file at index
public Task<File?> GetItemAt(uint index);
public Task<InitialQueryData?> Initialize(IShellFolderViewDual3 folderView);
}
public struct InitialQueryData
{
public uint FirstItemIndex { get; set; }
public uint ItemsCount { get; set; }
}
}

View File

@@ -0,0 +1,24 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Peek.UI.FolderItemSources
{
using System.Threading.Tasks;
using Peek.Common.Models;
using Shell32;
// Source of folder items for use when user activates Peek with multiple selected files
public class SelectedItemsSource : IFolderItemsSource
{
public Task<File?> GetItemAt(uint index)
{
throw new System.NotImplementedException();
}
public Task<InitialQueryData?> Initialize(IShellFolderViewDual3 folderView)
{
throw new System.NotImplementedException();
}
}
}

View File

@@ -0,0 +1,112 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Peek.UI.FolderItemSources
{
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Peek.Common.Models;
using Shell32;
using Windows.Storage;
using Windows.Storage.Search;
// Provides folder items across an entire folder
public class WholeFolderItemsSource : IFolderItemsSource
{
private StorageItemQueryResult? ItemQuery { get; set; } = null;
public async Task<File?> GetItemAt(uint index)
{
if (ItemQuery == null)
{
return null;
}
IReadOnlyList<IStorageItem> items;
try
{
// ~1ms runtime on workstation w/ a debugger attached.
// TODO: further optimize by pre-fetching and maintaining a window of items.
// There's a win32 API FindNextFile we could have used, but it doesn't allow fast, reverse iteration,
// which is needed for backwards navigation.
items = await ItemQuery.GetItemsAsync(index, 1);
if (items == null || items.Count == 0)
{
return null;
}
}
catch (Exception e)
{
Debug.WriteLine("Caught exception attempting to get file:\n", e.ToString());
return null;
}
// TODO: optimize by adding StorageItem as field to File class
return new File(items.First().Path);
}
public async Task<InitialQueryData?> Initialize(IShellFolderViewDual3 folderView)
{
try
{
var selectedItems = folderView.SelectedItems();
if (selectedItems == null || selectedItems.Count == 0)
{
return null;
}
Debug.Assert(selectedItems.Count == 1, "SelectedItemsSource is intended for multi-item activations");
var selectedItem = selectedItems.Item(0);
var parent = System.IO.Directory.GetParent(selectedItem.Path); // TODO: try get directory name instead
if (parent == null)
{
return null;
}
var folder = await StorageFolder.GetFolderFromPathAsync(parent.FullName);
// TODO: check if query options are supported (member helpers)
var queryOptions = new QueryOptions();
queryOptions.IndexerOption = IndexerOption.UseIndexerWhenAvailable;
// TODO: check if this clear is actually needed
Debug.WriteLine("count: " + queryOptions.SortOrder.Count);
queryOptions.SortOrder.Clear();
// TODO: fetch sort option
queryOptions.SortOrder.Add(new SortEntry("System.Size", false));
ItemQuery = folder.CreateItemQuery();
ItemQuery.ApplyNewQueryOptions(queryOptions);
Debug.WriteLine(selectedItem.Name);
// TODO: property passed in depends on sort order passed to query
var firstItemIndex = await ItemQuery.FindStartIndexAsync(selectedItem.Size);
// FindStartIndexAsync returns this when no item found
if (firstItemIndex == uint.MaxValue)
{
Debug.WriteLine("File not found");
return null;
}
InitialQueryData result = new ();
// TODO: pass & throw cancellation token (not essential, but may free thread resources earlier)
result.ItemsCount = await ItemQuery.GetItemCountAsync();
result.FirstItemIndex = firstItemIndex;
return result;
}
catch (Exception)
{
return null;
}
}
}
}

View File

@@ -7,12 +7,16 @@ namespace Peek.UI
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.UI.Dispatching;
using Peek.Common.Models;
using Peek.UI.FolderItemSources;
using Peek.UI.Helpers;
using Windows.Storage;
using Windows.Storage.Search;
public partial class FolderItemsQuery : ObservableObject
{
@@ -24,7 +28,7 @@ namespace Peek.UI
private File? currentFile;
[ObservableProperty]
private List<File> files = new ();
private int itemsCount = 0;
[ObservableProperty]
private bool isMultiSelection;
@@ -34,52 +38,66 @@ namespace Peek.UI
private CancellationTokenSource CancellationTokenSource { get; set; } = new CancellationTokenSource();
private Task? InitializeFilesTask { get; set; } = null;
private Task? InitializeQueryTask { get; set; } = null;
private IFolderItemsSource? FolderItemsSource { get; set; } = null;
// Must be called from UI thread
public void Clear()
{
CurrentFile = null;
IsMultiSelection = false;
if (InitializeFilesTask != null && InitializeFilesTask.Status == TaskStatus.Running)
if (InitializeQueryTask != null && InitializeQueryTask.Status == TaskStatus.Running)
{
Debug.WriteLine("Detected existing initializeFilesTask running. Cancelling it..");
Debug.WriteLine("Detected existing InitializeQueryTask running. Cancelling it..");
CancellationTokenSource.Cancel();
}
InitializeFilesTask = null;
InitializeQueryTask = null;
lock (_mutateQueryDataLock)
{
_dispatcherQueue.TryEnqueue(() =>
{
Files = new List<File>();
CurrentItemIndex = UninitializedItemIndex;
});
ItemsCount = 0; // TODO: can maybe move outside?
CurrentItemIndex = UninitializedItemIndex;
}
}
public void UpdateCurrentItemIndex(int desiredIndex)
// Must be called from UI thread
public async Task UpdateCurrentItemIndex(int desiredIndex)
{
if (Files.Count <= 1 || CurrentItemIndex == UninitializedItemIndex ||
(InitializeFilesTask != null && InitializeFilesTask.Status == TaskStatus.Running))
// TODO: add items count check
if (ItemsCount <= 1 || CurrentItemIndex == UninitializedItemIndex || FolderItemsSource == null ||
(InitializeQueryTask != null && InitializeQueryTask.Status == TaskStatus.Running))
{
return;
}
// Current index wraps around when reaching min/max folder item indices
desiredIndex %= Files.Count;
CurrentItemIndex = desiredIndex < 0 ? Files.Count + desiredIndex : desiredIndex;
desiredIndex %= itemsCount;
CurrentItemIndex = desiredIndex < 0 ? itemsCount + desiredIndex : desiredIndex;
if (CurrentItemIndex < 0 || CurrentItemIndex >= Files.Count)
if (CurrentItemIndex < 0 || CurrentItemIndex >= itemsCount)
{
Debug.Assert(false, "Out of bounds folder item index detected.");
CurrentItemIndex = 0;
}
CurrentFile = Files[CurrentItemIndex];
if (FolderItemsSource == null)
{
return;
}
var temp = await FolderItemsSource.GetItemAt((uint)CurrentItemIndex); // TODO: fix uint declarations
if (temp != null)
{
Debug.WriteLine("Navigated to " + temp.FileName);
}
CurrentFile = temp;
}
// Must be called from UI thread
public void Start()
{
var folderView = FileExplorerHelper.GetCurrentFolderView();
@@ -108,75 +126,54 @@ namespace Peek.UI
try
{
if (InitializeFilesTask != null && InitializeFilesTask.Status == TaskStatus.Running)
if (InitializeQueryTask != null && InitializeQueryTask.Status == TaskStatus.Running)
{
Debug.WriteLine("Detected unexpected existing initializeFilesTask running. Cancelling it..");
Debug.WriteLine("Detected unexpected existing InitializeQueryTask running. Cancelling it..");
CancellationTokenSource.Cancel();
}
CancellationTokenSource = new CancellationTokenSource();
InitializeFilesTask = new Task(() => InitializeFiles(items, firstSelectedItem, CancellationTokenSource.Token));
InitializeQueryTask = new Task(() => InitializeQuery(folderView, items, firstSelectedItem, CancellationTokenSource.Token));
// Execute file initialization/querying on background thread
InitializeFilesTask.Start();
InitializeQueryTask.Start();
}
catch (Exception e)
{
Debug.WriteLine("Exception trying to run initializeFilesTask:\n" + e.ToString());
Debug.WriteLine("Exception trying to run InitializeQueryTask:\n" + e.ToString());
}
}
// Finds index of firstSelectedItem either amongst folder items, initializing our internal File list
// since storing Shell32.FolderItems as a field isn't reliable.
// Can take a few seconds for folders with 1000s of items; ensure it runs on a background thread.
//
// TODO optimization:
// Handle case where selected items count > 1 separately. Although it'll still be slow for 1000s of items selected,
// we can leverage faster APIs like Windows.Storage when 1 item is selected, and navigation is scoped to
// the entire folder. We can then avoid iterating through all items here, and maintain a dynamic window of
// loaded items around the current item index.
private void InitializeFiles(
private async void InitializeQuery(
Shell32.IShellFolderViewDual3 folderView, // TODO: remove
Shell32.FolderItems items,
Shell32.FolderItem firstSelectedItem,
CancellationToken cancellationToken)
{
var tempFiles = new List<File>(items.Count);
var tempCurIndex = UninitializedItemIndex;
FolderItemsSource = IsMultiSelection ? new SelectedItemsSource() : new WholeFolderItemsSource();
for (int i = 0; i < items.Count; i++)
InitialQueryData? initialQueryData = await FolderItemsSource.Initialize(folderView);
if (initialQueryData == null || !initialQueryData.HasValue)
{
cancellationToken.ThrowIfCancellationRequested();
var item = items.Item(i);
if (item == null)
{
continue;
}
if (item.Name == firstSelectedItem.Name)
{
tempCurIndex = i;
}
tempFiles.Add(new File(item.Path));
}
if (tempCurIndex == UninitializedItemIndex)
{
Debug.WriteLine("File query initialization: selectedItem index not found. Navigation remains disabled.");
return;
}
InitialQueryData y = new ();
y.FirstItemIndex = 0;
cancellationToken.ThrowIfCancellationRequested();
// Lock to prevent race conditions with UI thread's Clear calls upon Peek deactivation/hide
lock (_mutateQueryDataLock)
{
cancellationToken.ThrowIfCancellationRequested();
_dispatcherQueue.TryEnqueue(() =>
{
Files = tempFiles;
CurrentItemIndex = tempCurIndex;
cancellationToken.ThrowIfCancellationRequested();
ItemsCount = (int)initialQueryData.Value.ItemsCount; // TODO: remove cast
CurrentItemIndex = (int)initialQueryData.Value.FirstItemIndex;
});
}
}

View File

@@ -13,7 +13,7 @@ namespace Peek.UI.Helpers
{
public static class FileExplorerHelper
{
public static Shell32.IShellFolderViewDual2? GetCurrentFolderView()
public static Shell32.IShellFolderViewDual3? GetCurrentFolderView()
{
var foregroundWindowHandle = NativeMethods.GetForegroundWindow();
@@ -26,7 +26,7 @@ namespace Peek.UI.Helpers
var shell = new Shell32.Shell();
foreach (SHDocVw.InternetExplorer window in shell.Windows())
{
var shellFolderView = (Shell32.IShellFolderViewDual2)window.Document;
var shellFolderView = (Shell32.IShellFolderViewDual3)window.Document;
var folderTitle = shellFolderView.Folder.Title;
if (window.HWND == (int)foregroundWindowHandle && folderTitle == foregroundWindowTitle)

View File

@@ -32,7 +32,7 @@
File="{x:Bind ViewModel.FolderItemsQuery.CurrentFile, Mode=OneWay}"
FileIndex="{x:Bind ViewModel.FolderItemsQuery.CurrentItemIndex, Mode=OneWay}"
IsMultiSelection="{x:Bind ViewModel.FolderItemsQuery.IsMultiSelection, Mode=OneWay}"
NumberOfFiles="{x:Bind ViewModel.FolderItemsQuery.Files.Count, Mode=OneWay}" />
NumberOfFiles="{x:Bind ViewModel.FolderItemsQuery.ItemsCount, Mode=OneWay}" />
<fp:FilePreview
Grid.Row="1"

View File

@@ -12,36 +12,39 @@ namespace Peek.UI
{
private const int NavigationThrottleDelayMs = 100;
private bool IsNavigating { get; set; } = false;
public MainWindowViewModel()
{
NavigationThrottleTimer.Tick += NavigationThrottleTimer_Tick;
NavigationThrottleTimer.Interval = TimeSpan.FromMilliseconds(NavigationThrottleDelayMs);
}
public void AttemptLeftNavigation()
private async void AttemptNavigation(bool goToNextItem)
{
if (NavigationThrottleTimer.IsEnabled)
if (NavigationThrottleTimer.IsEnabled && !IsNavigating)
{
return;
}
IsNavigating = true;
NavigationThrottleTimer.Start();
// TODO: return a bool so UI can give feedback in case navigation is unavailable
FolderItemsQuery.UpdateCurrentItemIndex(FolderItemsQuery.CurrentItemIndex - 1);
var desiredItemIndex = FolderItemsQuery.CurrentItemIndex + (goToNextItem ? 1 : -1);
// TODO: return a bool so UI can give feedback in case navigation is unavailable?
await FolderItemsQuery.UpdateCurrentItemIndex(desiredItemIndex);
IsNavigating = false;
}
public void AttemptLeftNavigation()
{
AttemptNavigation(false);
}
public void AttemptRightNavigation()
{
if (NavigationThrottleTimer.IsEnabled)
{
return;
}
NavigationThrottleTimer.Start();
// TODO: return a bool so UI can give feedback in case navigation is unavailable
FolderItemsQuery.UpdateCurrentItemIndex(FolderItemsQuery.CurrentItemIndex + 1);
AttemptNavigation(true);
}
private void NavigationThrottleTimer_Tick(object? sender, object e)