From ddeeddaca8feab0273356afc2703b4c75a398ebe Mon Sep 17 00:00:00 2001 From: Dylan Date: Tue, 19 Sep 2023 10:36:58 -0700 Subject: [PATCH 1/9] Add support for preview handlers --- .../peek/Peek.Common/NativeMethods.txt | 6 +- .../Controls/ShellPreviewHandlerControl.xaml | 17 ++ .../ShellPreviewHandlerControl.xaml.cs | 178 +++++++++++++++++ .../peek/Peek.FilePreviewer/FilePreview.xaml | 7 +- .../Peek.FilePreviewer/FilePreview.xaml.cs | 6 + .../Peek.FilePreviewer/NativeMethods.json | 4 + .../peek/Peek.FilePreviewer/NativeMethods.txt | 7 + .../Peek.FilePreviewer.csproj | 12 ++ .../IShellPreviewHandlerPreviewer.cs | 15 ++ .../Previewers/PreviewerFactory.cs | 4 + .../Helpers/IStreamWrapper.cs | 104 ++++++++++ .../ShellPreviewHandlerPreviewer.cs | 185 ++++++++++++++++++ 12 files changed, 543 insertions(+), 2 deletions(-) create mode 100644 src/modules/peek/Peek.FilePreviewer/Controls/ShellPreviewHandlerControl.xaml create mode 100644 src/modules/peek/Peek.FilePreviewer/Controls/ShellPreviewHandlerControl.xaml.cs create mode 100644 src/modules/peek/Peek.FilePreviewer/NativeMethods.json create mode 100644 src/modules/peek/Peek.FilePreviewer/NativeMethods.txt create mode 100644 src/modules/peek/Peek.FilePreviewer/Previewers/Interfaces/IShellPreviewHandlerPreviewer.cs create mode 100644 src/modules/peek/Peek.FilePreviewer/Previewers/ShellPreviewHandlerPreviewer/Helpers/IStreamWrapper.cs create mode 100644 src/modules/peek/Peek.FilePreviewer/Previewers/ShellPreviewHandlerPreviewer/ShellPreviewHandlerPreviewer.cs diff --git a/src/modules/peek/Peek.Common/NativeMethods.txt b/src/modules/peek/Peek.Common/NativeMethods.txt index c5ac7c039947..63dc3ff9d6a2 100644 --- a/src/modules/peek/Peek.Common/NativeMethods.txt +++ b/src/modules/peek/Peek.Common/NativeMethods.txt @@ -2,4 +2,8 @@ _SHCONTF SIGDN SHGDNF -SIATTRIBFLAGS \ No newline at end of file +SIATTRIBFLAGS +IInitializeWithFile +IInitializeWithStream +IPreviewHandler +IPreviewHandlerVisuals \ No newline at end of file diff --git a/src/modules/peek/Peek.FilePreviewer/Controls/ShellPreviewHandlerControl.xaml b/src/modules/peek/Peek.FilePreviewer/Controls/ShellPreviewHandlerControl.xaml new file mode 100644 index 000000000000..b68595d640c0 --- /dev/null +++ b/src/modules/peek/Peek.FilePreviewer/Controls/ShellPreviewHandlerControl.xaml @@ -0,0 +1,17 @@ + + + + + + diff --git a/src/modules/peek/Peek.FilePreviewer/Controls/ShellPreviewHandlerControl.xaml.cs b/src/modules/peek/Peek.FilePreviewer/Controls/ShellPreviewHandlerControl.xaml.cs new file mode 100644 index 000000000000..8168a012ab73 --- /dev/null +++ b/src/modules/peek/Peek.FilePreviewer/Controls/ShellPreviewHandlerControl.xaml.cs @@ -0,0 +1,178 @@ +// 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 Microsoft.UI; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Peek.FilePreviewer.Previewers; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.UI.Shell; +using Windows.Win32.UI.WindowsAndMessaging; + +namespace Peek.FilePreviewer.Controls +{ + public unsafe sealed partial class ShellPreviewHandlerControl : UserControl + { + // Making this into a DependencyProperty causes a InvalidCastException + private IPreviewHandler? _source; + + private HWND containerHwnd; + private WNDPROC containerWndProc; + private RECT controlRect; + + public static readonly DependencyProperty LoadingStateProperty = DependencyProperty.Register( + nameof(LoadingState), + typeof(PreviewState), + typeof(ShellPreviewHandlerControl), + new PropertyMetadata(PreviewState.Uninitialized)); + + public PreviewState? LoadingState + { + get { return (PreviewState)GetValue(LoadingStateProperty); } + set { SetValue(LoadingStateProperty, value); } + } + + public IPreviewHandler? Source + { + get => _source; + set + { + _source = value; + SourcePropertyChanged(); + } + } + + public ShellPreviewHandlerControl() + { + InitializeComponent(); + + containerWndProc = ContainerWndProc; + } + + private void SourcePropertyChanged() + { + if (Source != null) + { + UpdatePreviewerTheme(); + + try + { + // Attach the preview handler to the container window + Source.SetWindow(containerHwnd, (RECT*)Unsafe.AsPointer(ref controlRect)); + Source.DoPreview(); + + PInvoke.ShowWindow(containerHwnd, SHOW_WINDOW_CMD.SW_SHOW); + } + catch + { + } + } + else + { + PInvoke.ShowWindow(containerHwnd, SHOW_WINDOW_CMD.SW_HIDE); + } + } + + private void UpdatePreviewerTheme() + { + if (Source is IPreviewHandlerVisuals visuals) + { + try + { + switch (ActualTheme) + { + case ElementTheme.Light: + visuals.SetBackgroundColor(new COLORREF(0x00f3f3f3)); + visuals.SetTextColor(new COLORREF(0x00000000)); + break; + + case ElementTheme.Dark: + visuals.SetBackgroundColor(new COLORREF(0x00202020)); + visuals.SetTextColor(new COLORREF(0x00FFFFFF)); + break; + } + + // Changing the previewer colors might not always redraw itself + PInvoke.InvalidateRect(containerHwnd, (RECT*)null, true); + } + catch + { + } + } + } + + 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) + { + // Resize the container window + PInvoke.SetWindowPos( + containerHwnd, + (HWND)0, // HWND_TOP + (int)Math.Abs(args.EffectiveViewport.X), + (int)Math.Abs(args.EffectiveViewport.Y), + (int)ActualWidth, + (int)ActualHeight, + SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE); + + // Resize the preview handler window + controlRect.right = (int)ActualWidth; + controlRect.bottom = (int)ActualHeight; + try + { + Source?.SetRect((RECT*)Unsafe.AsPointer(ref controlRect)); + } + catch + { + } + } + + private void UserControl_GotFocus(object sender, RoutedEventArgs e) + { + try + { + Source?.SetFocus(); + } + catch + { + } + } + } +} diff --git a/src/modules/peek/Peek.FilePreviewer/FilePreview.xaml b/src/modules/peek/Peek.FilePreviewer/FilePreview.xaml index 9d1539cdd944..691071dd95eb 100644 --- a/src/modules/peek/Peek.FilePreviewer/FilePreview.xaml +++ b/src/modules/peek/Peek.FilePreviewer/FilePreview.xaml @@ -18,6 +18,11 @@ VerticalAlignment="Center" IsActive="{x:Bind MatchPreviewState(Previewer.State, previewers:PreviewState.Loading), Mode=OneWay}" /> + + - + Previewer as IArchivePreviewer; + public IShellPreviewHandlerPreviewer? ShellPreviewHandlerPreviewer => Previewer as IShellPreviewHandlerPreviewer; + public IUnsupportedFilePreviewer? UnsupportedFilePreviewer => Previewer as IUnsupportedFilePreviewer; public IFileSystemItem Item @@ -220,6 +223,9 @@ partial void OnPreviewerChanging(IPreviewer? value) ArchivePreview.Source = null; BrowserPreview.Source = null; + ShellPreviewHandlerPreviewer?.Clear(); + ShellPreviewHandlerPreview.Source = null; + if (Previewer != null) { Previewer.PropertyChanged -= Previewer_PropertyChanged; diff --git a/src/modules/peek/Peek.FilePreviewer/NativeMethods.json b/src/modules/peek/Peek.FilePreviewer/NativeMethods.json new file mode 100644 index 000000000000..dc43b5886133 --- /dev/null +++ b/src/modules/peek/Peek.FilePreviewer/NativeMethods.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://aka.ms/CsWin32.schema.json", + "public": false +} \ No newline at end of file diff --git a/src/modules/peek/Peek.FilePreviewer/NativeMethods.txt b/src/modules/peek/Peek.FilePreviewer/NativeMethods.txt new file mode 100644 index 000000000000..fba1dab6bd8e --- /dev/null +++ b/src/modules/peek/Peek.FilePreviewer/NativeMethods.txt @@ -0,0 +1,7 @@ +CreateWindowEx +DefWindowProc +InvalidateRect +RegisterClass +SetLayeredWindowAttributes +SetWindowPos +ShowWindow \ No newline at end of file diff --git a/src/modules/peek/Peek.FilePreviewer/Peek.FilePreviewer.csproj b/src/modules/peek/Peek.FilePreviewer/Peek.FilePreviewer.csproj index 02eb4c4dc39f..1a8832a4e0e7 100644 --- a/src/modules/peek/Peek.FilePreviewer/Peek.FilePreviewer.csproj +++ b/src/modules/peek/Peek.FilePreviewer/Peek.FilePreviewer.csproj @@ -8,6 +8,7 @@ win10-x86;win10-x64;win10-arm64 true enable + true @@ -17,6 +18,7 @@ + @@ -29,6 +31,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + @@ -38,6 +44,12 @@ + + + MSBuild:Compile + + + MSBuild:Compile diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/Interfaces/IShellPreviewHandlerPreviewer.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/Interfaces/IShellPreviewHandlerPreviewer.cs new file mode 100644 index 000000000000..3d71ea926a24 --- /dev/null +++ b/src/modules/peek/Peek.FilePreviewer/Previewers/Interfaces/IShellPreviewHandlerPreviewer.cs @@ -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(); + } +} diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/PreviewerFactory.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/PreviewerFactory.cs index 3ac9ceeceec7..8a505d9f9cf5 100644 --- a/src/modules/peek/Peek.FilePreviewer/Previewers/PreviewerFactory.cs +++ b/src/modules/peek/Peek.FilePreviewer/Previewers/PreviewerFactory.cs @@ -29,6 +29,10 @@ public IPreviewer Create(IFileSystemItem file) { return new ArchivePreviewer(file); } + else if (ShellPreviewHandlerPreviewer.IsFileTypeSupported(file.Extension)) + { + return new ShellPreviewHandlerPreviewer(file); + } // Other previewer types check their supported file types here return CreateDefaultPreviewer(file); diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/ShellPreviewHandlerPreviewer/Helpers/IStreamWrapper.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/ShellPreviewHandlerPreviewer/Helpers/IStreamWrapper.cs new file mode 100644 index 000000000000..786bd0d3b81d --- /dev/null +++ b/src/modules/peek/Peek.FilePreviewer/Previewers/ShellPreviewHandlerPreviewer/Helpers/IStreamWrapper.cs @@ -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(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(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(); + } + } +} diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/ShellPreviewHandlerPreviewer/ShellPreviewHandlerPreviewer.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/ShellPreviewHandlerPreviewer/ShellPreviewHandlerPreviewer.cs new file mode 100644 index 000000000000..e7414d0f3548 --- /dev/null +++ b/src/modules/peek/Peek.FilePreviewer/Previewers/ShellPreviewHandlerPreviewer/ShellPreviewHandlerPreviewer.cs @@ -0,0 +1,185 @@ +// 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 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.UI.Shell; +using Windows.Win32.UI.Shell.PropertiesSystem; + +namespace Peek.FilePreviewer.Previewers +{ + public partial class ShellPreviewHandlerPreviewer : ObservableObject, IShellPreviewHandlerPreviewer, IDisposable + { + [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 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)) + { + return Activator.CreateInstance(Type.GetTypeFromCLSID(Guid.Parse(previewHandlerGuid))!) as IPreviewHandler; + } + else + { + 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 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; + State = PreviewState.Loaded; + } + + 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; + } + } +} From 983e67b32fbd020452bcbe533e4302fecba10e2a Mon Sep 17 00:00:00 2001 From: Dylan Briedis Date: Thu, 21 Sep 2023 20:04:20 +0000 Subject: [PATCH 2/9] Fix spelling --- .github/actions/spell-check/expect.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index c5b83b656db5..6d7695fdb189 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -1430,6 +1430,7 @@ ppsi ppsid ppsrm ppsrree +ppstm ppsz pptal ppv @@ -1472,6 +1473,8 @@ psfi Psr psrm psrree +pstatstg +pstm pstr pstream pstrm @@ -1826,6 +1829,7 @@ STDMETHODCALLTYPE STDMETHODIMP stefan Stereolithography +STGC STGM STGMEDIUM sticpl @@ -2191,6 +2195,7 @@ wnd WNDCLASS WNDCLASSEX WNDCLASSEXW +WNDCLASSW WNDPROC wordpad workaround From 2b41dab00f5f987adc3c13c2a923c3c0312db78c Mon Sep 17 00:00:00 2001 From: Dylan Date: Thu, 21 Sep 2023 13:04:45 -0700 Subject: [PATCH 3/9] Fix DPI resizing and redraw --- .../Controls/ShellPreviewHandlerControl.xaml.cs | 17 +++++++++++------ .../peek/Peek.FilePreviewer/NativeMethods.txt | 1 + 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/modules/peek/Peek.FilePreviewer/Controls/ShellPreviewHandlerControl.xaml.cs b/src/modules/peek/Peek.FilePreviewer/Controls/ShellPreviewHandlerControl.xaml.cs index 8168a012ab73..06309096b510 100644 --- a/src/modules/peek/Peek.FilePreviewer/Controls/ShellPreviewHandlerControl.xaml.cs +++ b/src/modules/peek/Peek.FilePreviewer/Controls/ShellPreviewHandlerControl.xaml.cs @@ -142,19 +142,21 @@ private void UserControl_Loaded(object sender, RoutedEventArgs e) 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), - (int)Math.Abs(args.EffectiveViewport.Y), - (int)ActualWidth, - (int)ActualHeight, + (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; - controlRect.bottom = (int)ActualHeight; + controlRect.right = (int)(ActualWidth * dpi); + controlRect.bottom = (int)(ActualHeight * dpi); try { Source?.SetRect((RECT*)Unsafe.AsPointer(ref controlRect)); @@ -162,6 +164,9 @@ private void UserControl_EffectiveViewportChanged(FrameworkElement sender, Effec catch { } + + // Resizing the previewer might not always redraw itself + PInvoke.InvalidateRect(containerHwnd, (RECT*)null, false); } private void UserControl_GotFocus(object sender, RoutedEventArgs e) diff --git a/src/modules/peek/Peek.FilePreviewer/NativeMethods.txt b/src/modules/peek/Peek.FilePreviewer/NativeMethods.txt index fba1dab6bd8e..434ee9f167d0 100644 --- a/src/modules/peek/Peek.FilePreviewer/NativeMethods.txt +++ b/src/modules/peek/Peek.FilePreviewer/NativeMethods.txt @@ -1,5 +1,6 @@ CreateWindowEx DefWindowProc +GetDpiForWindow InvalidateRect RegisterClass SetLayeredWindowAttributes From 2502c5679928f5cdbabea66540cb4bc3be7ce80c Mon Sep 17 00:00:00 2001 From: Dylan Date: Thu, 21 Sep 2023 16:13:14 -0700 Subject: [PATCH 4/9] Make source into an ObservableProperty --- .../ShellPreviewHandlerControl.xaml.cs | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/src/modules/peek/Peek.FilePreviewer/Controls/ShellPreviewHandlerControl.xaml.cs b/src/modules/peek/Peek.FilePreviewer/Controls/ShellPreviewHandlerControl.xaml.cs index 06309096b510..56627ff8dbd8 100644 --- a/src/modules/peek/Peek.FilePreviewer/Controls/ShellPreviewHandlerControl.xaml.cs +++ b/src/modules/peek/Peek.FilePreviewer/Controls/ShellPreviewHandlerControl.xaml.cs @@ -4,6 +4,7 @@ using System; using System.Runtime.CompilerServices; +using CommunityToolkit.Mvvm.ComponentModel; using Microsoft.UI; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; @@ -15,10 +16,11 @@ namespace Peek.FilePreviewer.Controls { + [INotifyPropertyChanged] public unsafe sealed partial class ShellPreviewHandlerControl : UserControl { - // Making this into a DependencyProperty causes a InvalidCastException - private IPreviewHandler? _source; + [ObservableProperty] + private IPreviewHandler? source; private HWND containerHwnd; private WNDPROC containerWndProc; @@ -36,16 +38,6 @@ public PreviewState? LoadingState set { SetValue(LoadingStateProperty, value); } } - public IPreviewHandler? Source - { - get => _source; - set - { - _source = value; - SourcePropertyChanged(); - } - } - public ShellPreviewHandlerControl() { InitializeComponent(); @@ -53,7 +45,7 @@ public ShellPreviewHandlerControl() containerWndProc = ContainerWndProc; } - private void SourcePropertyChanged() + partial void OnSourceChanged(IPreviewHandler? value) { if (Source != null) { From ea9b5058570ece5e3d78e869ef14463955026e48 Mon Sep 17 00:00:00 2001 From: Dylan Date: Thu, 21 Sep 2023 17:30:28 -0700 Subject: [PATCH 5/9] Add handler visibility property --- .../Controls/ShellPreviewHandlerControl.xaml | 2 +- .../ShellPreviewHandlerControl.xaml.cs | 25 +++++++++++++++++-- .../peek/Peek.FilePreviewer/FilePreview.xaml | 3 ++- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/modules/peek/Peek.FilePreviewer/Controls/ShellPreviewHandlerControl.xaml b/src/modules/peek/Peek.FilePreviewer/Controls/ShellPreviewHandlerControl.xaml index b68595d640c0..b67292ec4b4f 100644 --- a/src/modules/peek/Peek.FilePreviewer/Controls/ShellPreviewHandlerControl.xaml +++ b/src/modules/peek/Peek.FilePreviewer/Controls/ShellPreviewHandlerControl.xaml @@ -11,7 +11,7 @@ mc:Ignorable="d" Loaded="UserControl_Loaded" EffectiveViewportChanged="UserControl_EffectiveViewportChanged" - IsTabStop="True" GotFocus="UserControl_GotFocus" + IsEnabled="False" IsTabStop="True" GotFocus="UserControl_GotFocus" ActualThemeChanged="{x:Bind UpdatePreviewerTheme}"> diff --git a/src/modules/peek/Peek.FilePreviewer/Controls/ShellPreviewHandlerControl.xaml.cs b/src/modules/peek/Peek.FilePreviewer/Controls/ShellPreviewHandlerControl.xaml.cs index 56627ff8dbd8..9734fbc8036a 100644 --- a/src/modules/peek/Peek.FilePreviewer/Controls/ShellPreviewHandlerControl.xaml.cs +++ b/src/modules/peek/Peek.FilePreviewer/Controls/ShellPreviewHandlerControl.xaml.cs @@ -32,12 +32,25 @@ public unsafe sealed partial class ShellPreviewHandlerControl : UserControl typeof(ShellPreviewHandlerControl), new PropertyMetadata(PreviewState.Uninitialized)); + public static readonly DependencyProperty HandlerVisibilityProperty = DependencyProperty.Register( + nameof(HandlerVisibility), + typeof(Visibility), + typeof(ShellPreviewHandlerControl), + new PropertyMetadata(Visibility.Collapsed, new PropertyChangedCallback((d, e) => ((ShellPreviewHandlerControl)d).OnHandlerVisibilityChanged()))); + public PreviewState? LoadingState { get { return (PreviewState)GetValue(LoadingStateProperty); } set { SetValue(LoadingStateProperty, value); } } + // 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(); @@ -56,16 +69,24 @@ partial void OnSourceChanged(IPreviewHandler? value) // Attach the preview handler to the container window Source.SetWindow(containerHwnd, (RECT*)Unsafe.AsPointer(ref controlRect)); Source.DoPreview(); - - PInvoke.ShowWindow(containerHwnd, SHOW_WINDOW_CMD.SW_SHOW); } catch { } } + } + + private void OnHandlerVisibilityChanged() + { + if (HandlerVisibility == Visibility.Visible) + { + PInvoke.ShowWindow(containerHwnd, SHOW_WINDOW_CMD.SW_SHOW); + IsEnabled = true; + } else { PInvoke.ShowWindow(containerHwnd, SHOW_WINDOW_CMD.SW_HIDE); + IsEnabled = false; } } diff --git a/src/modules/peek/Peek.FilePreviewer/FilePreview.xaml b/src/modules/peek/Peek.FilePreviewer/FilePreview.xaml index 691071dd95eb..489247bf1cb7 100644 --- a/src/modules/peek/Peek.FilePreviewer/FilePreview.xaml +++ b/src/modules/peek/Peek.FilePreviewer/FilePreview.xaml @@ -21,7 +21,8 @@ + Source="{x:Bind ShellPreviewHandlerPreviewer.Preview, Mode=OneWay}" + HandlerVisibility="{x:Bind IsPreviewVisible(ShellPreviewHandlerPreviewer, Previewer.State), Mode=OneWay}" /> Date: Sun, 24 Sep 2023 15:30:40 -0700 Subject: [PATCH 6/9] Better error handling --- .../ShellPreviewHandlerControl.xaml.cs | 18 ++++++------------ .../peek/Peek.FilePreviewer/FilePreview.xaml | 5 +++-- .../Peek.FilePreviewer/FilePreview.xaml.cs | 16 ++++++++++++++++ .../ShellPreviewHandlerPreviewer.cs | 1 - 4 files changed, 25 insertions(+), 15 deletions(-) diff --git a/src/modules/peek/Peek.FilePreviewer/Controls/ShellPreviewHandlerControl.xaml.cs b/src/modules/peek/Peek.FilePreviewer/Controls/ShellPreviewHandlerControl.xaml.cs index 9734fbc8036a..9f98d16f9904 100644 --- a/src/modules/peek/Peek.FilePreviewer/Controls/ShellPreviewHandlerControl.xaml.cs +++ b/src/modules/peek/Peek.FilePreviewer/Controls/ShellPreviewHandlerControl.xaml.cs @@ -8,7 +8,6 @@ using Microsoft.UI; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; -using Peek.FilePreviewer.Previewers; using Windows.Win32; using Windows.Win32.Foundation; using Windows.Win32.UI.Shell; @@ -26,11 +25,9 @@ public unsafe sealed partial class ShellPreviewHandlerControl : UserControl private WNDPROC containerWndProc; private RECT controlRect; - public static readonly DependencyProperty LoadingStateProperty = DependencyProperty.Register( - nameof(LoadingState), - typeof(PreviewState), - typeof(ShellPreviewHandlerControl), - new PropertyMetadata(PreviewState.Uninitialized)); + public event EventHandler? HandlerLoaded; + + public event EventHandler? HandlerError; public static readonly DependencyProperty HandlerVisibilityProperty = DependencyProperty.Register( nameof(HandlerVisibility), @@ -38,12 +35,6 @@ public unsafe sealed partial class ShellPreviewHandlerControl : UserControl typeof(ShellPreviewHandlerControl), new PropertyMetadata(Visibility.Collapsed, new PropertyChangedCallback((d, e) => ((ShellPreviewHandlerControl)d).OnHandlerVisibilityChanged()))); - public PreviewState? LoadingState - { - get { return (PreviewState)GetValue(LoadingStateProperty); } - set { SetValue(LoadingStateProperty, value); } - } - // Must have its own visibility property so resize events can still fire public Visibility HandlerVisibility { @@ -69,9 +60,12 @@ partial void OnSourceChanged(IPreviewHandler? value) // 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); } } } diff --git a/src/modules/peek/Peek.FilePreviewer/FilePreview.xaml b/src/modules/peek/Peek.FilePreviewer/FilePreview.xaml index 489247bf1cb7..12ba4874b5d2 100644 --- a/src/modules/peek/Peek.FilePreviewer/FilePreview.xaml +++ b/src/modules/peek/Peek.FilePreviewer/FilePreview.xaml @@ -20,9 +20,10 @@ + HandlerVisibility="{x:Bind IsPreviewVisible(ShellPreviewHandlerPreviewer, Previewer.State), Mode=OneWay}" + HandlerLoaded="ShellPreviewHandlerPreview_HandlerLoaded" + HandlerError="ShellPreviewHandlerPreview_HandlerError" /> Date: Mon, 25 Sep 2023 19:09:41 -0700 Subject: [PATCH 7/9] Add support for IInitializeWithItem --- src/modules/peek/Peek.Common/NativeMethods.txt | 1 + src/modules/peek/Peek.FilePreviewer/NativeMethods.txt | 3 ++- .../ShellPreviewHandlerPreviewer.cs | 8 ++++++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/modules/peek/Peek.Common/NativeMethods.txt b/src/modules/peek/Peek.Common/NativeMethods.txt index 63dc3ff9d6a2..5fed179af898 100644 --- a/src/modules/peek/Peek.Common/NativeMethods.txt +++ b/src/modules/peek/Peek.Common/NativeMethods.txt @@ -4,6 +4,7 @@ SIGDN SHGDNF SIATTRIBFLAGS IInitializeWithFile +IInitializeWithItem IInitializeWithStream IPreviewHandler IPreviewHandlerVisuals \ No newline at end of file diff --git a/src/modules/peek/Peek.FilePreviewer/NativeMethods.txt b/src/modules/peek/Peek.FilePreviewer/NativeMethods.txt index 434ee9f167d0..c113c8d127e9 100644 --- a/src/modules/peek/Peek.FilePreviewer/NativeMethods.txt +++ b/src/modules/peek/Peek.FilePreviewer/NativeMethods.txt @@ -5,4 +5,5 @@ InvalidateRect RegisterClass SetLayeredWindowAttributes SetWindowPos -ShowWindow \ No newline at end of file +ShowWindow +SHCreateItemFromParsingName \ No newline at end of file diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/ShellPreviewHandlerPreviewer/ShellPreviewHandlerPreviewer.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/ShellPreviewHandlerPreviewer/ShellPreviewHandlerPreviewer.cs index 9c8f64f063d2..b2d5a43672f9 100644 --- a/src/modules/peek/Peek.FilePreviewer/Previewers/ShellPreviewHandlerPreviewer/ShellPreviewHandlerPreviewer.cs +++ b/src/modules/peek/Peek.FilePreviewer/Previewers/ShellPreviewHandlerPreviewer/ShellPreviewHandlerPreviewer.cs @@ -17,6 +17,7 @@ using Peek.FilePreviewer.Previewers.Helpers; using Windows.Win32.UI.Shell; using Windows.Win32.UI.Shell.PropertiesSystem; +using IShellItem = Windows.Win32.UI.Shell.IShellItem; namespace Peek.FilePreviewer.Previewers { @@ -98,6 +99,13 @@ public async Task LoadPreviewAsync(CancellationToken cancellationToken) 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 From e4347be729185bd2172126648dec57dd9064d227 Mon Sep 17 00:00:00 2001 From: Dylan Date: Mon, 25 Sep 2023 19:10:21 -0700 Subject: [PATCH 8/9] Run preview handlers in separate processes --- .../peek/Peek.FilePreviewer/NativeMethods.txt | 4 +- .../ShellPreviewHandlerPreviewer.cs | 55 +++++++++++++++++-- 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/src/modules/peek/Peek.FilePreviewer/NativeMethods.txt b/src/modules/peek/Peek.FilePreviewer/NativeMethods.txt index c113c8d127e9..18dff7f5e376 100644 --- a/src/modules/peek/Peek.FilePreviewer/NativeMethods.txt +++ b/src/modules/peek/Peek.FilePreviewer/NativeMethods.txt @@ -1,4 +1,6 @@ -CreateWindowEx +IClassFactory +CoGetClassObject +CreateWindowEx DefWindowProc GetDpiForWindow InvalidateRect diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/ShellPreviewHandlerPreviewer/ShellPreviewHandlerPreviewer.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/ShellPreviewHandlerPreviewer/ShellPreviewHandlerPreviewer.cs index b2d5a43672f9..1d13c4412d79 100644 --- a/src/modules/peek/Peek.FilePreviewer/Previewers/ShellPreviewHandlerPreviewer/ShellPreviewHandlerPreviewer.cs +++ b/src/modules/peek/Peek.FilePreviewer/Previewers/ShellPreviewHandlerPreviewer/ShellPreviewHandlerPreviewer.cs @@ -3,6 +3,7 @@ // 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; @@ -15,6 +16,8 @@ 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; @@ -23,6 +26,8 @@ namespace Peek.FilePreviewer.Previewers { public partial class ShellPreviewHandlerPreviewer : ObservableObject, IShellPreviewHandlerPreviewer, IDisposable { + private static readonly ConcurrentDictionary HandlerFactories = new(); + [ObservableProperty] private IPreviewHandler? preview; @@ -74,12 +79,52 @@ public async Task LoadPreviewAsync(CancellationToken cancellationToken) var previewHandlerGuid = GetPreviewHandlerGuid(FileItem.Extension); if (!string.IsNullOrEmpty(previewHandlerGuid)) { - return Activator.CreateInstance(Type.GetTypeFromCLSID(Guid.Parse(previewHandlerGuid))!) as IPreviewHandler; - } - else - { - return null; + 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) From 184fcccc8260a46637378c57c25ea975ccf9136c Mon Sep 17 00:00:00 2001 From: Dylan Date: Tue, 26 Sep 2023 12:09:25 -0700 Subject: [PATCH 9/9] Fix redrawing when switching previewers --- .../ShellPreviewHandlerControl.xaml.cs | 61 ++++++++++++------- .../peek/Peek.FilePreviewer/NativeMethods.txt | 3 + 2 files changed, 43 insertions(+), 21 deletions(-) diff --git a/src/modules/peek/Peek.FilePreviewer/Controls/ShellPreviewHandlerControl.xaml.cs b/src/modules/peek/Peek.FilePreviewer/Controls/ShellPreviewHandlerControl.xaml.cs index 9f98d16f9904..006fd76a844b 100644 --- a/src/modules/peek/Peek.FilePreviewer/Controls/ShellPreviewHandlerControl.xaml.cs +++ b/src/modules/peek/Peek.FilePreviewer/Controls/ShellPreviewHandlerControl.xaml.cs @@ -10,6 +10,7 @@ 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; @@ -18,11 +19,19 @@ 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; @@ -76,6 +85,13 @@ private void OnHandlerVisibilityChanged() { 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 { @@ -86,29 +102,32 @@ private void OnHandlerVisibilityChanged() 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) { - try - { - switch (ActualTheme) - { - case ElementTheme.Light: - visuals.SetBackgroundColor(new COLORREF(0x00f3f3f3)); - visuals.SetTextColor(new COLORREF(0x00000000)); - break; - - case ElementTheme.Dark: - visuals.SetBackgroundColor(new COLORREF(0x00202020)); - visuals.SetTextColor(new COLORREF(0x00FFFFFF)); - break; - } - - // Changing the previewer colors might not always redraw itself - PInvoke.InvalidateRect(containerHwnd, (RECT*)null, true); - } - catch - { - } + visuals.SetBackgroundColor(bgColor); + visuals.SetTextColor(fgColor); + + // Changing the previewer colors might not always redraw itself + PInvoke.InvalidateRect(containerHwnd, (RECT*)null, true); } } diff --git a/src/modules/peek/Peek.FilePreviewer/NativeMethods.txt b/src/modules/peek/Peek.FilePreviewer/NativeMethods.txt index 18dff7f5e376..2a6906e2e909 100644 --- a/src/modules/peek/Peek.FilePreviewer/NativeMethods.txt +++ b/src/modules/peek/Peek.FilePreviewer/NativeMethods.txt @@ -1,11 +1,14 @@ IClassFactory CoGetClassObject +CreateSolidBrush CreateWindowEx DefWindowProc GetDpiForWindow InvalidateRect RegisterClass +SetClassLongPtr SetLayeredWindowAttributes SetWindowPos ShowWindow +UpdateWindow SHCreateItemFromParsingName \ No newline at end of file