Compare commits

...

14 Commits

Author SHA1 Message Date
Samuel Chapleau
bf72adc942 wip 2022-12-08 22:12:32 -08:00
Jojo Zhou
60bf86825b [Peek] Add tooltip to File (#22640)
* Add tooltip to File

* Add placeholder text for no tooltip

* Address comments

* Use StringBuilder

Co-authored-by: Jojo Zhou <yizzho@microsoft.com>
2022-12-08 15:55:28 -08:00
sujessie
4ef3f23897 [Peek] Unsupported File Previewer - Setting Window Size (#22645)
* Adding setting for unsupported file window

* Fix
2022-12-08 15:44:42 -08:00
Robson
5bd9dd5935 [Peek] add extra logic to properly render PNG files with transparency (#22613)
* [Peek] added extra logic to render PNG files with proper transparency

* Moved logic to ThumbnailHelper
Cleanup

* Created a separated previewer for PNG to only load the preview image with thumbnail logic

* removed unused code

* Updated state loading change
2022-12-08 15:42:38 -08:00
Michael Salmon
5590eb9484 [Peek] New File Explorer tabs break Shell API to get selected files (#22641)
* fix FE tab bug

* remove unnecessary unsafe keyword
2022-12-08 15:39:14 -08:00
Jojo Zhou
6f06f76784 [Peek] Refine titlebar adaptive width (#22642)
* Adjust adaptive width of titlebar

* Remove visualstate setters for AppTitle_FileCount

Co-authored-by: Jojo Zhou <yizzho@microsoft.com>
2022-12-08 15:06:16 -08:00
Robson
5981d0e81e [Peek] add PDF viewing support (#22636)
* [Peek] add PDF viewing support

* Fixed issue which would redirect some HTML and PDF files to external browser

* Fixed refactored interface name
2022-12-08 13:41:02 -08:00
Michael Salmon
539a4e5678 [Peek] ImagePreviewer - Handle error states (#22637)
* add better preview state handling

* add error handling in imagepreviewer and better state handling

* fix error handling so exception is not bubbled up

* improve performance and hook up unsupported previewer on error

* remove commented code

* address pr comments
2022-12-08 11:25:29 -08:00
Daniel Chau
aea217ddca [Peek] Fix foregrounding (#22633)
* Fixing foregrounding

* Get window handle inside BringToForeground extension method

Co-authored-by: Daniel Chau <dancha@microsoft.com>
Co-authored-by: Samuel Chapleau <sachaple@microsoft.com>
2022-12-08 10:11:24 -08:00
sujessie
d001a4c0e0 [Peek] Unsupported File Previewer - Formatting string from resources (#22609)
* Moving to string resource usage

* Moving ReadableStringHelper to common project

* Fix comments
2022-12-08 09:49:00 -08:00
Samuel Chapleau
5712123598 Fix titlebar draggable region and interactive region (bump WinAppSdk to latest) 2022-12-07 22:57:45 -08:00
Samuel Chapleau
e504653323 Update titlebar filecount text 2022-12-07 20:47:35 -08:00
Esteban Margaron
b46b8d176f Make CurrentItemIndex setter private 2022-12-07 18:51:02 -08:00
Esteban Margaron
b98f233b75 Fix wrong thread exception 2022-12-07 18:45:11 -08:00
40 changed files with 1418 additions and 381 deletions

View File

@@ -4,7 +4,7 @@
namespace Peek.Common.Converters
{
public class BoolConverter
public static class BoolConverter
{
public static bool Invert(bool value)
{

View File

@@ -0,0 +1,37 @@
// 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.Common.Extensions
{
using System;
using System.Threading.Tasks;
using Microsoft.UI.Dispatching;
public static class DispatcherExtensions
{
/// <summary>
/// Run work on UI thread safely.
/// </summary>
/// <returns>True if the work was run successfully, False otherwise.</returns>
public static Task RunOnUiThread(this DispatcherQueue dispatcher, Func<Task> work)
{
var tcs = new TaskCompletionSource();
dispatcher.TryEnqueue(async () =>
{
try
{
await work();
tcs.SetResult();
}
catch (Exception e)
{
tcs.SetException(e);
}
});
return tcs.Task;
}
}
}

View File

@@ -0,0 +1,32 @@
// 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.Common.Extensions
{
using System;
using System.Threading.Tasks;
public static class TaskExtension
{
public static Task<bool> RunSafe(Func<Task> work)
{
var tcs = new TaskCompletionSource<bool>();
Task.Run(async () =>
{
try
{
await work();
tcs.SetResult(true);
}
catch (Exception)
{
tcs.SetResult(false);
}
});
return tcs.Task;
}
}
}

View File

@@ -0,0 +1,57 @@
// 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.Common.Helpers
{
using System;
using System.Collections.Generic;
using Windows.ApplicationModel.Resources;
public static class ReadableStringHelper
{
private const int DecimalPercision = 10;
public static string BytesToReadableString(ulong bytes)
{
var resourceLoader = ResourceLoader.GetForViewIndependentUse();
List<string> format = new List<string>
{
resourceLoader.GetString("ReadableString_ByteAbbreviationFormat"), // "B"
resourceLoader.GetString("ReadableString_KiloByteAbbreviationFormat"), // "KB"
resourceLoader.GetString("ReadableString_MegaByteAbbreviationFormat"), // "MB"
resourceLoader.GetString("ReadableString_GigaByteAbbreviationFormat"), // "GB"
resourceLoader.GetString("ReadableString_TeraByteAbbreviationFormat"), // "TB"
resourceLoader.GetString("ReadableString_PetaByteAbbreviationFormat"), // "PB"
resourceLoader.GetString("ReadableString_ExaByteAbbreviationFormat"), // "EB"
};
int index = 0;
double number = 0.0;
if (bytes > 0)
{
index = (int)Math.Floor(Math.Log(bytes) / Math.Log(1024));
number = Math.Round((bytes / Math.Pow(1024, index)) * DecimalPercision) / DecimalPercision;
}
return string.Format(format[index], number);
}
public static string FormatResourceString(string resourceId, object? args)
{
var formatString = ResourceLoader.GetForViewIndependentUse()?.GetString(resourceId);
var formattedString = string.IsNullOrEmpty(formatString) ? string.Empty : string.Format(formatString, args);
return formattedString;
}
public static string FormatResourceString(string resourceId, object? args0, object? args1)
{
var formatString = ResourceLoader.GetForViewIndependentUse()?.GetString(resourceId);
var formattedString = string.IsNullOrEmpty(formatString) ? string.Empty : string.Format(formatString, args0, args1);
return formattedString;
}
}
}

View File

@@ -10,7 +10,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.1.5" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.2.221116.1" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.22621.1" />
</ItemGroup>
</Project>

View File

@@ -12,7 +12,14 @@ namespace Peek.FilePreviewer.Controls
public sealed partial class BrowserControl : UserControl
{
public delegate void NavigationCompletedHandler(WebView2 sender, CoreWebView2NavigationCompletedEventArgs args);
/// <summary>
/// Helper private Uri where we cache the last navigated page
/// so we can redirect internal PDF or Webpage links to external
/// webbrowser, avoiding WebView internal navigation.
/// </summary>
private Uri? _navigatedUri;
public delegate void NavigationCompletedHandler(WebView2? sender, CoreWebView2NavigationCompletedEventArgs? args);
public event NavigationCompletedHandler? NavigationCompleted;
@@ -53,6 +60,7 @@ namespace Peek.FilePreviewer.Controls
public void Navigate()
{
IsNavigationCompleted = false;
_navigatedUri = null;
if (Source != null)
{
@@ -90,8 +98,13 @@ namespace Peek.FilePreviewer.Controls
private async void PreviewBrowser_NavigationStarting(WebView2 sender, Microsoft.Web.WebView2.Core.CoreWebView2NavigationStartingEventArgs args)
{
if (_navigatedUri == null)
{
return;
}
// In case user starts or tries to navigate from within the HTML file we launch default web browser for navigation.
if (args.Uri != null && args.Uri != Source?.ToString() && args.IsUserInitiated)
if (args.Uri != null && args.Uri != _navigatedUri?.ToString() && args.IsUserInitiated)
{
args.Cancel = true;
await Launcher.LaunchUriAsync(new Uri(args.Uri));
@@ -100,7 +113,12 @@ namespace Peek.FilePreviewer.Controls
private void PreviewWV2_NavigationCompleted(WebView2 sender, Microsoft.Web.WebView2.Core.CoreWebView2NavigationCompletedEventArgs args)
{
IsNavigationCompleted = true;
if (args.IsSuccess)
{
IsNavigationCompleted = true;
_navigatedUri = Source;
}
NavigationCompleted?.Invoke(sender, args);
}

View File

@@ -5,38 +5,40 @@
x:Class="Peek.FilePreviewer.FilePreview"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:conv="using:Peek.Common.Converters"
xmlns:controls="using:Peek.FilePreviewer.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:Peek.FilePreviewer"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:controls="using:Peek.FilePreviewer.Controls"
xmlns:previewers="using:Peek.FilePreviewer.Previewers"
mc:Ignorable="d">
<Grid>
<ProgressRing
HorizontalAlignment="Center"
VerticalAlignment="Center"
IsActive="{x:Bind conv:BoolConverter.Invert(Previewer.IsPreviewLoaded), Mode=OneWay}" />
IsActive="{x:Bind MatchPreviewState(Previewer.State, previewers:PreviewState.Loading), Mode=OneWay}" />
<Image
x:Name="PreviewImage"
x:Name="ImagePreview"
Source="{x:Bind BitmapPreviewer.Preview, Mode=OneWay}"
Visibility="{x:Bind IsImageVisible, Mode=OneWay}" />
ToolTipService.ToolTip="{x:Bind ImageInfoTooltip, Mode=OneWay}"
Visibility="{x:Bind IsPreviewVisible(BitmapPreviewer, Previewer.State), Mode=OneWay}" />
<controls:BrowserControl x:Name="PreviewBrowser"
x:Load="True"
Source="{x:Bind BrowserPreviewer.Preview, Mode=OneWay}"
IsNavigationCompleted="{x:Bind BrowserPreviewer.IsPreviewLoaded, Mode=TwoWay}"
Visibility="{x:Bind IsBrowserVisible, Mode=OneWay, FallbackValue=Collapsed}"
NavigationCompleted="PreviewBrowser_NavigationCompleted"/>
<controls:BrowserControl
x:Name="BrowserPreview"
x:Load="True"
NavigationCompleted="PreviewBrowser_NavigationCompleted"
Source="{x:Bind BrowserPreviewer.Preview, Mode=OneWay}"
Visibility="{x:Bind IsPreviewVisible(BrowserPreviewer, Previewer.State), Mode=OneWay, FallbackValue=Collapsed}" />
<local:UnsupportedFilePreview
x:Name="UnsupportedFilePreview"
DateModified="{x:Bind UnsupportedFilePreviewer.DateModified, Mode=OneWay}"
FileName="{x:Bind UnsupportedFilePreviewer.FileName, Mode=OneWay}"
FileSize="{x:Bind UnsupportedFilePreviewer.FileSize, Mode=OneWay}"
FileType="{x:Bind UnsupportedFilePreviewer.FileType, Mode=OneWay}"
IconPreview="{x:Bind UnsupportedFilePreviewer.IconPreview, Mode=OneWay}"
Visibility="{x:Bind IsUnsupportedPreviewVisible, Mode=OneWay}" />
Visibility="{x:Bind IsPreviewVisible(UnsupportedFilePreviewer, Previewer.State), Mode=OneWay}" />
</Grid>
</UserControl>

View File

@@ -5,14 +5,17 @@
namespace Peek.FilePreviewer
{
using System;
using System.Text;
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;
using Peek.FilePreviewer.Previewers;
using Windows.ApplicationModel.Resources;
using Windows.Foundation;
[INotifyPropertyChanged]
@@ -31,21 +34,35 @@ namespace Peek.FilePreviewer
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(BitmapPreviewer))]
[NotifyPropertyChangedFor(nameof(IsImageVisible))]
[NotifyPropertyChangedFor(nameof(UnsupportedFilePreviewer))]
[NotifyPropertyChangedFor(nameof(IsUnsupportedPreviewVisible))]
[NotifyPropertyChangedFor(nameof(BrowserPreviewer))]
[NotifyPropertyChangedFor(nameof(IsBrowserVisible))]
[NotifyPropertyChangedFor(nameof(UnsupportedFilePreviewer))]
private IPreviewer? previewer;
[ObservableProperty]
private string imageInfoTooltip = ResourceLoader.GetForViewIndependentUse().GetString("PreviewTooltip_Blank");
public FilePreview()
{
InitializeComponent();
}
private async void Previewer_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{
// Fallback on DefaultPreviewer if we fail to load the correct Preview
if (e.PropertyName == nameof(IPreviewer.State))
{
if (Previewer?.State == PreviewState.Error)
{
Previewer = previewerFactory.CreateDefaultPreviewer(File);
await UpdatePreviewAsync();
}
}
}
public IBitmapPreviewer? BitmapPreviewer => Previewer as IBitmapPreviewer;
public IBrowserPreview? BrowserPreviewer => Previewer as IBrowserPreview;
public IBrowserPreviewer? BrowserPreviewer => Previewer as IBrowserPreviewer;
public bool IsImageVisible => BitmapPreviewer != null;
@@ -53,49 +70,106 @@ namespace Peek.FilePreviewer
public bool IsUnsupportedPreviewVisible => UnsupportedFilePreviewer != null;
/* TODO: need a better way to switch visibility according to the Preview.
* Could use Enum + Converter to switch according to the current preview. */
public bool IsBrowserVisible
{
get
{
if (BrowserPreviewer != null)
{
return BrowserPreviewer.IsPreviewLoaded;
}
return false;
}
}
public File File
{
get => (File)GetValue(FilesProperty);
set => SetValue(FilesProperty, value);
}
public bool MatchPreviewState(PreviewState? value, PreviewState stateToMatch)
{
return value == stateToMatch;
}
public Visibility IsPreviewVisible(IPreviewer? previewer, PreviewState? state)
{
var isValidPreview = previewer != null && MatchPreviewState(state, PreviewState.Loaded);
return isValidPreview ? Visibility.Visible : Visibility.Collapsed;
}
private async Task OnFilePropertyChanged()
{
// TODO: track and cancel existing async preview tasks
// https://github.com/microsoft/PowerToys/issues/22480
if (File == null)
{
Previewer = null;
ImagePreview.Visibility = Visibility.Collapsed;
BrowserPreview.Visibility = Visibility.Collapsed;
UnsupportedFilePreview.Visibility = Visibility.Collapsed;
return;
}
Previewer = previewerFactory.Create(File);
await UpdatePreviewAsync();
}
private async Task UpdatePreviewAsync()
{
if (Previewer != null)
{
var size = await Previewer.GetPreviewSizeAsync();
PreviewSizeChanged?.Invoke(this, new PreviewSizeChangedArgs(size));
SizeFormat windowSizeFormat = UnsupportedFilePreviewer != null ? SizeFormat.Percentage : SizeFormat.Pixels;
PreviewSizeChanged?.Invoke(this, new PreviewSizeChangedArgs(size, windowSizeFormat));
await Previewer.LoadPreviewAsync();
}
await UpdateImageTooltipAsync();
}
partial void OnPreviewerChanging(IPreviewer? value)
{
if (Previewer != null)
{
Previewer.PropertyChanged -= Previewer_PropertyChanged;
}
if (value != null)
{
value.PropertyChanged += Previewer_PropertyChanged;
}
}
private void PreviewBrowser_NavigationCompleted(WebView2 sender, Microsoft.Web.WebView2.Core.CoreWebView2NavigationCompletedEventArgs args)
{
// Once browser has completed navigation it is ready to be visible
OnPropertyChanged(nameof(IsBrowserVisible));
if (BrowserPreviewer != null)
{
BrowserPreviewer.State = PreviewState.Loaded;
}
}
private async Task UpdateImageTooltipAsync()
{
if (File == null)
{
return;
}
// Fetch and format available file properties
var sb = new StringBuilder();
string fileNameFormatted = ReadableStringHelper.FormatResourceString("PreviewTooltip_FileName", File.FileName);
sb.Append(fileNameFormatted);
string fileType = await PropertyHelper.GetFileType(File.Path);
string fileTypeFormatted = string.IsNullOrEmpty(fileType) ? string.Empty : "\n" + ReadableStringHelper.FormatResourceString("PreviewTooltip_FileType", fileType);
sb.Append(fileTypeFormatted);
string dateModified = File.DateModified.ToString();
string dateModifiedFormatted = string.IsNullOrEmpty(dateModified) ? string.Empty : "\n" + ReadableStringHelper.FormatResourceString("PreviewTooltip_DateModified", dateModified);
sb.Append(dateModifiedFormatted);
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);
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);
sb.Append(fileSizeFormatted);
ImageInfoTooltip = sb.ToString();
}
}
}

View File

@@ -6,13 +6,22 @@ namespace Peek.FilePreviewer.Models
{
using Windows.Foundation;
public enum SizeFormat
{
Pixels,
Percentage,
}
public class PreviewSizeChangedArgs
{
public PreviewSizeChangedArgs(Size windowSizeRequested)
public PreviewSizeChangedArgs(Size windowSizeRequested, SizeFormat sizeFormat = SizeFormat.Pixels)
{
WindowSizeRequested = windowSizeRequested;
WindowSizeFormat = sizeFormat;
}
public Size WindowSizeRequested { get; init; }
public SizeFormat WindowSizeFormat { get; init; }
}
}

View File

@@ -16,12 +16,13 @@
<ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.0.0" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.1.5" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.2.221116.1" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.22621.1" />
<PackageReference Include="System.Drawing.Common" Version="7.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\settings-ui\Settings.UI.Library\Settings.UI.Library.csproj" />
<ProjectReference Include="..\Peek.Common\Peek.Common.csproj" />
<ProjectReference Include="..\WIC\WIC.csproj" />
</ItemGroup>

View File

@@ -2,7 +2,7 @@
// 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.FilePreviewer.Previewers
namespace Peek.FilePreviewer.Previewers.Helpers
{
using System;
using System.Drawing;

View File

@@ -1,40 +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.FilePreviewer.Previewers
{
using System;
using System.Collections.Generic;
public static class ReadableStringHelper
{
private const int DecimalPercision = 10;
public static string BytesToReadableString(ulong bytes)
{
// TODO: get string from resources
List<string> format = new List<string>
{
"B",
"KB",
"MB",
"GB",
"TB",
"PB",
"EB",
};
int index = 0;
double number = 0.0;
if (bytes > 0)
{
index = (int)Math.Floor(Math.Log(bytes) / Math.Log(1024));
number = Math.Round((bytes / Math.Pow(1024, index)) * DecimalPercision) / DecimalPercision;
}
return string.Concat(number, format[index]);
}
}
}

View File

@@ -6,10 +6,8 @@ namespace Peek.FilePreviewer.Previewers
{
using System;
public interface IBrowserPreview : IPreviewer
public interface IBrowserPreviewer : IPreviewer
{
public Uri? Preview { get; }
public new bool IsPreviewLoaded { get; set; }
}
}

View File

@@ -11,7 +11,7 @@ namespace Peek.FilePreviewer.Previewers
public interface IPreviewer : INotifyPropertyChanged
{
bool IsPreviewLoaded { get; }
PreviewState State { get; set; }
public static bool IsFileTypeSupported(string fileExt) => throw new NotImplementedException();
@@ -19,4 +19,12 @@ namespace Peek.FilePreviewer.Previewers
Task LoadPreviewAsync();
}
public enum PreviewState
{
Uninitialized,
Loading,
Loaded,
Error,
}
}

View File

@@ -7,6 +7,8 @@ namespace Peek.FilePreviewer.Previewers
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Microsoft.UI.Xaml.Media.Imaging;
using Peek.Common;
using Peek.Common.Models;
@@ -59,5 +61,32 @@ namespace Peek.FilePreviewer.Previewers
return hr;
}
public static async Task<BitmapImage?> GetThumbnailAsync(string path, uint size)
{
BitmapImage? bitmapImage = null;
// preview image
var file = await Windows.Storage.StorageFile.GetFileFromPathAsync(path);
if (file == null)
{
return bitmapImage;
}
var imageStream = await file.GetThumbnailAsync(
Windows.Storage.FileProperties.ThumbnailMode.SingleItem,
size,
Windows.Storage.FileProperties.ThumbnailOptions.None);
if (imageStream == null)
{
return bitmapImage;
}
bitmapImage = new BitmapImage();
bitmapImage.SetSource(imageStream);
return bitmapImage;
}
}
}

View File

@@ -13,33 +13,41 @@ namespace Peek.FilePreviewer.Previewers
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media.Imaging;
using Peek.Common;
using Peek.Common.Extensions;
using Windows.Foundation;
using File = Peek.Common.Models.File;
public partial class ImagePreviewer : ObservableObject, IBitmapPreviewer, IDisposable
{
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsPreviewLoaded))]
private BitmapSource? preview;
[ObservableProperty]
private PreviewState state;
public ImagePreviewer(File file)
{
File = file;
Dispatcher = DispatcherQueue.GetForCurrentThread();
}
public bool IsPreviewLoaded => preview != null;
PropertyChanged += OnPropertyChanged;
}
private File File { get; }
private DispatcherQueue Dispatcher { get; }
private bool IsHighQualityThumbnailLoaded { get; set; }
private Task<bool>? LowQualityThumbnailTask { get; set; }
private bool IsFullImageLoaded { get; set; }
private Task<bool>? HighQualityThumbnailTask { get; set; }
private Task<bool>? FullQualityImageTask { get; set; }
private bool IsHighQualityThumbnailLoaded => HighQualityThumbnailTask?.Status == TaskStatus.RanToCompletion;
private bool IsFullImageLoaded => FullQualityImageTask?.Status == TaskStatus.RanToCompletion;
private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
@@ -62,19 +70,36 @@ namespace Peek.FilePreviewer.Previewers
return await WICHelper.GetImageSize(File.Path);
}
public Task LoadPreviewAsync()
public async Task LoadPreviewAsync()
{
var lowQualityThumbnailTask = LoadLowQualityThumbnailAsync();
var highQualityThumbnailTask = LoadHighQualityThumbnailAsync();
var fullImageTask = LoadFullQualityImageAsync();
State = PreviewState.Loading;
return Task.WhenAll(lowQualityThumbnailTask, highQualityThumbnailTask, fullImageTask);
LowQualityThumbnailTask = LoadLowQualityThumbnailAsync();
HighQualityThumbnailTask = LoadHighQualityThumbnailAsync();
FullQualityImageTask = LoadFullQualityImageAsync();
await Task.WhenAll(LowQualityThumbnailTask, HighQualityThumbnailTask, FullQualityImageTask);
if (Preview == null && HasFailedLoadingPreview())
{
State = PreviewState.Error;
}
}
private Task LoadLowQualityThumbnailAsync()
private void OnPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{
var thumbnailTCS = new TaskCompletionSource();
Dispatcher.TryEnqueue(async () =>
if (e.PropertyName == nameof(Preview))
{
if (Preview != null)
{
State = PreviewState.Loaded;
}
}
}
private Task<bool> LoadLowQualityThumbnailAsync()
{
return TaskExtension.RunSafe(async () =>
{
if (CancellationToken.IsCancellationRequested)
{
@@ -85,28 +110,25 @@ namespace Peek.FilePreviewer.Previewers
if (!IsFullImageLoaded && !IsHighQualityThumbnailLoaded)
{
var hr = ThumbnailHelper.GetThumbnail(Path.GetFullPath(File.Path), out IntPtr hbitmap, ThumbnailHelper.LowQualityThumbnailSize);
if (hr == Common.Models.HResult.Ok)
if (hr != Common.Models.HResult.Ok)
{
Debug.WriteLine("Error loading low quality thumbnail - hresult: " + hr);
throw new ArgumentNullException(nameof(hbitmap));
}
await Dispatcher.RunOnUiThread(async () =>
{
var thumbnailBitmap = await GetBitmapFromHBitmapAsync(hbitmap);
Preview = thumbnailBitmap;
}
else
{
// TODO: handle thumbnail errors
Debug.WriteLine("Error loading thumbnail - hresult: " + hr);
}
});
}
thumbnailTCS.SetResult();
});
return thumbnailTCS.Task;
}
private Task LoadHighQualityThumbnailAsync()
private Task<bool> LoadHighQualityThumbnailAsync()
{
var thumbnailTCS = new TaskCompletionSource();
Dispatcher.TryEnqueue(async () =>
return TaskExtension.RunSafe(async () =>
{
if (CancellationToken.IsCancellationRequested)
{
@@ -117,45 +139,48 @@ namespace Peek.FilePreviewer.Previewers
if (!IsFullImageLoaded)
{
var hr = ThumbnailHelper.GetThumbnail(Path.GetFullPath(File.Path), out IntPtr hbitmap, ThumbnailHelper.HighQualityThumbnailSize);
if (hr == Common.Models.HResult.Ok)
if (hr != Common.Models.HResult.Ok)
{
Debug.WriteLine("Error loading high quality thumbnail - hresult: " + hr);
throw new ArgumentNullException(nameof(hbitmap));
}
await Dispatcher.RunOnUiThread(async () =>
{
var thumbnailBitmap = await GetBitmapFromHBitmapAsync(hbitmap);
IsHighQualityThumbnailLoaded = true;
Preview = thumbnailBitmap;
}
else
{
// TODO: handle thumbnail errors
Debug.WriteLine("Error loading thumbnail - hresult: " + hr);
}
});
}
thumbnailTCS.SetResult();
});
return thumbnailTCS.Task;
}
private Task LoadFullQualityImageAsync()
private Task<bool> LoadFullQualityImageAsync()
{
var fullImageTCS = new TaskCompletionSource();
Dispatcher.TryEnqueue(async () =>
return TaskExtension.RunSafe(async () =>
{
// TODO: Check if this is performant
var bitmap = await GetFullBitmapFromPathAsync(File.Path);
IsFullImageLoaded = true;
if (CancellationToken.IsCancellationRequested)
{
_cancellationTokenSource = new CancellationTokenSource();
return;
}
Preview = bitmap;
fullImageTCS.SetResult();
// TODO: Check if this is performant
await Dispatcher.RunOnUiThread(async () =>
{
var bitmap = await GetFullBitmapFromPathAsync(File.Path);
Preview = bitmap;
});
});
}
return fullImageTCS.Task;
private bool HasFailedLoadingPreview()
{
var hasFailedLoadingLowQualityThumbnail = !(LowQualityThumbnailTask?.Result ?? true);
var hasFailedLoadingHighQualityThumbnail = !(HighQualityThumbnailTask?.Result ?? true);
var hasFailedLoadingFullQualityImage = !(FullQualityImageTask?.Result ?? true);
return hasFailedLoadingLowQualityThumbnail && hasFailedLoadingHighQualityThumbnail && hasFailedLoadingFullQualityImage;
}
private static async Task<BitmapImage> GetFullBitmapFromPathAsync(string path)
@@ -207,12 +232,13 @@ namespace Peek.FilePreviewer.Previewers
".jif",
".jpeg",
".jpe",
".png",
// ".png", // The current ImagePreviewer logic does not support transparency so PNG has it's own logic in PngPreviewer
".tif",
".tiff",
".dib",
// ".heic", // Error in System.Drawing.Image.FromHbitmap(hbitmap);
".heic", // Error in System.Drawing.Image.FromHbitmap(hbitmap);
".heif",
".hif",
".avif",

View File

@@ -0,0 +1,117 @@
// 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.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 Windows.Foundation;
using Windows.Graphics.Imaging;
using Windows.Storage.Streams;
using File = Peek.Common.Models.File;
public partial class PngPreviewer : ObservableObject, IBitmapPreviewer
{
private readonly uint _png_image_size = 1280;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsPreviewLoaded))]
private BitmapSource? preview;
[ObservableProperty]
private PreviewState state;
public PngPreviewer(File file)
{
File = file;
Dispatcher = DispatcherQueue.GetForCurrentThread();
PropertyChanged += OnPropertyChanged;
}
public bool IsPreviewLoaded => preview != null;
private File File { get; }
private DispatcherQueue Dispatcher { get; }
private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
private CancellationToken CancellationToken => _cancellationTokenSource.Token;
public void Dispose()
{
_cancellationTokenSource.Dispose();
GC.SuppressFinalize(this);
}
public async Task<Size> GetPreviewSizeAsync()
{
var propertyImageSize = await PropertyHelper.GetImageSize(File.Path);
if (propertyImageSize != Size.Empty)
{
return propertyImageSize;
}
return await WICHelper.GetImageSize(File.Path);
}
public async Task LoadPreviewAsync()
{
State = PreviewState.Loading;
var previewTask = LoadPreviewImageAsync();
await Task.WhenAll(previewTask);
if (Preview == null)
{
State = PreviewState.Error;
}
}
public static bool IsFileTypeSupported(string fileExt)
{
return fileExt == ".png" ? true : false;
}
private void OnPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(Preview))
{
if (Preview != null)
{
State = PreviewState.Loaded;
}
}
}
private Task LoadPreviewImageAsync()
{
var thumbnailTCS = new TaskCompletionSource();
Dispatcher.TryEnqueue(async () =>
{
if (CancellationToken.IsCancellationRequested)
{
_cancellationTokenSource = new CancellationTokenSource();
return;
}
Preview = await ThumbnailHelper.GetThumbnailAsync(File.Path, _png_image_size);
thumbnailTCS.SetResult();
});
return thumbnailTCS.Task;
}
}
}

View File

@@ -8,18 +8,27 @@ namespace Peek.FilePreviewer.Previewers
public class PreviewerFactory
{
public IPreviewer? Create(File file)
public IPreviewer Create(File file)
{
if (ImagePreviewer.IsFileTypeSupported(file.Extension))
if (PngPreviewer.IsFileTypeSupported(file.Extension))
{
return new PngPreviewer(file);
}
else if (ImagePreviewer.IsFileTypeSupported(file.Extension))
{
return new ImagePreviewer(file);
}
else if (HtmlPreviewer.IsFileTypeSupported(file.Extension))
else if (WebBrowserPreviewer.IsFileTypeSupported(file.Extension))
{
return new HtmlPreviewer(file);
return new WebBrowserPreviewer(file);
}
// Other previewer types check their supported file types here
return CreateDefaultPreviewer(file);
}
public IPreviewer CreateDefaultPreviewer(File file)
{
return new UnsupportedFilePreviewer(file);
}
}

View File

@@ -10,16 +10,21 @@ namespace Peek.FilePreviewer.Previewers
using System.Threading;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media.Imaging;
using Peek.Common;
using Peek.Common.Extensions;
using Peek.Common.Helpers;
using Peek.FilePreviewer.Previewers.Helpers;
using Windows.Foundation;
using File = Peek.Common.Models.File;
public partial class UnsupportedFilePreviewer : ObservableObject, IUnsupportedFilePreviewer, IDisposable
{
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsPreviewLoaded))]
private BitmapSource? iconPreview;
[ObservableProperty]
@@ -34,14 +39,31 @@ namespace Peek.FilePreviewer.Previewers
[ObservableProperty]
private string? dateModified;
[ObservableProperty]
private PreviewState state;
public UnsupportedFilePreviewer(File file)
{
File = file;
FileName = file.FileName;
DateModified = file.DateModified.ToString();
Dispatcher = DispatcherQueue.GetForCurrentThread();
PropertyChanged += OnPropertyChanged;
var settingsUtils = new SettingsUtils();
var settings = settingsUtils.GetSettingsOrDefault<PeekSettings>(PeekSettings.ModuleName);
if (settings != null)
{
UnsupportedFileWidthPercent = settings.Properties.UnsupportedFileWidthPercent / 100.0;
UnsupportedFileHeightPercent = settings.Properties.UnsupportedFileHeightPercent / 100.0;
}
}
private double UnsupportedFileWidthPercent { get; set; }
private double UnsupportedFileHeightPercent { get; set; }
public bool IsPreviewLoaded => iconPreview != null;
private File File { get; }
@@ -52,6 +74,10 @@ namespace Peek.FilePreviewer.Previewers
private CancellationToken CancellationToken => _cancellationTokenSource.Token;
private Task<bool>? IconPreviewTask { get; set; }
private Task<bool>? DisplayInfoTask { get; set; }
public void Dispose()
{
_cancellationTokenSource.Dispose();
@@ -62,23 +88,28 @@ namespace Peek.FilePreviewer.Previewers
{
return Task.Run(() =>
{
// TODO: This is the min size. Calculate a 20-25% of the screen.
return new Size(680, 500);
return new Size(UnsupportedFileWidthPercent, UnsupportedFileHeightPercent);
});
}
public Task LoadPreviewAsync()
public async Task LoadPreviewAsync()
{
var iconPreviewTask = LoadIconPreviewAsync();
var displayInfoTask = LoadDisplayInfoAsync();
State = PreviewState.Loading;
return Task.WhenAll(iconPreviewTask, displayInfoTask);
IconPreviewTask = LoadIconPreviewAsync();
DisplayInfoTask = LoadDisplayInfoAsync();
await Task.WhenAll(IconPreviewTask, DisplayInfoTask);
if (HasFailedLoadingPreview())
{
State = PreviewState.Error;
}
}
public Task LoadIconPreviewAsync()
public Task<bool> LoadIconPreviewAsync()
{
var iconTCS = new TaskCompletionSource();
Dispatcher.TryEnqueue(async () =>
return TaskExtension.RunSafe(async () =>
{
if (CancellationToken.IsCancellationRequested)
{
@@ -88,19 +119,17 @@ namespace Peek.FilePreviewer.Previewers
// TODO: Get icon with transparency
IconHelper.GetIcon(Path.GetFullPath(File.Path), out IntPtr hbitmap);
var iconBitmap = await GetBitmapFromHBitmapAsync(hbitmap);
IconPreview = iconBitmap;
iconTCS.SetResult();
await Dispatcher.RunOnUiThread(async () =>
{
var iconBitmap = await GetBitmapFromHBitmapAsync(hbitmap);
IconPreview = iconBitmap;
});
});
return iconTCS.Task;
}
public Task LoadDisplayInfoAsync()
public Task<bool> LoadDisplayInfoAsync()
{
var displayInfoTCS = new TaskCompletionSource();
Dispatcher.TryEnqueue(async () =>
return TaskExtension.RunSafe(async () =>
{
if (CancellationToken.IsCancellationRequested)
{
@@ -110,15 +139,34 @@ namespace Peek.FilePreviewer.Previewers
// File Properties
var bytes = await PropertyHelper.GetFileSizeInBytes(File.Path);
FileSize = ReadableStringHelper.BytesToReadableString(bytes);
var type = await PropertyHelper.GetFileType(File.Path);
FileType = type;
displayInfoTCS.SetResult();
await Dispatcher.RunOnUiThread(() =>
{
FileSize = ReadableStringHelper.BytesToReadableString(bytes);
FileType = type;
return Task.CompletedTask;
});
});
}
return displayInfoTCS.Task;
private void OnPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(IconPreview))
{
if (IconPreview != null)
{
State = PreviewState.Loaded;
}
}
}
private bool HasFailedLoadingPreview()
{
var hasFailedLoadingIconPreview = !(IconPreviewTask?.Result ?? true);
var hasFailedLoadingDisplayInfo = !(DisplayInfoTask?.Result ?? true);
return hasFailedLoadingIconPreview && hasFailedLoadingDisplayInfo;
}
// TODO: Move this to a helper file (ImagePrevier uses the same code)

View File

@@ -14,21 +14,25 @@ namespace Peek.FilePreviewer.Previewers
using Windows.Foundation;
using File = Peek.Common.Models.File;
public partial class HtmlPreviewer : ObservableObject, IBrowserPreview
public partial class WebBrowserPreviewer : ObservableObject, IBrowserPreviewer
{
private static readonly HashSet<string> _supportedFileTypes = new HashSet<string>
{
// Web
".html",
".htm",
// Document
".pdf",
};
[ObservableProperty]
private Uri? preview;
[ObservableProperty]
private bool isPreviewLoaded;
private PreviewState state;
public HtmlPreviewer(File file)
public WebBrowserPreviewer(File file)
{
File = file;
}
@@ -44,6 +48,8 @@ namespace Peek.FilePreviewer.Previewers
public Task LoadPreviewAsync()
{
State = PreviewState.Loading;
Preview = new Uri(File.Path);
return Task.CompletedTask;

View File

@@ -35,22 +35,9 @@
FontSize="26"
FontWeight="SemiBold"
Text="{x:Bind FileName, Mode=OneWay}" />
<!-- TODO: move strings to resw -->
<StackPanel Orientation="Horizontal">
<TextBlock Text="File Type: " />
<TextBlock Text="{x:Bind FileType, Mode=OneWay}" />
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Text="Size: " />
<TextBlock Text="{x:Bind FileSize, Mode=OneWay}" />
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Text="Date Modified: " />
<TextBlock Text="{x:Bind DateModified, Mode=OneWay}" />
</StackPanel>
<TextBlock Text="{x:Bind FormattedFileType, Mode=OneWay}" />
<TextBlock Text="{x:Bind FormattedFileSize, Mode=OneWay}" />
<TextBlock Text="{x:Bind FormattedDateModified, Mode=OneWay}" />
</StackPanel>
</Grid>
</UserControl>

View File

@@ -7,6 +7,7 @@ namespace Peek.FilePreviewer
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media.Imaging;
using Peek.Common.Helpers;
[INotifyPropertyChanged]
public sealed partial class UnsupportedFilePreview : UserControl
@@ -18,14 +19,23 @@ namespace Peek.FilePreviewer
private string? fileName;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(FormattedFileType))]
private string? fileType;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(FormattedFileSize))]
private string? fileSize;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(FormattedDateModified))]
private string? dateModified;
public string FormattedFileType => ReadableStringHelper.FormatResourceString("UnsupportedFile_FileType", FileType);
public string FormattedFileSize => ReadableStringHelper.FormatResourceString("UnsupportedFile_FileSize", FileSize);
public string FormattedDateModified => ReadableStringHelper.FormatResourceString("UnsupportedFile_DateModified", DateModified);
public UnsupportedFilePreview()
{
this.InitializeComponent();

View File

@@ -4,7 +4,9 @@
namespace Peek.UI.Extensions
{
using System;
using Microsoft.UI.Xaml;
using Peek.UI.Native;
using Windows.Foundation;
using Windows.Win32;
using Windows.Win32.Foundation;
@@ -34,5 +36,101 @@ namespace Peek.UI.Extensions
return scalingFactor;
}
public static void BringToForeground(this Window window)
{
var windowHandle = window.GetWindowHandle();
// Restore the window.
_ = NativeMethods.SendMessage(windowHandle, NativeMethods.WM_SYSCOMMAND, NativeMethods.SC_RESTORE, -2);
// Bring the window to the front.
if (!NativeMethods.SetWindowPos(
windowHandle,
NativeMethods.HWND_TOP,
0,
0,
0,
0,
NativeMethods.SWP_NOMOVE | NativeMethods.SWP_DRAWFRAME | NativeMethods.SWP_NOSIZE | NativeMethods.SWP_SHOWWINDOW))
{
throw new InvalidOperationException("Failed to set window position.");
}
// Grab the SetForegroundWindow privilege from the shell process.
AcquireForegroundPrivilege();
// Make our window the foreground window.
_ = NativeMethods.SetForegroundWindow(windowHandle);
}
private static void AcquireForegroundPrivilege()
{
IntPtr remoteProcessHandle = 0;
IntPtr user32Handle = 0;
IntPtr remoteThreadHandle = 0;
try
{
// Get the handle of the shell window.
IntPtr topHandle = NativeMethods.GetShellWindow();
if (topHandle == 0)
{
throw new InvalidOperationException("Failed to get the shell desktop window.");
}
// Open the process that owns it.
IntPtr remoteProcessId = 0;
NativeMethods.GetWindowThreadProcessId(topHandle, ref remoteProcessId);
if (remoteProcessId == 0)
{
throw new InvalidOperationException("Failed to get the shell process ID.");
}
remoteProcessHandle = NativeMethods.OpenProcess(NativeMethods.PROCESS_ALL_ACCESS, false, remoteProcessId);
if (remoteProcessHandle == 0)
{
throw new InvalidOperationException("Failed to open the shell process.");
}
// Get the address of the AllowSetForegroundWindow API.
user32Handle = NativeMethods.LoadLibrary("user32.dll");
IntPtr entryPoint = NativeMethods.GetProcAddress(user32Handle, "AllowSetForegroundWindow");
// Create a remote thread in the other process and make it call the API.
remoteThreadHandle = NativeMethods.CreateRemoteThread(
remoteProcessHandle,
0,
100000,
entryPoint,
NativeMethods.GetCurrentProcessId(),
0,
0);
if (remoteThreadHandle == 0)
{
throw new InvalidOperationException("Failed to create the remote thread.");
}
// Wait for the remote thread to terminate.
_ = NativeMethods.WaitForSingleObject(remoteThreadHandle, 5000);
}
finally
{
if (remoteProcessHandle != 0)
{
_ = NativeMethods.CloseHandle(remoteProcessHandle);
}
if (remoteThreadHandle != 0)
{
_ = NativeMethods.CloseHandle(remoteThreadHandle);
}
if (user32Handle != 0)
{
_ = NativeMethods.FreeLibrary(user32Handle);
}
}
}
}
}

