mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-01-16 17:26:22 +01:00
Compare commits
13 Commits
samchaps/w
...
estebanm12
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e71bbdf43 | ||
|
|
ea1cea7b7e | ||
|
|
5fa7205458 | ||
|
|
853086d243 | ||
|
|
ac17cadce3 | ||
|
|
195f14a3c7 | ||
|
|
f2a7a5ce1a | ||
|
|
e8ebf73033 | ||
|
|
5540aeb20c | ||
|
|
ab898a2911 | ||
|
|
32bd48e6de | ||
|
|
88c7435f93 | ||
|
|
89437f6749 |
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
namespace Peek.FilePreviewer.Previewers
|
||||
{
|
||||
using System.Threading;
|
||||
using Peek.Common.Models;
|
||||
|
||||
public class PreviewerFactory
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,17 +2,21 @@
|
||||
// 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.FileSystem
|
||||
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.FileSystem
|
||||
private File? currentFile;
|
||||
|
||||
[ObservableProperty]
|
||||
private List<File> files = new ();
|
||||
private int itemsCount = 0;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool isMultiSelection;
|
||||
@@ -34,52 +38,66 @@ namespace Peek.UI.FileSystem
|
||||
|
||||
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.FileSystem
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -12,7 +12,6 @@ namespace Peek.UI
|
||||
using Peek.UI.Extensions;
|
||||
using Peek.UI.Native;
|
||||
using Windows.Foundation;
|
||||
using Windows.Win32;
|
||||
using WinUIEx;
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -7,14 +7,12 @@ namespace Peek.UI
|
||||
using System;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Peek.UI.FileSystem;
|
||||
|
||||
public partial class MainWindowViewModel : ObservableObject
|
||||
{
|
||||
private const int NavigationThrottleDelayMs = 100;
|
||||
|
||||
[ObservableProperty]
|
||||
private FolderItemsQuery _folderItemsQuery = new ();
|
||||
private bool IsNavigating { get; set; } = false;
|
||||
|
||||
public MainWindowViewModel()
|
||||
{
|
||||
@@ -22,32 +20,31 @@ namespace Peek.UI
|
||||
NavigationThrottleTimer.Interval = TimeSpan.FromMilliseconds(NavigationThrottleDelayMs);
|
||||
}
|
||||
|
||||
private DispatcherTimer NavigationThrottleTimer { get; set; } = new ();
|
||||
|
||||
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)
|
||||
@@ -59,5 +56,10 @@ namespace Peek.UI
|
||||
|
||||
((DispatcherTimer)sender).Stop();
|
||||
}
|
||||
|
||||
[ObservableProperty]
|
||||
private FolderItemsQuery _folderItemsQuery = new ();
|
||||
|
||||
private DispatcherTimer NavigationThrottleTimer { get; set; } = new ();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace Peek.UI.Native
|
||||
{
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Text;
|
||||
using Peek.Common.Models;
|
||||
|
||||
@@ -20,6 +21,37 @@ namespace Peek.UI.Native
|
||||
internal const int WM_SYSCOMMAND = 0x0112;
|
||||
internal const int SC_RESTORE = 0xF120;
|
||||
|
||||
[Flags]
|
||||
public enum AssocF
|
||||
{
|
||||
None = 0,
|
||||
Init_NoRemapCLSID = 0x1,
|
||||
Init_ByExeName = 0x2,
|
||||
Open_ByExeName = 0x2,
|
||||
Init_DefaultToStar = 0x4,
|
||||
Init_DefaultToFolder = 0x8,
|
||||
NoUserSettings = 0x10,
|
||||
NoTruncate = 0x20,
|
||||
Verify = 0x40,
|
||||
RemapRunDll = 0x80,
|
||||
NoFixUps = 0x100,
|
||||
IgnoreBaseClass = 0x200,
|
||||
}
|
||||
|
||||
public enum AssocStr
|
||||
{
|
||||
Command = 1,
|
||||
Executable,
|
||||
FriendlyDocName,
|
||||
FriendlyAppName,
|
||||
NoOpen,
|
||||
ShellNewValue,
|
||||
DDECommand,
|
||||
DDEIfExec,
|
||||
DDEApplication,
|
||||
DDETopic,
|
||||
}
|
||||
|
||||
[DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
|
||||
internal static extern IntPtr GetForegroundWindow();
|
||||
|
||||
@@ -71,150 +103,4 @@ namespace Peek.UI.Native
|
||||
[DllImport("user32.dll")]
|
||||
internal static extern int GetWindowText(Windows.Win32.Foundation.HWND hWnd, StringBuilder lpString, int nMaxCount);
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum AssocF
|
||||
{
|
||||
None = 0,
|
||||
Init_NoRemapCLSID = 0x1,
|
||||
Init_ByExeName = 0x2,
|
||||
Open_ByExeName = 0x2,
|
||||
Init_DefaultToStar = 0x4,
|
||||
Init_DefaultToFolder = 0x8,
|
||||
NoUserSettings = 0x10,
|
||||
NoTruncate = 0x20,
|
||||
Verify = 0x40,
|
||||
RemapRunDll = 0x80,
|
||||
NoFixUps = 0x100,
|
||||
IgnoreBaseClass = 0x200,
|
||||
}
|
||||
|
||||
public enum AssocStr
|
||||
{
|
||||
Command = 1,
|
||||
Executable,
|
||||
FriendlyDocName,
|
||||
FriendlyAppName,
|
||||
NoOpen,
|
||||
ShellNewValue,
|
||||
DDECommand,
|
||||
DDEIfExec,
|
||||
DDEApplication,
|
||||
DDETopic,
|
||||
}
|
||||
|
||||
public enum AccessibleObjectID : uint
|
||||
{
|
||||
OBJID_WINDOW = 0x00000000,
|
||||
OBJID_SYSMENU = 0xFFFFFFFF,
|
||||
OBJID_TITLEBAR = 0xFFFFFFFE,
|
||||
OBJID_MENU = 0xFFFFFFFD,
|
||||
OBJID_CLIENT = 0xFFFFFFFC,
|
||||
OBJID_VSCROLL = 0xFFFFFFFB,
|
||||
OBJID_HSCROLL = 0xFFFFFFFA,
|
||||
OBJID_SIZEGRIP = 0xFFFFFFF9,
|
||||
OBJID_CARET = 0xFFFFFFF8,
|
||||
OBJID_CURSOR = 0xFFFFFFF7,
|
||||
OBJID_ALERT = 0xFFFFFFF6,
|
||||
OBJID_SOUND = 0xFFFFFFF5,
|
||||
}
|
||||
|
||||
public enum WindowEvent : uint
|
||||
{
|
||||
EVENT_MIN = 0x00000001,
|
||||
EVENT_SYSTEM_START = 0x0001,
|
||||
EVENT_SYSTEM_SOUND = 0x0001,
|
||||
EVENT_SYSTEM_ALERT = 0x0002,
|
||||
EVENT_SYSTEM_FOREGROUND = 0x0003,
|
||||
EVENT_SYSTEM_MENUSTART = 0x0004,
|
||||
EVENT_SYSTEM_MENUEND = 0x0005,
|
||||
EVENT_SYSTEM_MENUPOPUPSTART = 0x0006,
|
||||
EVENT_SYSTEM_MENUPOPUPEND = 0x0007,
|
||||
EVENT_SYSTEM_CAPTURESTART = 0x0008,
|
||||
EVENT_SYSTEM_CAPTUREEND = 0x0009,
|
||||
EVENT_SYSTEM_MOVESIZESTART = 0x000A,
|
||||
EVENT_SYSTEM_MOVESIZEEND = 0x000B,
|
||||
EVENT_SYSTEM_CONTEXTHELPSTART = 0x000C,
|
||||
EVENT_SYSTEM_CONTEXTHELPEND = 0x000D,
|
||||
EVENT_SYSTEM_DRAGDROPSTART = 0x000E,
|
||||
EVENT_SYSTEM_DRAGDROPEND = 0x000F,
|
||||
EVENT_SYSTEM_DIALOGSTART = 0x0010,
|
||||
EVENT_SYSTEM_DIALOGEND = 0x0011,
|
||||
EVENT_SYSTEM_SCROLLINGSTART = 0x0012,
|
||||
EVENT_SYSTEM_SCROLLINGEND = 0x0013,
|
||||
EVENT_SYSTEM_SWITCHSTART = 0x0014,
|
||||
EVENT_SYSTEM_SWITCHEND = 0x0015,
|
||||
EVENT_SYSTEM_MINIMIZESTART = 0x0016,
|
||||
EVENT_SYSTEM_MINIMIZEEND = 0x0017,
|
||||
EVENT_SYSTEM_DESKTOPSWITCH = 0x0020,
|
||||
EVENT_SYSTEM_END = 0x00FF,
|
||||
EVENT_OEM_DEFINED_START = 0x0101,
|
||||
EVENT_OEM_DEFINED_END = 0x01FF,
|
||||
EVENT_CONSOLE_START = 0x4001,
|
||||
EVENT_CONSOLE_CARET = 0x4001,
|
||||
EVENT_CONSOLE_UPDATE_REGION = 0x4002,
|
||||
EVENT_CONSOLE_UPDATE_SIMPLE = 0x4003,
|
||||
EVENT_CONSOLE_UPDATE_SCROLL = 0x4004,
|
||||
EVENT_CONSOLE_LAYOUT = 0x4005,
|
||||
EVENT_CONSOLE_START_APPLICATION = 0x4006,
|
||||
EVENT_CONSOLE_END_APPLICATION = 0x4007,
|
||||
EVENT_CONSOLE_END = 0x40FF,
|
||||
EVENT_UIA_EVENTID_START = 0x4E00,
|
||||
EVENT_UIA_EVENTID_END = 0x4EFF,
|
||||
EVENT_UIA_PROPID_START = 0x7500,
|
||||
EVENT_UIA_PROPID_END = 0x75FF,
|
||||
EVENT_OBJECT_START = 0x8000,
|
||||
EVENT_OBJECT_CREATE = 0x8000,
|
||||
EVENT_OBJECT_DESTROY = 0x8001,
|
||||
EVENT_OBJECT_SHOW = 0x8002,
|
||||
EVENT_OBJECT_HIDE = 0x8003,
|
||||
EVENT_OBJECT_REORDER = 0x8004,
|
||||
EVENT_OBJECT_FOCUS = 0x8005,
|
||||
EVENT_OBJECT_SELECTION = 0x8006,
|
||||
EVENT_OBJECT_SELECTIONADD = 0x8007,
|
||||
EVENT_OBJECT_SELECTIONREMOVE = 0x8008,
|
||||
EVENT_OBJECT_SELECTIONWITHIN = 0x8009,
|
||||
EVENT_OBJECT_STATECHANGE = 0x800A,
|
||||
EVENT_OBJECT_LOCATIONCHANGE = 0x800B,
|
||||
EVENT_OBJECT_NAMECHANGE = 0x800C,
|
||||
EVENT_OBJECT_DESCRIPTIONCHANGE = 0x800D,
|
||||
EVENT_OBJECT_VALUECHANGE = 0x800E,
|
||||
EVENT_OBJECT_PARENTCHANGE = 0x800F,
|
||||
EVENT_OBJECT_HELPCHANGE = 0x8010,
|
||||
EVENT_OBJECT_DEFACTIONCHANGE = 0x8011,
|
||||
EVENT_OBJECT_ACCELERATORCHANGE = 0x8012,
|
||||
EVENT_OBJECT_INVOKED = 0x8013,
|
||||
EVENT_OBJECT_TEXTSELECTIONCHANGED = 0x8014,
|
||||
EVENT_OBJECT_CONTENTSCROLLED = 0x8015,
|
||||
EVENT_SYSTEM_ARRANGMENTPREVIEW = 0x8016,
|
||||
EVENT_OBJECT_CLOAKED = 0x8017,
|
||||
EVENT_OBJECT_UNCLOAKED = 0x8018,
|
||||
EVENT_OBJECT_LIVEREGIONCHANGED = 0x8019,
|
||||
EVENT_OBJECT_HOSTEDOBJECTSINVALIDATED = 0x8020,
|
||||
EVENT_OBJECT_DRAGSTART = 0x8021,
|
||||
EVENT_OBJECT_DRAGCANCEL = 0x8022,
|
||||
EVENT_OBJECT_DRAGCOMPLETE = 0x8023,
|
||||
EVENT_OBJECT_DRAGENTER = 0x8024,
|
||||
EVENT_OBJECT_DRAGLEAVE = 0x8025,
|
||||
EVENT_OBJECT_DRAGDROPPED = 0x8026,
|
||||
EVENT_OBJECT_IME_SHOW = 0x8027,
|
||||
EVENT_OBJECT_IME_HIDE = 0x8028,
|
||||
EVENT_OBJECT_IME_CHANGE = 0x8029,
|
||||
EVENT_OBJECT_TEXTEDIT_CONVERSIONTARGETCHANGED = 0x8030,
|
||||
EVENT_OBJECT_END = 0x80FF,
|
||||
EVENT_ATOM_START = 0xC000,
|
||||
EVENT_AIA_START = 0xA000,
|
||||
EVENT_AIA_END = 0xAFFF,
|
||||
EVENT_ATOM_END = 0xFFFF,
|
||||
EVENT_MAX = 0x7FFFFFFF,
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum WinEventHookFlags : uint
|
||||
{
|
||||
WINEVENT_OUTOFCONTEXT = 0x0000,
|
||||
WINEVENT_SKIPOWNTHREAD = 0x0001,
|
||||
WINEVENT_SKIPOWNPROCESS = 0x0002,
|
||||
WINEVENT_INCONTEXT = 0x0004,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
MonitorFromWindow
|
||||
GetMonitorInfo
|
||||
GetDpiForWindow
|
||||
GetWindowTextLength
|
||||
SetWinEventHook
|
||||
UnhookWinEvent
|
||||
GetWindowTextLength
|
||||
@@ -1,56 +0,0 @@
|
||||
// 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.WindowEventHook
|
||||
{
|
||||
using System;
|
||||
using System.Reflection.Metadata;
|
||||
using System.Runtime.ConstrainedExecution;
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.Win32.SafeHandles;
|
||||
using Peek.UI.Native;
|
||||
using Windows.Win32;
|
||||
|
||||
public class WindowEventHook : IDisposable
|
||||
{
|
||||
public event EventHandler<WindowEventHookEventArgs>? WindowEventReceived;
|
||||
|
||||
public WindowEventHook()
|
||||
{
|
||||
var moveOrResizeEvent = WindowEvent.EVENT_SYSTEM_MOVESIZEEND;
|
||||
|
||||
var windowHookEventHandler = new WindowEventProc(OnWindowEventProc);
|
||||
|
||||
var hook = PInvoke.SetWinEventHook(
|
||||
(uint)moveOrResizeEvent,
|
||||
(uint)moveOrResizeEvent,
|
||||
new SafeHandle(),
|
||||
windowHookEventHandler,
|
||||
0,
|
||||
0,
|
||||
WinEventHookFlags.WINEVENT_OUTOFCONTEXT | WinEventHookFlags.WINEVENT_SKIPOWNPROCESS);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
private void OnWindowEventProc(nint hWinEventHook, WindowEvent eventType, nint hwnd, AccessibleObjectID idObject, int idChild, uint dwEventThread, uint dwmsEventTime)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
public record WindowEventHookEventArgs(WindowEvent eventType, IntPtr windowHandle);
|
||||
|
||||
public delegate void WindowEventProc(
|
||||
IntPtr hWinEventHook,
|
||||
WindowEvent eventType,
|
||||
IntPtr hwnd,
|
||||
AccessibleObjectID idObject,
|
||||
int idChild,
|
||||
uint dwEventThread,
|
||||
uint dwmsEventTime);
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
// 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.WindowEventHook
|
||||
{
|
||||
using System;
|
||||
using System.Runtime.ConstrainedExecution;
|
||||
using Microsoft.Win32.SafeHandles;
|
||||
using Peek.UI.Native;
|
||||
|
||||
public class WindowEventSafeHandle : SafeHandleZeroOrMinusOneIsInvalid
|
||||
{
|
||||
private WindowEventSafeHandle(IntPtr handle)
|
||||
: base(true)
|
||||
{
|
||||
SetHandle(handle);
|
||||
}
|
||||
|
||||
public WindowEventSafeHandle()
|
||||
: base(true)
|
||||
{
|
||||
SetHandle(handle);
|
||||
}
|
||||
|
||||
protected override bool ReleaseHandle()
|
||||
{
|
||||
NativeMethods.DeleteObject(this);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user