Files
PowerToys/src/modules/peek/Peek.FilePreviewer/Previewers/ShellPreviewHandlerPreviewer/ShellPreviewHandlerPreviewer.cs

241 lines
8.4 KiB
C#
Raw Normal View History

// 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 Peek.FilePreviewer.Previewers.Interfaces;
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))
{
[Peek] Fix ShellPreviewHandler Previewer (#40111) <!-- Enter a brief description/summary of your PR here. What does it fix/what does it change/how was it tested (even manually, if necessary)? --> ## Summary of the Pull Request CsWin32 0.3.183 broke preview through preview handlers in Peek. `Error in UpdatePreviewAsync, falling back to default previewer: Unable to cast COM object of type 'System.__ComObject' to class type 'System.IntPtr'. Instances of types that represent COM components cannot be cast to types that do not represent COM components; however they can be cast to interfaces as long as the underlying COM component supports QueryInterface calls for the IID of the interface.` Signature of `CoGetClassObject` changed for last argument from `void*` to `object`. Thanks @AArnott 🙂 <!-- Please review the items on the PR checklist before submitting--> ## PR Checklist - [ ] **Closes:** #xxx - [ ] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [ ] **Tests:** Added/updated and all pass - [ ] **Localization:** All end-user-facing strings can be localized - [ ] **Dev docs:** Added/updated - [ ] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx <!-- Provide a more detailed description of the PR, other things fixed, or any additional comments/features here --> ## Detailed Description of the Pull Request / Additional comments <!-- Describe how you validated the behavior. Add automated tests wherever possible, but list manual validation steps taken as well --> ## Validation Steps Performed Tested Preview of Office files in Peek.
2025-06-19 03:05:44 +02:00
var hr = PInvoke_FilePreviewer.CoGetClassObject(clsid, CLSCTX.CLSCTX_LOCAL_SERVER, null, typeof(IClassFactory).GUID, out object 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?
[Peek] Fix ShellPreviewHandler Previewer (#40111) <!-- Enter a brief description/summary of your PR here. What does it fix/what does it change/how was it tested (even manually, if necessary)? --> ## Summary of the Pull Request CsWin32 0.3.183 broke preview through preview handlers in Peek. `Error in UpdatePreviewAsync, falling back to default previewer: Unable to cast COM object of type 'System.__ComObject' to class type 'System.IntPtr'. Instances of types that represent COM components cannot be cast to types that do not represent COM components; however they can be cast to interfaces as long as the underlying COM component supports QueryInterface calls for the IID of the interface.` Signature of `CoGetClassObject` changed for last argument from `void*` to `object`. Thanks @AArnott 🙂 <!-- Please review the items on the PR checklist before submitting--> ## PR Checklist - [ ] **Closes:** #xxx - [ ] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [ ] **Tests:** Added/updated and all pass - [ ] **Localization:** All end-user-facing strings can be localized - [ ] **Dev docs:** Added/updated - [ ] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx <!-- Provide a more detailed description of the PR, other things fixed, or any additional comments/features here --> ## Detailed Description of the Pull Request / Additional comments <!-- Describe how you validated the behavior. Add automated tests wherever possible, but list manual validation steps taken as well --> ## Validation Steps Performed Tested Preview of Office files in Peek.
2025-06-19 03:05:44 +02:00
factory = (IClassFactory)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)
{
Added basic support for Windows App Actions. (#39927) ## Summary of the Pull Request Adds basic support for finding, listing, and executing Windows App Actions on files found by the Microsoft.CmdPal.Ext.Indexer extension. ## PR Checklist - [X] **Closes:** #39926 - [X] **Communication:** I've discussed this with core contributors already. If work hasn't been agreed, this work might be rejected - [ ] **Tests:** Added/updated and all pass - [X] **Localization:** All end user facing strings can be localized - [ ] **Dev docs:** Added/updated - [ ] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx ## Detailed Description of the Pull Request / Additional comments We also update cswin32 to stable version. <!-- Describe how you validated the behavior. Add automated tests wherever possible, but list manual validation steps taken as well --> ## Validation Steps Performed Validated that it doesn't show on older versions of Windows (<26100 insiders) and that it does work on newer version that have the App Actions runtime. --------- Co-authored-by: Gordon Lam (SH) <yeelam@microsoft.com> Co-authored-by: Mike Griese <migrie@microsoft.com> Co-authored-by: Leilei Zhang <leilzh@microsoft.com>
2025-06-18 01:34:26 -07:00
var hr = PInvoke_FilePreviewer.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 IsItemSupported(IFileSystemItem item)
{
return !string.IsNullOrEmpty(GetPreviewHandlerGuid(item.Extension));
}
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;
}
}
}