View File

@@ -2,7 +2,7 @@
// 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
namespace Peek.UI.FileSystem
{
using System;
using System.Collections.Generic;
@@ -10,6 +10,7 @@ namespace Peek.UI
using System.Threading;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.UI.Dispatching;
using Peek.Common.Models;
using Peek.UI.Helpers;
@@ -17,6 +18,7 @@ namespace Peek.UI
{
private const int UninitializedItemIndex = -1;
private readonly object _mutateQueryDataLock = new ();
private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread();
[ObservableProperty]
private File? currentFile;
@@ -24,7 +26,11 @@ namespace Peek.UI
[ObservableProperty]
private List<File> files = new ();
public int CurrentItemIndex { get; set; } = UninitializedItemIndex;
[ObservableProperty]
private bool isMultiSelection;
[ObservableProperty]
private int currentItemIndex = UninitializedItemIndex;
private CancellationTokenSource CancellationTokenSource { get; set; } = new CancellationTokenSource();
@@ -33,6 +39,7 @@ namespace Peek.UI
public void Clear()
{
CurrentFile = null;
IsMultiSelection = false;
if (InitializeFilesTask != null && InitializeFilesTask.Status == TaskStatus.Running)
{
@@ -44,8 +51,11 @@ namespace Peek.UI
lock (_mutateQueryDataLock)
{
Files = new List<File>();
CurrentItemIndex = UninitializedItemIndex;
_dispatcherQueue.TryEnqueue(() =>
{
Files = new List<File>();
CurrentItemIndex = UninitializedItemIndex;
});
}
}
@@ -84,6 +94,8 @@ namespace Peek.UI
return;
}
IsMultiSelection = selectedItems.Count > 1;
// Prioritize setting CurrentFile, which notifies UI
var firstSelectedItem = selectedItems.Item(0);
CurrentFile = new File(firstSelectedItem.Path);
@@ -160,8 +172,12 @@ namespace Peek.UI
lock (_mutateQueryDataLock)
{
cancellationToken.ThrowIfCancellationRequested();
Files = tempFiles;
CurrentItemIndex = tempCurIndex;
_dispatcherQueue.TryEnqueue(() =>
{
Files = tempFiles;
CurrentItemIndex = tempCurIndex;
});
}
}
}

