mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-01-05 03:47:01 +01:00
Compare commits
14 Commits
yizzho/pee
...
samchaps/w
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf72adc942 | ||
|
|
60bf86825b | ||
|
|
4ef3f23897 | ||
|
|
5bd9dd5935 | ||
|
|
5590eb9484 | ||
|
|
6f06f76784 | ||
|
|
5981d0e81e | ||
|
|
539a4e5678 | ||
|
|
aea217ddca | ||
|
|
d001a4c0e0 | ||
|
|
5712123598 | ||
|
|
e504653323 | ||
|
|
b46b8d176f | ||
|
|
b98f233b75 |
@@ -4,7 +4,7 @@
|
||||
|
||||
namespace Peek.Common.Converters
|
||||
{
|
||||
public class BoolConverter
|
||||
public static class BoolConverter
|
||||
{
|
||||
public static bool Invert(bool value)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
32
src/modules/peek/Peek.Common/Extensions/TaskExtension.cs
Normal file
32
src/modules/peek/Peek.Common/Extensions/TaskExtension.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
57
src/modules/peek/Peek.Common/Helpers/ReadableStringHelper.cs
Normal file
57
src/modules/peek/Peek.Common/Helpers/ReadableStringHelper.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 ();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
MonitorFromWindow
|
||||
GetMonitorInfo
|
||||
GetDpiForWindow
|
||||
GetDpiForWindow
|
||||
GetWindowTextLength
|
||||
SetWinEventHook
|
||||
UnhookWinEvent
|
||||
@@ -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)" />
|
||||
|
||||
@@ -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>
|
||||
@@ -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="" />
|
||||
<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="" />
|
||||
<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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
56
src/modules/peek/Peek.UI/Window/WindowEventHook.cs
Normal file
56
src/modules/peek/Peek.UI/Window/WindowEventHook.cs
Normal 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);
|
||||
}
|
||||
32
src/modules/peek/Peek.UI/Window/WindowEventSafeHandle.cs
Normal file
32
src/modules/peek/Peek.UI/Window/WindowEventSafeHandle.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user