mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-16 19:57:57 +01:00
[Peek]Add support for Explorer preview handlers (#28690)
* Add support for preview handlers * Fix spelling * Fix DPI resizing and redraw * Make source into an ObservableProperty * Add handler visibility property * Better error handling * Add support for IInitializeWithItem * Run preview handlers in separate processes * Fix redrawing when switching previewers
This commit is contained in:
5
.github/actions/spell-check/expect.txt
vendored
5
.github/actions/spell-check/expect.txt
vendored
@@ -1429,6 +1429,7 @@ ppsi
|
|||||||
ppsid
|
ppsid
|
||||||
ppsrm
|
ppsrm
|
||||||
ppsrree
|
ppsrree
|
||||||
|
ppstm
|
||||||
ppsz
|
ppsz
|
||||||
pptal
|
pptal
|
||||||
ppv
|
ppv
|
||||||
@@ -1471,6 +1472,8 @@ psfi
|
|||||||
Psr
|
Psr
|
||||||
psrm
|
psrm
|
||||||
psrree
|
psrree
|
||||||
|
pstatstg
|
||||||
|
pstm
|
||||||
pstr
|
pstr
|
||||||
pstream
|
pstream
|
||||||
pstrm
|
pstrm
|
||||||
@@ -1823,6 +1826,7 @@ STDMETHODCALLTYPE
|
|||||||
STDMETHODIMP
|
STDMETHODIMP
|
||||||
stefan
|
stefan
|
||||||
Stereolithography
|
Stereolithography
|
||||||
|
STGC
|
||||||
STGM
|
STGM
|
||||||
STGMEDIUM
|
STGMEDIUM
|
||||||
sticpl
|
sticpl
|
||||||
@@ -2188,6 +2192,7 @@ wnd
|
|||||||
WNDCLASS
|
WNDCLASS
|
||||||
WNDCLASSEX
|
WNDCLASSEX
|
||||||
WNDCLASSEXW
|
WNDCLASSEXW
|
||||||
|
WNDCLASSW
|
||||||
WNDPROC
|
WNDPROC
|
||||||
wordpad
|
wordpad
|
||||||
workaround
|
workaround
|
||||||
|
|||||||
@@ -3,3 +3,8 @@ _SHCONTF
|
|||||||
SIGDN
|
SIGDN
|
||||||
SHGDNF
|
SHGDNF
|
||||||
SIATTRIBFLAGS
|
SIATTRIBFLAGS
|
||||||
|
IInitializeWithFile
|
||||||
|
IInitializeWithItem
|
||||||
|
IInitializeWithStream
|
||||||
|
IPreviewHandler
|
||||||
|
IPreviewHandlerVisuals
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<!-- Copyright (c) Microsoft Corporation. All rights reserved. -->
|
||||||
|
<!-- Licensed under the MIT License. See LICENSE in the project root for license information. -->
|
||||||
|
|
||||||
|
<UserControl
|
||||||
|
x:Class="Peek.FilePreviewer.Controls.ShellPreviewHandlerControl"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:local="using:Peek.FilePreviewer.Controls"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
Loaded="UserControl_Loaded"
|
||||||
|
EffectiveViewportChanged="UserControl_EffectiveViewportChanged"
|
||||||
|
IsEnabled="False" IsTabStop="True" GotFocus="UserControl_GotFocus"
|
||||||
|
ActualThemeChanged="{x:Bind UpdatePreviewerTheme}">
|
||||||
|
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using Microsoft.UI;
|
||||||
|
using Microsoft.UI.Xaml;
|
||||||
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
using Windows.Win32;
|
||||||
|
using Windows.Win32.Foundation;
|
||||||
|
using Windows.Win32.Graphics.Gdi;
|
||||||
|
using Windows.Win32.UI.Shell;
|
||||||
|
using Windows.Win32.UI.WindowsAndMessaging;
|
||||||
|
|
||||||
|
namespace Peek.FilePreviewer.Controls
|
||||||
|
{
|
||||||
|
[INotifyPropertyChanged]
|
||||||
|
public unsafe sealed partial class ShellPreviewHandlerControl : UserControl
|
||||||
|
{
|
||||||
|
// Mica fallback colors
|
||||||
|
private static readonly COLORREF LightThemeBgColor = new(0x00f3f3f3);
|
||||||
|
private static readonly COLORREF DarkThemeBgColor = new(0x00202020);
|
||||||
|
|
||||||
|
private static readonly HBRUSH LightThemeBgBrush = PInvoke.CreateSolidBrush(LightThemeBgColor);
|
||||||
|
private static readonly HBRUSH DarkThemeBgBrush = PInvoke.CreateSolidBrush(DarkThemeBgColor);
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private IPreviewHandler? source;
|
||||||
|
|
||||||
|
private HWND containerHwnd;
|
||||||
|
private WNDPROC containerWndProc;
|
||||||
|
private HBRUSH containerBgBrush;
|
||||||
|
private RECT controlRect;
|
||||||
|
|
||||||
|
public event EventHandler? HandlerLoaded;
|
||||||
|
|
||||||
|
public event EventHandler? HandlerError;
|
||||||
|
|
||||||
|
public static readonly DependencyProperty HandlerVisibilityProperty = DependencyProperty.Register(
|
||||||
|
nameof(HandlerVisibility),
|
||||||
|
typeof(Visibility),
|
||||||
|
typeof(ShellPreviewHandlerControl),
|
||||||
|
new PropertyMetadata(Visibility.Collapsed, new PropertyChangedCallback((d, e) => ((ShellPreviewHandlerControl)d).OnHandlerVisibilityChanged())));
|
||||||
|
|
||||||
|
// Must have its own visibility property so resize events can still fire
|
||||||
|
public Visibility HandlerVisibility
|
||||||
|
{
|
||||||
|
get { return (Visibility)GetValue(HandlerVisibilityProperty); }
|
||||||
|
set { SetValue(HandlerVisibilityProperty, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public ShellPreviewHandlerControl()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
|
||||||
|
containerWndProc = ContainerWndProc;
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnSourceChanged(IPreviewHandler? value)
|
||||||
|
{
|
||||||
|
if (Source != null)
|
||||||
|
{
|
||||||
|
UpdatePreviewerTheme();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Attach the preview handler to the container window
|
||||||
|
Source.SetWindow(containerHwnd, (RECT*)Unsafe.AsPointer(ref controlRect));
|
||||||
|
Source.DoPreview();
|
||||||
|
|
||||||
|
HandlerLoaded?.Invoke(this, EventArgs.Empty);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
HandlerError?.Invoke(this, EventArgs.Empty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnHandlerVisibilityChanged()
|
||||||
|
{
|
||||||
|
if (HandlerVisibility == Visibility.Visible)
|
||||||
|
{
|
||||||
|
PInvoke.ShowWindow(containerHwnd, SHOW_WINDOW_CMD.SW_SHOW);
|
||||||
|
IsEnabled = true;
|
||||||
|
|
||||||
|
// Clears the background from the last previewer
|
||||||
|
// The brush can only be drawn here because flashes will occur during resize
|
||||||
|
PInvoke.SetClassLongPtr(containerHwnd, GET_CLASS_LONG_INDEX.GCLP_HBRBACKGROUND, containerBgBrush);
|
||||||
|
PInvoke.UpdateWindow(containerHwnd);
|
||||||
|
PInvoke.SetClassLongPtr(containerHwnd, GET_CLASS_LONG_INDEX.GCLP_HBRBACKGROUND, IntPtr.Zero);
|
||||||
|
PInvoke.InvalidateRect(containerHwnd, (RECT*)null, true);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
PInvoke.ShowWindow(containerHwnd, SHOW_WINDOW_CMD.SW_HIDE);
|
||||||
|
IsEnabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdatePreviewerTheme()
|
||||||
|
{
|
||||||
|
COLORREF bgColor, fgColor;
|
||||||
|
switch (ActualTheme)
|
||||||
|
{
|
||||||
|
case ElementTheme.Light:
|
||||||
|
bgColor = LightThemeBgColor;
|
||||||
|
fgColor = new COLORREF(0x00000000); // Black
|
||||||
|
|
||||||
|
containerBgBrush = LightThemeBgBrush;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ElementTheme.Dark:
|
||||||
|
default:
|
||||||
|
bgColor = DarkThemeBgColor;
|
||||||
|
fgColor = new COLORREF(0x00FFFFFF); // White
|
||||||
|
|
||||||
|
containerBgBrush = DarkThemeBgBrush;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Source is IPreviewHandlerVisuals visuals)
|
||||||
|
{
|
||||||
|
visuals.SetBackgroundColor(bgColor);
|
||||||
|
visuals.SetTextColor(fgColor);
|
||||||
|
|
||||||
|
// Changing the previewer colors might not always redraw itself
|
||||||
|
PInvoke.InvalidateRect(containerHwnd, (RECT*)null, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private LRESULT ContainerWndProc(HWND hWnd, uint msg, WPARAM wParam, LPARAM lParam)
|
||||||
|
{
|
||||||
|
// Here for future use :)
|
||||||
|
return PInvoke.DefWindowProc(hWnd, msg, wParam, lParam);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UserControl_Loaded(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
fixed (char* pContainerClassName = "PeekShellPreviewHandlerContainer")
|
||||||
|
{
|
||||||
|
PInvoke.RegisterClass(new WNDCLASSW()
|
||||||
|
{
|
||||||
|
lpfnWndProc = containerWndProc,
|
||||||
|
lpszClassName = pContainerClassName,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create the container window to host the preview handler
|
||||||
|
containerHwnd = PInvoke.CreateWindowEx(
|
||||||
|
WINDOW_EX_STYLE.WS_EX_LAYERED,
|
||||||
|
pContainerClassName,
|
||||||
|
null,
|
||||||
|
WINDOW_STYLE.WS_CHILD,
|
||||||
|
0, // X
|
||||||
|
0, // Y
|
||||||
|
0, // Width
|
||||||
|
0, // Height
|
||||||
|
(HWND)Win32Interop.GetWindowFromWindowId(XamlRoot.ContentIslandEnvironment.AppWindowId), // Peek UI window
|
||||||
|
HMENU.Null,
|
||||||
|
HINSTANCE.Null);
|
||||||
|
|
||||||
|
// Allows the preview handlers to display properly
|
||||||
|
PInvoke.SetLayeredWindowAttributes(containerHwnd, default, byte.MaxValue, LAYERED_WINDOW_ATTRIBUTES_FLAGS.LWA_ALPHA);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UserControl_EffectiveViewportChanged(FrameworkElement sender, EffectiveViewportChangedEventArgs args)
|
||||||
|
{
|
||||||
|
var dpi = (float)PInvoke.GetDpiForWindow(containerHwnd) / 96;
|
||||||
|
|
||||||
|
// Resize the container window
|
||||||
|
PInvoke.SetWindowPos(
|
||||||
|
containerHwnd,
|
||||||
|
(HWND)0, // HWND_TOP
|
||||||
|
(int)(Math.Abs(args.EffectiveViewport.X) * dpi),
|
||||||
|
(int)(Math.Abs(args.EffectiveViewport.Y) * dpi),
|
||||||
|
(int)(ActualWidth * dpi),
|
||||||
|
(int)(ActualHeight * dpi),
|
||||||
|
SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE);
|
||||||
|
|
||||||
|
// Resize the preview handler window
|
||||||
|
controlRect.right = (int)(ActualWidth * dpi);
|
||||||
|
controlRect.bottom = (int)(ActualHeight * dpi);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Source?.SetRect((RECT*)Unsafe.AsPointer(ref controlRect));
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resizing the previewer might not always redraw itself
|
||||||
|
PInvoke.InvalidateRect(containerHwnd, (RECT*)null, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UserControl_GotFocus(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Source?.SetFocus();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,13 @@
|
|||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
IsActive="{x:Bind MatchPreviewState(Previewer.State, previewers:PreviewState.Loading), Mode=OneWay}" />
|
IsActive="{x:Bind MatchPreviewState(Previewer.State, previewers:PreviewState.Loading), Mode=OneWay}" />
|
||||||
|
|
||||||
|
<controls:ShellPreviewHandlerControl
|
||||||
|
x:Name="ShellPreviewHandlerPreview"
|
||||||
|
Source="{x:Bind ShellPreviewHandlerPreviewer.Preview, Mode=OneWay}"
|
||||||
|
HandlerVisibility="{x:Bind IsPreviewVisible(ShellPreviewHandlerPreviewer, Previewer.State), Mode=OneWay}"
|
||||||
|
HandlerLoaded="ShellPreviewHandlerPreview_HandlerLoaded"
|
||||||
|
HandlerError="ShellPreviewHandlerPreview_HandlerError" />
|
||||||
|
|
||||||
<Image
|
<Image
|
||||||
x:Name="ImagePreview"
|
x:Name="ImagePreview"
|
||||||
MaxWidth="{x:Bind ImagePreviewer.MaxImageSize.Width, Mode=OneWay}"
|
MaxWidth="{x:Bind ImagePreviewer.MaxImageSize.Width, Mode=OneWay}"
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ namespace Peek.FilePreviewer
|
|||||||
[NotifyPropertyChangedFor(nameof(VideoPreviewer))]
|
[NotifyPropertyChangedFor(nameof(VideoPreviewer))]
|
||||||
[NotifyPropertyChangedFor(nameof(BrowserPreviewer))]
|
[NotifyPropertyChangedFor(nameof(BrowserPreviewer))]
|
||||||
[NotifyPropertyChangedFor(nameof(ArchivePreviewer))]
|
[NotifyPropertyChangedFor(nameof(ArchivePreviewer))]
|
||||||
|
[NotifyPropertyChangedFor(nameof(ShellPreviewHandlerPreviewer))]
|
||||||
[NotifyPropertyChangedFor(nameof(UnsupportedFilePreviewer))]
|
[NotifyPropertyChangedFor(nameof(UnsupportedFilePreviewer))]
|
||||||
|
|
||||||
private IPreviewer? previewer;
|
private IPreviewer? previewer;
|
||||||
@@ -96,6 +97,8 @@ namespace Peek.FilePreviewer
|
|||||||
|
|
||||||
public IArchivePreviewer? ArchivePreviewer => Previewer as IArchivePreviewer;
|
public IArchivePreviewer? ArchivePreviewer => Previewer as IArchivePreviewer;
|
||||||
|
|
||||||
|
public IShellPreviewHandlerPreviewer? ShellPreviewHandlerPreviewer => Previewer as IShellPreviewHandlerPreviewer;
|
||||||
|
|
||||||
public IUnsupportedFilePreviewer? UnsupportedFilePreviewer => Previewer as IUnsupportedFilePreviewer;
|
public IUnsupportedFilePreviewer? UnsupportedFilePreviewer => Previewer as IUnsupportedFilePreviewer;
|
||||||
|
|
||||||
public IFileSystemItem Item
|
public IFileSystemItem Item
|
||||||
@@ -220,6 +223,9 @@ namespace Peek.FilePreviewer
|
|||||||
ArchivePreview.Source = null;
|
ArchivePreview.Source = null;
|
||||||
BrowserPreview.Source = null;
|
BrowserPreview.Source = null;
|
||||||
|
|
||||||
|
ShellPreviewHandlerPreviewer?.Clear();
|
||||||
|
ShellPreviewHandlerPreview.Source = null;
|
||||||
|
|
||||||
if (Previewer != null)
|
if (Previewer != null)
|
||||||
{
|
{
|
||||||
Previewer.PropertyChanged -= Previewer_PropertyChanged;
|
Previewer.PropertyChanged -= Previewer_PropertyChanged;
|
||||||
@@ -268,6 +274,22 @@ namespace Peek.FilePreviewer
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ShellPreviewHandlerPreview_HandlerLoaded(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (ShellPreviewHandlerPreviewer != null)
|
||||||
|
{
|
||||||
|
ShellPreviewHandlerPreviewer.State = PreviewState.Loaded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShellPreviewHandlerPreview_HandlerError(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (ShellPreviewHandlerPreviewer != null)
|
||||||
|
{
|
||||||
|
ShellPreviewHandlerPreviewer.State = PreviewState.Error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async void KeyboardAccelerator_CtrlC_Invoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs args)
|
private async void KeyboardAccelerator_CtrlC_Invoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs args)
|
||||||
{
|
{
|
||||||
if (Previewer != null)
|
if (Previewer != null)
|
||||||
|
|||||||
4
src/modules/peek/Peek.FilePreviewer/NativeMethods.json
Normal file
4
src/modules/peek/Peek.FilePreviewer/NativeMethods.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://aka.ms/CsWin32.schema.json",
|
||||||
|
"public": false
|
||||||
|
}
|
||||||
14
src/modules/peek/Peek.FilePreviewer/NativeMethods.txt
Normal file
14
src/modules/peek/Peek.FilePreviewer/NativeMethods.txt
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
IClassFactory
|
||||||
|
CoGetClassObject
|
||||||
|
CreateSolidBrush
|
||||||
|
CreateWindowEx
|
||||||
|
DefWindowProc
|
||||||
|
GetDpiForWindow
|
||||||
|
InvalidateRect
|
||||||
|
RegisterClass
|
||||||
|
SetClassLongPtr
|
||||||
|
SetLayeredWindowAttributes
|
||||||
|
SetWindowPos
|
||||||
|
ShowWindow
|
||||||
|
UpdateWindow
|
||||||
|
SHCreateItemFromParsingName
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
<RuntimeIdentifiers>win10-x86;win10-x64;win10-arm64</RuntimeIdentifiers>
|
<RuntimeIdentifiers>win10-x86;win10-x64;win10-arm64</RuntimeIdentifiers>
|
||||||
<UseWinUI>true</UseWinUI>
|
<UseWinUI>true</UseWinUI>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PRIResource Include="..\Peek.UI\Strings\en-US\Resources.resw" Link="Strings\en-US\Resources.resw">
|
<PRIResource Include="..\Peek.UI\Strings\en-US\Resources.resw" Link="Strings\en-US\Resources.resw">
|
||||||
@@ -17,6 +18,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Remove="Controls\ArchiveControl.xaml" />
|
<None Remove="Controls\ArchiveControl.xaml" />
|
||||||
<None Remove="Controls\BrowserControl.xaml" />
|
<None Remove="Controls\BrowserControl.xaml" />
|
||||||
|
<None Remove="Controls\ShellPreviewHandlerControl.xaml" />
|
||||||
<None Remove="Controls\UnsupportedFilePreview\FailedFallbackPreviewControl.xaml" />
|
<None Remove="Controls\UnsupportedFilePreview\FailedFallbackPreviewControl.xaml" />
|
||||||
<None Remove="Controls\UnsupportedFilePreview\InformationalPreviewControl.xaml" />
|
<None Remove="Controls\UnsupportedFilePreview\InformationalPreviewControl.xaml" />
|
||||||
<None Remove="FilePreview.xaml" />
|
<None Remove="FilePreview.xaml" />
|
||||||
@@ -29,6 +31,10 @@
|
|||||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" />
|
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" />
|
||||||
<PackageReference Include="SharpCompress" />
|
<PackageReference Include="SharpCompress" />
|
||||||
<PackageReference Include="System.Drawing.Common" />
|
<PackageReference Include="System.Drawing.Common" />
|
||||||
|
<PackageReference Include="Microsoft.Windows.CsWin32">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -38,6 +44,12 @@
|
|||||||
<ProjectReference Include="..\Peek.Common\Peek.Common.csproj" />
|
<ProjectReference Include="..\Peek.Common\Peek.Common.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Page Update="Controls\ShellPreviewHandlerControl.xaml">
|
||||||
|
<Generator>MSBuild:Compile</Generator>
|
||||||
|
</Page>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Page Update="Controls\ArchiveControl.xaml">
|
<Page Update="Controls\ArchiveControl.xaml">
|
||||||
<Generator>MSBuild:Compile</Generator>
|
<Generator>MSBuild:Compile</Generator>
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
using Windows.Win32.UI.Shell;
|
||||||
|
|
||||||
|
namespace Peek.FilePreviewer.Previewers
|
||||||
|
{
|
||||||
|
public interface IShellPreviewHandlerPreviewer : IPreviewer
|
||||||
|
{
|
||||||
|
public IPreviewHandler? Preview { get; }
|
||||||
|
|
||||||
|
public void Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,6 +29,10 @@ namespace Peek.FilePreviewer.Previewers
|
|||||||
{
|
{
|
||||||
return new ArchivePreviewer(file);
|
return new ArchivePreviewer(file);
|
||||||
}
|
}
|
||||||
|
else if (ShellPreviewHandlerPreviewer.IsFileTypeSupported(file.Extension))
|
||||||
|
{
|
||||||
|
return new ShellPreviewHandlerPreviewer(file);
|
||||||
|
}
|
||||||
|
|
||||||
// Other previewer types check their supported file types here
|
// Other previewer types check their supported file types here
|
||||||
return CreateDefaultPreviewer(file);
|
return CreateDefaultPreviewer(file);
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using Windows.Win32.Foundation;
|
||||||
|
using Windows.Win32.System.Com;
|
||||||
|
|
||||||
|
namespace Peek.FilePreviewer.Previewers.Helpers
|
||||||
|
{
|
||||||
|
public unsafe class IStreamWrapper : IStream
|
||||||
|
{
|
||||||
|
public Stream Stream { get; }
|
||||||
|
|
||||||
|
public IStreamWrapper(Stream stream) => Stream = stream;
|
||||||
|
|
||||||
|
public HRESULT Read(void* pv, uint cb, [Optional] uint* pcbRead)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
int read = Stream.Read(new Span<byte>(pv, (int)cb));
|
||||||
|
if (pcbRead != null)
|
||||||
|
{
|
||||||
|
*pcbRead = (uint)read;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (HRESULT)0;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return (HRESULT)Marshal.GetHRForException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public HRESULT Write(void* pv, uint cb, [Optional] uint* pcbWritten)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Stream.Write(new ReadOnlySpan<byte>(pv, (int)cb));
|
||||||
|
if (pcbWritten != null)
|
||||||
|
{
|
||||||
|
*pcbWritten = cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (HRESULT)0;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return (HRESULT)Marshal.GetHRForException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Seek(long dlibMove, STREAM_SEEK dwOrigin, [Optional] ulong* plibNewPosition)
|
||||||
|
{
|
||||||
|
long position = Stream.Seek(dlibMove, (SeekOrigin)dwOrigin);
|
||||||
|
if (plibNewPosition != null)
|
||||||
|
{
|
||||||
|
*plibNewPosition = (ulong)position;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetSize(ulong libNewSize)
|
||||||
|
{
|
||||||
|
Stream.SetLength((long)libNewSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CopyTo(IStream pstm, ulong cb, [Optional] ulong* pcbRead, [Optional] ulong* pcbWritten)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Commit(STGC grfCommitFlags)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Revert()
|
||||||
|
{
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void LockRegion(ulong libOffset, ulong cb, uint dwLockType)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UnlockRegion(ulong libOffset, ulong cb, uint dwLockType)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Stat(STATSTG* pstatstg, uint grfStatFlag)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Clone(out IStream ppstm)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,237 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.IO;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using Microsoft.UI.Dispatching;
|
||||||
|
using Microsoft.Win32;
|
||||||
|
using Peek.Common.Extensions;
|
||||||
|
using Peek.Common.Helpers;
|
||||||
|
using Peek.Common.Models;
|
||||||
|
using Peek.FilePreviewer.Models;
|
||||||
|
using Peek.FilePreviewer.Previewers.Helpers;
|
||||||
|
using Windows.Win32;
|
||||||
|
using Windows.Win32.System.Com;
|
||||||
|
using Windows.Win32.UI.Shell;
|
||||||
|
using Windows.Win32.UI.Shell.PropertiesSystem;
|
||||||
|
using IShellItem = Windows.Win32.UI.Shell.IShellItem;
|
||||||
|
|
||||||
|
namespace Peek.FilePreviewer.Previewers
|
||||||
|
{
|
||||||
|
public partial class ShellPreviewHandlerPreviewer : ObservableObject, IShellPreviewHandlerPreviewer, IDisposable
|
||||||
|
{
|
||||||
|
private static readonly ConcurrentDictionary<Guid, IClassFactory> HandlerFactories = new();
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private IPreviewHandler? preview;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private PreviewState state;
|
||||||
|
|
||||||
|
private Stream? fileStream;
|
||||||
|
|
||||||
|
public ShellPreviewHandlerPreviewer(IFileSystemItem file)
|
||||||
|
{
|
||||||
|
FileItem = file;
|
||||||
|
Dispatcher = DispatcherQueue.GetForCurrentThread();
|
||||||
|
}
|
||||||
|
|
||||||
|
private IFileSystemItem FileItem { get; }
|
||||||
|
|
||||||
|
private DispatcherQueue Dispatcher { get; }
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Clear();
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task CopyAsync()
|
||||||
|
{
|
||||||
|
await Dispatcher.RunOnUiThread(async () =>
|
||||||
|
{
|
||||||
|
var storageItem = await FileItem.GetStorageItemAsync();
|
||||||
|
ClipboardHelper.SaveToClipboard(storageItem);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<PreviewSize> GetPreviewSizeAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return Task.FromResult(new PreviewSize { MonitorSize = null });
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task LoadPreviewAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
Clear();
|
||||||
|
State = PreviewState.Loading;
|
||||||
|
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
// Create the preview handler
|
||||||
|
var previewHandler = await Task.Run(() =>
|
||||||
|
{
|
||||||
|
var previewHandlerGuid = GetPreviewHandlerGuid(FileItem.Extension);
|
||||||
|
if (!string.IsNullOrEmpty(previewHandlerGuid))
|
||||||
|
{
|
||||||
|
var clsid = Guid.Parse(previewHandlerGuid);
|
||||||
|
|
||||||
|
bool retry = false;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
unsafe
|
||||||
|
{
|
||||||
|
// This runs the preview handler in a separate process (prevhost.exe)
|
||||||
|
// TODO: Figure out how to get it to run in a low integrity level
|
||||||
|
if (!HandlerFactories.TryGetValue(clsid, out var factory))
|
||||||
|
{
|
||||||
|
var hr = PInvoke.CoGetClassObject(clsid, CLSCTX.CLSCTX_LOCAL_SERVER, null, typeof(IClassFactory).GUID, out var pFactory);
|
||||||
|
Marshal.ThrowExceptionForHR(hr);
|
||||||
|
|
||||||
|
// Storing the factory in memory helps makes the handlers load faster
|
||||||
|
// TODO: Maybe free them after some inactivity or when Peek quits?
|
||||||
|
factory = (IClassFactory)Marshal.GetObjectForIUnknown((IntPtr)pFactory);
|
||||||
|
factory.LockServer(true);
|
||||||
|
HandlerFactories.AddOrUpdate(clsid, factory, (_, _) => factory);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var iid = typeof(IPreviewHandler).GUID;
|
||||||
|
factory.CreateInstance(null, &iid, out var instance);
|
||||||
|
return instance as IPreviewHandler;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
if (!retry)
|
||||||
|
{
|
||||||
|
// Process is probably dead, attempt to get the factory again (once)
|
||||||
|
HandlerFactories.TryRemove(new(clsid, factory));
|
||||||
|
retry = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (retry);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (previewHandler == null)
|
||||||
|
{
|
||||||
|
State = PreviewState.Error;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
// Initialize the preview handler with the selected file
|
||||||
|
bool success = await Task.Run(() =>
|
||||||
|
{
|
||||||
|
const uint STGM_READ = 0x00000000;
|
||||||
|
if (previewHandler is IInitializeWithStream initWithStream)
|
||||||
|
{
|
||||||
|
fileStream = File.OpenRead(FileItem.Path);
|
||||||
|
initWithStream.Initialize(new IStreamWrapper(fileStream), STGM_READ);
|
||||||
|
}
|
||||||
|
else if (previewHandler is IInitializeWithItem initWithItem)
|
||||||
|
{
|
||||||
|
var hr = PInvoke.SHCreateItemFromParsingName(FileItem.Path, null, typeof(IShellItem).GUID, out var item);
|
||||||
|
Marshal.ThrowExceptionForHR(hr);
|
||||||
|
|
||||||
|
initWithItem.Initialize((IShellItem)item, STGM_READ);
|
||||||
|
}
|
||||||
|
else if (previewHandler is IInitializeWithFile initWithFile)
|
||||||
|
{
|
||||||
|
unsafe
|
||||||
|
{
|
||||||
|
fixed (char* pPath = FileItem.Path)
|
||||||
|
{
|
||||||
|
initWithFile.Initialize(pPath, STGM_READ);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Handler is missing the required interfaces
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!success)
|
||||||
|
{
|
||||||
|
State = PreviewState.Error;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
// Preview.SetWindow() needs to be set in the control
|
||||||
|
Preview = previewHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
if (Preview != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Preview.Unload();
|
||||||
|
Marshal.FinalReleaseComObject(Preview);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
Preview = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileStream != null)
|
||||||
|
{
|
||||||
|
fileStream.Dispose();
|
||||||
|
fileStream = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsFileTypeSupported(string fileExt)
|
||||||
|
{
|
||||||
|
return !string.IsNullOrEmpty(GetPreviewHandlerGuid(fileExt));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? GetPreviewHandlerGuid(string fileExt)
|
||||||
|
{
|
||||||
|
const string PreviewHandlerKeyPath = "shellex\\{8895b1c6-b41f-4c1c-a562-0d564250836f}";
|
||||||
|
|
||||||
|
// Search by file extension
|
||||||
|
using var classExtensionKey = Registry.ClassesRoot.OpenSubKey(fileExt);
|
||||||
|
using var classExtensionPreviewHandlerKey = classExtensionKey?.OpenSubKey(PreviewHandlerKeyPath);
|
||||||
|
|
||||||
|
if (classExtensionKey != null && classExtensionPreviewHandlerKey == null)
|
||||||
|
{
|
||||||
|
// Search by file class
|
||||||
|
var className = classExtensionKey.GetValue(null) as string;
|
||||||
|
if (!string.IsNullOrEmpty(className))
|
||||||
|
{
|
||||||
|
using var classKey = Registry.ClassesRoot.OpenSubKey(className);
|
||||||
|
using var classPreviewHandlerKey = classKey?.OpenSubKey(PreviewHandlerKeyPath);
|
||||||
|
|
||||||
|
return classPreviewHandlerKey?.GetValue(null) as string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return classExtensionPreviewHandlerKey?.GetValue(null) as string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user