View File

@@ -2,10 +2,12 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Diagnostics;
using Peek.Common.Models;
using System;
using System.Runtime.InteropServices;
using System.Text;
using Peek.UI.Native;
using Windows.Win32;
using Windows.Win32.Foundation;
namespace Peek.UI.Helpers
{
@@ -15,14 +17,21 @@ namespace Peek.UI.Helpers
{
var foregroundWindowHandle = NativeMethods.GetForegroundWindow();
int capacity = PInvoke.GetWindowTextLength(new HWND(foregroundWindowHandle)) * 2;
StringBuilder foregroundWindowTitleBuffer = new StringBuilder(capacity);
NativeMethods.GetWindowText(new HWND(foregroundWindowHandle), foregroundWindowTitleBuffer, foregroundWindowTitleBuffer.Capacity);
string foregroundWindowTitle = foregroundWindowTitleBuffer.ToString();
var shell = new Shell32.Shell();
foreach (SHDocVw.InternetExplorer window in shell.Windows())
{
// TODO: figure out which window is the active explorer tab
// https://github.com/microsoft/PowerToys/issues/22507
if (window.HWND == (int)foregroundWindowHandle)
var shellFolderView = (Shell32.IShellFolderViewDual2)window.Document;
var folderTitle = shellFolderView.Folder.Title;
if (window.HWND == (int)foregroundWindowHandle && folderTitle == foregroundWindowTitle)
{
return (Shell32.IShellFolderViewDual2)window.Document;
return shellFolderView;
}
}

View File

@@ -30,6 +30,8 @@
x:Name="TitleBarControl"
Grid.Row="0"
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}" />
<fp:FilePreview

View File

@@ -4,6 +4,7 @@
namespace Peek.UI
{
using System.Diagnostics;
using interop;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml.Input;
@@ -11,8 +12,7 @@ namespace Peek.UI
using Peek.UI.Extensions;
using Peek.UI.Native;
using Windows.Foundation;
using Windows.System;
using Windows.UI.Core;
using Windows.Win32;
using WinUIEx;
/// <summary>
@@ -86,24 +86,47 @@ namespace Peek.UI
/// <param name="e">PreviewSizeChangedArgs</param>
private void FilePreviewer_PreviewSizeChanged(object sender, PreviewSizeChangedArgs e)
{
// TODO: Use design-defined rules for adjusted window size
var requestedSize = e.WindowSizeRequested;
var monitorSize = this.GetMonitorSize();
// TODO: Use design-defined rules for adjusted window size
var titleBarHeight = TitleBarControl.ActualHeight;
var maxContentSize = new Size(monitorSize.Width * MaxWindowToMonitorRatio, (monitorSize.Height - titleBarHeight) * MaxWindowToMonitorRatio);
var maxContentSize = new Size(0, 0);
var minContentSize = new Size(MinWindowWidth, MinWindowHeight - titleBarHeight);
var adjustedContentSize = requestedSize.Fit(maxContentSize, minContentSize);
var adjustedContentSize = new Size(0, 0);
if (e.WindowSizeFormat == SizeFormat.Percentage)
{
maxContentSize = new Size(monitorSize.Width * requestedSize.Width, (monitorSize.Height - titleBarHeight) * requestedSize.Height);
minContentSize = new Size(MinWindowWidth, MinWindowHeight - titleBarHeight);
adjustedContentSize = maxContentSize.Fit(maxContentSize, minContentSize);
}
else if (e.WindowSizeFormat == SizeFormat.Pixels)
{
maxContentSize = new Size(monitorSize.Width * MaxWindowToMonitorRatio, (monitorSize.Height - titleBarHeight) * MaxWindowToMonitorRatio);
minContentSize = new Size(MinWindowWidth, MinWindowHeight - titleBarHeight);
adjustedContentSize = requestedSize.Fit(maxContentSize, minContentSize);
}
else
{
Debug.Assert(false, "Unknown SizeFormat set for resizing window.");
adjustedContentSize = minContentSize;
return;
}
// TODO: Only re-center if window has not been resized by user (or use design-defined logic).
// TODO: Investigate why portrait images do not perfectly fit edge-to-edge
var monitorScale = this.GetMonitorScale();
var scaledWindowWidth = adjustedContentSize.Width / monitorScale;
var scaledWindowHeight = adjustedContentSize.Height / monitorScale;
this.CenterOnScreen(scaledWindowWidth + WindowHeightContentPadding, scaledWindowHeight + titleBarHeight + WindowWidthContentPadding);
this.Show();
this.BringToFront();
this.BringToForeground();
}
/// <summary>

View File

@@ -7,17 +7,23 @@ 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 ();
public MainWindowViewModel()
{
NavigationThrottleTimer.Tick += NavigationThrottleTimer_Tick;
NavigationThrottleTimer.Interval = TimeSpan.FromMilliseconds(NavigationThrottleDelayMs);
}
private DispatcherTimer NavigationThrottleTimer { get; set; } = new ();
public void AttemptLeftNavigation()
{
if (NavigationThrottleTimer.IsEnabled)
@@ -53,10 +59,5 @@ namespace Peek.UI
((DispatcherTimer)sender).Stop();
}
[ObservableProperty]
private FolderItemsQuery _folderItemsQuery = new ();
private DispatcherTimer NavigationThrottleTimer { get; set; } = new ();
}
}

View File

@@ -11,41 +11,210 @@ namespace Peek.UI.Native
public static class NativeMethods
{
internal const uint PROCESS_ALL_ACCESS = 0x1f0fff;
internal const IntPtr HWND_TOP = 0;
internal const uint SWP_DRAWFRAME = 0x0020;
internal const uint SWP_NOMOVE = 0x0002;
internal const uint SWP_NOSIZE = 0x0001;
internal const uint SWP_SHOWWINDOW = 0x0040;
internal const int WM_SYSCOMMAND = 0x0112;
internal const int SC_RESTORE = 0xF120;
[DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
internal static extern IntPtr GetForegroundWindow();
[DllImport("Shlwapi.dll", SetLastError = true, CharSet = CharSet.Auto)]
internal static extern HResult AssocQueryString(AssocF flags, AssocStr str, string pszAssoc, string? pszExtra, [Out] StringBuilder? pszOut, [In][Out] ref uint pcchOut);
[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,
}
[DllImport("user32.dll")]
internal static extern IntPtr GetWindowThreadProcessId(IntPtr hWnd, ref IntPtr ProcessId);
public enum AssocStr
{
Command = 1,
Executable,
FriendlyDocName,
FriendlyAppName,
NoOpen,
ShellNewValue,
DDECommand,
DDEIfExec,
DDEApplication,
DDETopic,
}
[DllImport("kernel32.dll")]
internal static extern IntPtr OpenProcess(uint fdwAccess, bool fInherit, IntPtr IDProcess);
[DllImport("kernel32.dll")]
internal static extern int CloseHandle(IntPtr hObject);
[DllImport("kernel32.dll")]
internal static extern IntPtr GetProcAddress(IntPtr hModule, string lpProcName);
[DllImport("kernel32.dll")]
internal static extern IntPtr LoadLibrary(string lpLibName);
[DllImport("kernel32.dll")]
internal static extern bool FreeLibrary(IntPtr lib);
[DllImport("kernel32.dll")]
internal static extern IntPtr CreateRemoteThread(IntPtr hProcess, IntPtr bogusAttributes, int dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, int dwCreationFlags, IntPtr lpThreadId);
[DllImport("kernel32.dll")]
internal static extern uint WaitForSingleObject(IntPtr hObject, int dwMilliseconds);
[DllImport("user32.dll")]
internal static extern IntPtr GetShellWindow();
[DllImport("kernel32.dll")]
internal static extern IntPtr GetCurrentProcess();
[DllImport("kernel32.dll")]
internal static extern int GetCurrentProcessId();
[DllImport("user32.dll")]
internal static extern int SendMessage(IntPtr hwnd, int wMsg, int wParam, int lParam);
[DllImport("user32.dll")]
internal static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
[DllImport("user32.dll")]
internal static extern int SetForegroundWindow(IntPtr hWnd);
[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,
}
}

View File

@@ -1,3 +1,6 @@
MonitorFromWindow
GetMonitorInfo
GetDpiForWindow
GetDpiForWindow
GetWindowTextLength
SetWinEventHook
UnhookWinEvent

View File

@@ -63,7 +63,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.0.0" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.1.5" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.2.221116.1" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.22621.1" />
<PackageReference Include="WinUIEx" Version="1.8.0" />
<Manifest Include="$(ApplicationManifest)" />

View File

@@ -122,7 +122,7 @@
<comment>Name of application.</comment>
</data>
<data name="AppTitle_FileCounts_Text" xml:space="preserve">
<value>({0}/{1} files)</value>
<value>({0}/{1})</value>
<comment>Text for the file count in the titlebar. 0: the index of the current file. 1: the total number of files selected.</comment>
</data>
<data name="LaunchAppButton_OpenWith_Text" xml:space="preserve">
@@ -141,4 +141,68 @@
<value>Open with {0} (Enter)</value>
<comment>Tooltip for button to launch default application. 0: name of the default application.</comment>
</data>
<data name="UnsupportedFile_FileType" xml:space="preserve">
<value>File Type: {0}</value>
<comment>File Type label for the unsupported files view. {0} is the type.</comment>
</data>
<data name="UnsupportedFile_FileSize" xml:space="preserve">
<value>Size: {0}</value>
<comment>File Size label for the unsupported files view. {0} is the size.</comment>
</data>
<data name="UnsupportedFile_DateModified" xml:space="preserve">
<value>Date Modified: {0}</value>
<comment>Date Modified label for the unsupported files view. {0} is the date.</comment>
</data>
<data name="ReadableString_ByteAbbreviationFormat" xml:space="preserve">
<value>{0} B</value>
<comment>Abbrivation for the size unit byte.</comment>
</data>
<data name="ReadableString_KiloByteAbbreviationFormat" xml:space="preserve">
<value>{0} KB</value>
<comment>Abbrivation for the size unit kilobyte.</comment>
</data>
<data name="ReadableString_MegaByteAbbreviationFormat" xml:space="preserve">
<value>{0} MB</value>
<comment>Abbrivation for the size unit megabyte.</comment>
</data>
<data name="ReadableString_GigaByteAbbreviationFormat" xml:space="preserve">
<value>{0} GB</value>
<comment>Abbrivation for the size unit gigabyte.</comment>
</data>
<data name="ReadableString_TeraByteAbbreviationFormat" xml:space="preserve">
<value>{0} TB</value>
<comment>Abbrivation for the size unit terabyte.</comment>
</data>
<data name="ReadableString_PetaByteAbbreviationFormat" xml:space="preserve">
<value>{0} PB</value>
<comment>Abbrivation for the size unit petabyte.</comment>
</data>
<data name="ReadableString_ExaByteAbbreviationFormat" xml:space="preserve">
<value>{0} EB</value>
<comment>Abbrivation for the size unit exabyte.</comment>
</data>
<data name="PreviewTooltip_FileName" xml:space="preserve">
<value>Filename: {0}</value>
<comment>Filename for the tooltip of preview. {0} is the name.</comment>
</data>
<data name="PreviewTooltip_FileType" xml:space="preserve">
<value>Item Type: {0}</value>
<comment>Item Type for the tooltip of preview. {0} is the type.</comment>
</data>
<data name="PreviewTooltip_DateModified" xml:space="preserve">
<value>Date Modified: {0}</value>
<comment>Date Modified label for the tooltip of preview. {0} is the date.</comment>
</data>
<data name="PreviewTooltip_Dimensions" xml:space="preserve">
<value>Dimensions: {0} x {1}</value>
<comment>Dimensions label for the tooltip of preview. {0} is the width, {1} is the height.</comment>
</data>
<data name="PreviewTooltip_FileSize" xml:space="preserve">
<value>Size: {0}</value>
<comment>File Size label for the tooltip of preview. {0} is the size.</comment>
</data>
<data name="PreviewTooltip_Blank" xml:space="preserve">
<value>File preview</value>
<comment>Tooltip of preview when there's no file info available.</comment>
</data>
</root>

View File

@@ -12,111 +12,110 @@
<Grid x:Name="TitleBarRootContainer" Height="48">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition x:Name="SystemLeftPaddingColumn" Width="0" />
<ColumnDefinition x:Name="DraggableColumn" Width="*" />
<ColumnDefinition x:Name="LaunchAppButtonColumn" Width="Auto" />
<ColumnDefinition x:Name="AppRightPaddingColumn" Width="65" />
<ColumnDefinition x:Name="SystemRightPaddingColumn" Width="0" />
</Grid.ColumnDefinitions>
<StackPanel
x:Name="AppIconAndName"
Grid.Column="0"
Margin="8,4"
<Grid
x:Name="AppIconAndFileTitleContainer"
Grid.Column="1"
Margin="8,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Orientation="Horizontal">
ColumnSpacing="4">
<Grid.ColumnDefinitions>
<ColumnDefinition x:Name="AppIconColumn" Width="32" />
<ColumnDefinition x:Name="FileTitleColumn" Width="*" />
</Grid.ColumnDefinitions>
<Image
x:Name="PeekLogo"
x:Uid="PeekLogo"
Grid.Column="0"
Width="24"
Height="24"
Margin="4,0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Source="../Assets/AppList.png"
Source="../Assets/AppList.scale-400.png"
Stretch="UniformToFill" />
<TextBlock
x:Name="AppTitle_FileCount"
x:Uid="AppTitle_FileCount"
Margin="4,0,0,0"
<Grid
x:Name="FileCountAndNameContainer"
Grid.Column="1"
HorizontalAlignment="Left"
VerticalAlignment="Center"
FontWeight="Bold"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind FileCountText, Mode=OneWay}"
Visibility="Collapsed" />
ColumnSpacing="4">
<Grid.ColumnDefinitions>
<ColumnDefinition x:Name="FileCountColumn" Width="auto" />
<ColumnDefinition x:Name="FileNameColumn" Width="*" />
</Grid.ColumnDefinitions>
<TextBlock
x:Name="AppTitle_FileName"
x:Uid="AppTitle_FileName"
MaxWidth="100"
Margin="4,0,0,0"
VerticalAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind File.FileName, Mode=OneWay}"
TextWrapping="NoWrap" />
</StackPanel>
<TextBlock
x:Name="AppTitle_FileCount"
x:Uid="AppTitle_FileCount"
Grid.Column="0"
FontWeight="Bold"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind FileCountText, Mode=OneWay}"
Visibility="{x:Bind IsMultiSelection, Mode=OneWay}" />
<Grid
x:Name="LaunchAppButtonContainer"
Grid.Column="1"
Margin="4,4,144,4"
HorizontalAlignment="Right"
VerticalAlignment="Center">
<Button
x:Name="LaunchAppButton"
x:Uid="LaunchAppButton"
Command="{x:Bind LaunchDefaultAppButtonAsyncCommand, Mode=OneWay}"
ToolTipService.ToolTip="{x:Bind OpenWithAppToolTip, Mode=OneWay}">
<Button.Content>
<StackPanel Orientation="Horizontal">
<FontIcon
x:Name="LaunchAppButton_Icon"
x:Uid="LaunchAppButton_Icon"
FontSize="{StaticResource CaptionTextBlockFontSize}"
Glyph="&#xE8E5;" />
<TextBlock
x:Name="LaunchAppButton_Text"
x:Uid="LaunchAppButton_Text"
Margin="4,0,0,0"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind OpenWithAppText, Mode=OneWay}" />
</StackPanel>
</Button.Content>
<Button.KeyboardAccelerators>
<KeyboardAccelerator Key="Enter" />
</Button.KeyboardAccelerators>
</Button>
<TextBlock
x:Name="AppTitle_FileName"
x:Uid="AppTitle_FileName"
Grid.Column="1"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind File.FileName, Mode=OneWay}"
TextWrapping="NoWrap" />
</Grid>
</Grid>
<Button
x:Name="LaunchAppButton"
x:Uid="LaunchAppButton"
Grid.Column="2"
VerticalAlignment="Center"
Command="{x:Bind LaunchDefaultAppButtonAsyncCommand, Mode=OneWay}"
ToolTipService.ToolTip="{x:Bind OpenWithAppToolTip, Mode=OneWay}">
<Button.Content>
<StackPanel Orientation="Horizontal" Spacing="4">
<FontIcon
x:Name="LaunchAppButton_Icon"
x:Uid="LaunchAppButton_Icon"
FontSize="{StaticResource CaptionTextBlockFontSize}"
Glyph="&#xE8E5;" />
<TextBlock
x:Name="LaunchAppButton_Text"
x:Uid="LaunchAppButton_Text"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind OpenWithAppText, Mode=OneWay}" />
</StackPanel>
</Button.Content>
<Button.KeyboardAccelerators>
<KeyboardAccelerator Key="Enter" />
</Button.KeyboardAccelerators>
</Button>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="AdaptiveWidth">
<VisualState x:Name="MaximumLayout">
<VisualState.StateTriggers>
<AdaptiveTrigger MinWindowWidth="720" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="AppTitle_FileName.MaxWidth" Value="560" />
<Setter Target="AppTitle_FileCount.Visibility" Value="Visible" />
<Setter Target="LaunchAppButton_Text.Visibility" Value="Visible" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="WideLayout">
<VisualState.StateTriggers>
<AdaptiveTrigger MinWindowWidth="560" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="AppTitle_FileName.MaxWidth" Value="400" />
<Setter Target="AppTitle_FileCount.Visibility" Value="Visible" />
<Setter Target="LaunchAppButton_Text.Visibility" Value="Visible" />
<Setter Target="LaunchAppButton.Visibility" Value="Visible" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="MediumLayout">
<VisualState.StateTriggers>
<AdaptiveTrigger MinWindowWidth="480" />
<AdaptiveTrigger MinWindowWidth="340" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="AppTitle_FileName.MaxWidth" Value="320" />
<Setter Target="AppTitle_FileCount.Visibility" Value="Visible" />
<Setter Target="LaunchAppButton_Text.Visibility" Value="Collapsed" />
<Setter Target="LaunchAppButton.Visibility" Value="Visible" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="MinimumLayout">
@@ -124,9 +123,8 @@
<AdaptiveTrigger MinWindowWidth="0" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="AppTitle_FileName.MaxWidth" Value="160" />
<Setter Target="AppTitle_FileCount.Visibility" Value="Collapsed" />
<Setter Target="LaunchAppButton_Text.Visibility" Value="Collapsed" />
<Setter Target="LaunchAppButton.Visibility" Value="Collapsed" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>

View File

@@ -5,6 +5,7 @@
namespace Peek.UI.Views
{
using System;
using System.Collections.Generic;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using ManagedCommon;
@@ -13,8 +14,10 @@ namespace Peek.UI.Views
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Peek.Common.Models;
using Peek.UI.Extensions;
using Peek.UI.Helpers;
using Windows.ApplicationModel.Resources;
using Windows.Graphics;
using Windows.Storage;
using Windows.System;
using WinUIEx;
@@ -23,20 +26,32 @@ namespace Peek.UI.Views
public sealed partial class TitleBar : UserControl
{
public static readonly DependencyProperty FileProperty =
DependencyProperty.Register(
nameof(File),
typeof(File),
typeof(TitleBar),
new PropertyMetadata(null, (d, e) => ((TitleBar)d).OnFilePropertyChanged()));
DependencyProperty.Register(
nameof(File),
typeof(File),
typeof(TitleBar),
new PropertyMetadata(null, (d, e) => ((TitleBar)d).OnFilePropertyChanged()));
public static readonly DependencyProperty FileIndexProperty =
DependencyProperty.Register(
nameof(FileIndex),
typeof(int),
typeof(TitleBar),
new PropertyMetadata(-1, (d, e) => ((TitleBar)d).OnFileIndexPropertyChanged()));
public static readonly DependencyProperty IsMultiSelectionProperty =
DependencyProperty.Register(
nameof(IsMultiSelection),
typeof(bool),
typeof(TitleBar),
new PropertyMetadata(false));
public static readonly DependencyProperty NumberOfFilesProperty =
DependencyProperty.Register(
nameof(NumberOfFiles),
typeof(int),
typeof(TitleBar),
new PropertyMetadata(null, null));
private string? defaultAppName;
DependencyProperty.Register(
nameof(NumberOfFiles),
typeof(int),
typeof(TitleBar),
new PropertyMetadata(null, null));
[ObservableProperty]
private string openWithAppText = ResourceLoader.GetForViewIndependentUse().GetString("LaunchAppButton_OpenWith_Text");
@@ -50,6 +65,7 @@ namespace Peek.UI.Views
public TitleBar()
{
InitializeComponent();
TitleBarRootContainer.SizeChanged += TitleBarRootContainer_SizeChanged;
}
public File File
@@ -58,14 +74,32 @@ namespace Peek.UI.Views
set => SetValue(FileProperty, value);
}
public int FileIndex
{
get => (int)GetValue(FileIndexProperty);
set => SetValue(FileIndexProperty, value);
}
public bool IsMultiSelection
{
get => (bool)GetValue(IsMultiSelectionProperty);
set => SetValue(IsMultiSelectionProperty, value);
}
public int NumberOfFiles
{
get => (int)GetValue(NumberOfFilesProperty);
set => SetValue(NumberOfFilesProperty, value);
}
private string? DefaultAppName { get; set; }
private Window? MainWindow { get; set; }
public void SetTitleBarToWindow(MainWindow mainWindow)
{
MainWindow = mainWindow;
if (AppWindowTitleBar.IsCustomizationSupported())
{
UpdateTitleBarCustomization(mainWindow);
@@ -83,65 +117,13 @@ namespace Peek.UI.Views
}
}
private void UpdateTitleBarCustomization(MainWindow mainWindow)
{
if (AppWindowTitleBar.IsCustomizationSupported())
{
AppWindow appWindow = mainWindow.GetAppWindow();
appWindow.TitleBar.ExtendsContentIntoTitleBar = true;
appWindow.TitleBar.ButtonBackgroundColor = Colors.Transparent;
appWindow.TitleBar.ButtonInactiveBackgroundColor = Colors.Transparent;
appWindow.TitleBar.SetDragRectangles(new Windows.Graphics.RectInt32[]
{
new Windows.Graphics.RectInt32(0, 0, (int)TitleBarRootContainer.ActualWidth, (int)TitleBarRootContainer.ActualHeight),
});
mainWindow.SetTitleBar(this);
}
}
private void OnFilePropertyChanged()
{
if (File == null)
{
return;
}
UpdateFileCountText();
UpdateDefaultAppToLaunch();
}
private void UpdateFileCountText()
{
// Update file count
if (NumberOfFiles > 1)
{
// TODO: Update the hardcoded fileIndex when the NFQ PR gets merged
int currentFileIndex = 1;
string fileCountTextFormat = ResourceLoader.GetForViewIndependentUse().GetString("AppTitle_FileCounts_Text");
FileCountText = string.Format(fileCountTextFormat, currentFileIndex, NumberOfFiles);
}
}
private void UpdateDefaultAppToLaunch()
{
// Update the name of default app to launch
defaultAppName = DefaultAppHelper.TryGetDefaultAppName(File.Extension);
string openWithAppTextFormat = ResourceLoader.GetForViewIndependentUse().GetString("LaunchAppButton_OpenWithApp_Text");
OpenWithAppText = string.Format(openWithAppTextFormat, defaultAppName);
string openWithAppToolTipFormat = ResourceLoader.GetForViewIndependentUse().GetString("LaunchAppButton_OpenWithApp_ToolTip");
OpenWithAppToolTip = string.Format(openWithAppToolTipFormat, defaultAppName);
}
[RelayCommand]
private async void LaunchDefaultAppButtonAsync()
{
StorageFile storageFile = await File.GetStorageFileAsync();
LauncherOptions options = new ();
if (string.IsNullOrEmpty(defaultAppName))
if (string.IsNullOrEmpty(DefaultAppName))
{
// If there's no default app found, open the App picker
options.DisplayApplicationPicker = true;
@@ -159,5 +141,97 @@ namespace Peek.UI.Views
}
}
}
private void TitleBarRootContainer_SizeChanged(object sender, SizeChangedEventArgs e)
{
UpdateDragRegion();
}
private void UpdateDragRegion()
{
if (MainWindow == null)
{
return;
}
var appWindow = MainWindow.GetAppWindow();
if (AppWindowTitleBar.IsCustomizationSupported() && appWindow != null && appWindow.TitleBar.ExtendsContentIntoTitleBar)
{
var scale = MainWindow.GetMonitorScale();
SystemRightPaddingColumn.Width = new GridLength(appWindow.TitleBar.RightInset / scale);
SystemLeftPaddingColumn.Width = new GridLength(appWindow.TitleBar.LeftInset / scale);
var dragRectsList = new List<RectInt32>();
RectInt32 dragRectangleLeft;
dragRectangleLeft.X = (int)(SystemLeftPaddingColumn.ActualWidth * scale);
dragRectangleLeft.Y = 0;
dragRectangleLeft.Height = (int)(TitleBarRootContainer.ActualHeight * scale);
dragRectangleLeft.Width = (int)(DraggableColumn.ActualWidth * scale);
RectInt32 dragRectangleRight;
dragRectangleRight.X = (int)((SystemLeftPaddingColumn.ActualWidth + DraggableColumn.ActualWidth + LaunchAppButtonColumn.ActualWidth) * scale);
dragRectangleRight.Y = 0;
dragRectangleRight.Height = (int)(TitleBarRootContainer.ActualHeight * scale);
dragRectangleRight.Width = (int)(AppRightPaddingColumn.ActualWidth * scale);
dragRectsList.Add(dragRectangleLeft);
dragRectsList.Add(dragRectangleRight);
appWindow.TitleBar.SetDragRectangles(dragRectsList.ToArray());
}
}
private void UpdateTitleBarCustomization(MainWindow mainWindow)
{
if (AppWindowTitleBar.IsCustomizationSupported())
{
AppWindow appWindow = mainWindow.GetAppWindow();
appWindow.TitleBar.ExtendsContentIntoTitleBar = true;
appWindow.TitleBar.ButtonBackgroundColor = Colors.Transparent;
appWindow.TitleBar.ButtonInactiveBackgroundColor = Colors.Transparent;
mainWindow.SetTitleBar(this);
}
}
private void OnFilePropertyChanged()
{
if (File == null)
{
return;
}
UpdateFileCountText();
UpdateDefaultAppToLaunch();
}
private void OnFileIndexPropertyChanged()
{
UpdateFileCountText();
}
private void UpdateFileCountText()
{
// Update file count
if (NumberOfFiles > 1)
{
string fileCountTextFormat = ResourceLoader.GetForViewIndependentUse().GetString("AppTitle_FileCounts_Text");
FileCountText = string.Format(fileCountTextFormat, FileIndex + 1, NumberOfFiles);
}
}
private void UpdateDefaultAppToLaunch()
{
// Update the name of default app to launch
DefaultAppName = DefaultAppHelper.TryGetDefaultAppName(File.Extension);
string openWithAppTextFormat = ResourceLoader.GetForViewIndependentUse().GetString("LaunchAppButton_OpenWithApp_Text");
OpenWithAppText = string.Format(openWithAppTextFormat, DefaultAppName);
string openWithAppToolTipFormat = ResourceLoader.GetForViewIndependentUse().GetString("LaunchAppButton_OpenWithApp_ToolTip");
OpenWithAppToolTip = string.Format(openWithAppToolTipFormat, DefaultAppName);
}
}
}

View File

@@ -0,0 +1,56 @@
// 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);
}

View File

@@ -0,0 +1,32 @@
// 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;
}
}
}

View File

@@ -10,13 +10,22 @@ namespace Microsoft.PowerToys.Settings.UI.Library
{
public class PeekProperties
{
public const double DefaultUnsupportedFileWidthPercent = 40.0;
public const double DefaultUnsupportedFileHeightPercent = 40.0;
public PeekProperties()
{
ActivationShortcut = new HotkeySettings(false, true, false, false, 0x20);
UnsupportedFileWidthPercent = DefaultUnsupportedFileWidthPercent;
UnsupportedFileHeightPercent = DefaultUnsupportedFileHeightPercent;
}
public HotkeySettings ActivationShortcut { get; set; }
public double UnsupportedFileWidthPercent { get; set; }
public double UnsupportedFileHeightPercent { get; set; }
public override string ToString()
=> JsonSerializer.Serialize(this);
}

View File

@@ -79,6 +79,35 @@ namespace Microsoft.PowerToys.Settings.UI.Library.ViewModels
}
}
public double UnsupportedFileWidthPercent
{
get => _peekSettings.Properties.UnsupportedFileWidthPercent;
set
{
if (_peekSettings.Properties.UnsupportedFileWidthPercent != value)
{
_peekSettings.Properties.UnsupportedFileWidthPercent = value;
OnPropertyChanged(nameof(UnsupportedFileWidthPercent));
NotifySettingsChanged();
}
}
}
public double UnsupportedFileHeightPercent
{
get => _peekSettings.Properties.UnsupportedFileHeightPercent;
set
{
if (_peekSettings.Properties.UnsupportedFileHeightPercent != value)
{
_peekSettings.Properties.UnsupportedFileHeightPercent = value;
OnPropertyChanged(nameof(UnsupportedFileHeightPercent));
NotifySettingsChanged();
}
}
}
private void NotifySettingsChanged()
{
// Using InvariantCulture as this is an IPC message

View File

@@ -2426,6 +2426,14 @@ From there, simply click on one of the supported files in the File Explorer and
<value>Enable Peek</value>
<comment>Peek is a product name, do not loc</comment>
</data>
<data name="Peek_UnsupportedFileWindowWidth.Header" xml:space="preserve">
<value>Unsupported file window width (%)</value>
<comment>Setting for width percent for unsupported file window.</comment>
</data>
<data name="Peek_UnsupportedFileWindowHeight.Header" xml:space="preserve">
<value>Unsupported file window height (%)</value>
<comment>Setting for height percent for unsupported file window.</comment>
</data>
<data name="FancyZones_DisableRoundCornersOnWindowSnap.Content" xml:space="preserve">
<value>Disable round corners when window is snapped</value>
</data>

View File

@@ -22,6 +22,24 @@
<controls:ShortcutControl MinWidth="{StaticResource SettingActionControlMinWidth}" HotkeySettings="{x:Bind Path=ViewModel.ActivationShortcut, Mode=TwoWay}" />
</labs:SettingsCard>
<!-- Temporary internal setting. -->
<labs:SettingsCard x:Uid="Peek_UnsupportedFileWindowWidth">
<Slider
MinWidth="{StaticResource SettingActionControlMinWidth}"
Maximum="100"
Minimum="20"
Value="{x:Bind Mode=TwoWay, Path=ViewModel.UnsupportedFileWidthPercent}" />
</labs:SettingsCard>
<!-- Temporary internal setting. -->
<labs:SettingsCard x:Uid="Peek_UnsupportedFileWindowHeight">
<Slider
MinWidth="{StaticResource SettingActionControlMinWidth}"
Maximum="100"
Minimum="20"
Value="{x:Bind Mode=TwoWay, Path=ViewModel.UnsupportedFileHeightPercent}" />
</labs:SettingsCard>
</StackPanel>
</controls:SettingsPageControl.ModuleContent>
</controls:SettingsPageControl>