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>
This commit is contained in:
Alexandre Zollinger Chohfi
2025-06-18 01:34:26 -07:00
committed by GitHub
parent c83be3e74c
commit 4737ec987e
46 changed files with 324 additions and 93 deletions

View File

@@ -4,7 +4,6 @@
using System;
using System.Runtime.CompilerServices;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.UI;
using Microsoft.UI.Xaml;
@@ -24,8 +23,8 @@ namespace Peek.FilePreviewer.Controls
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);
private static readonly HBRUSH LightThemeBgBrush = PInvoke_FilePreviewer.CreateSolidBrush(LightThemeBgColor);
private static readonly HBRUSH DarkThemeBgBrush = PInvoke_FilePreviewer.CreateSolidBrush(DarkThemeBgColor);
[ObservableProperty]
private IPreviewHandler? source;
@@ -88,19 +87,19 @@ namespace Peek.FilePreviewer.Controls
if (HandlerVisibility == Visibility.Visible)
{
PInvoke.ShowWindow(containerHwnd, SHOW_WINDOW_CMD.SW_SHOW);
PInvoke_FilePreviewer.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);
PInvoke_FilePreviewer.SetClassLongPtr(containerHwnd, GET_CLASS_LONG_INDEX.GCLP_HBRBACKGROUND, containerBgBrush);
PInvoke_FilePreviewer.UpdateWindow(containerHwnd);
PInvoke_FilePreviewer.SetClassLongPtr(containerHwnd, GET_CLASS_LONG_INDEX.GCLP_HBRBACKGROUND, IntPtr.Zero);
PInvoke_FilePreviewer.InvalidateRect(containerHwnd, (RECT*)null, true);
}
else
{
PInvoke.ShowWindow(containerHwnd, SHOW_WINDOW_CMD.SW_HIDE);
PInvoke_FilePreviewer.ShowWindow(containerHwnd, SHOW_WINDOW_CMD.SW_HIDE);
IsEnabled = false;
}
}
@@ -132,14 +131,14 @@ namespace Peek.FilePreviewer.Controls
visuals.SetTextColor(fgColor);
// Changing the previewer colors might not always redraw itself
PInvoke.InvalidateRect(containerHwnd, (RECT*)null, true);
PInvoke_FilePreviewer.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);
return PInvoke_FilePreviewer.DefWindowProc(hWnd, msg, wParam, lParam);
}
private void EnsureContainerHwndCreated()
@@ -157,14 +156,14 @@ namespace Peek.FilePreviewer.Controls
fixed (char* pContainerClassName = "PeekShellPreviewHandlerContainer")
{
PInvoke.RegisterClass(new WNDCLASSW()
PInvoke_FilePreviewer.RegisterClass(new WNDCLASSW()
{
lpfnWndProc = containerWndProc,
lpszClassName = pContainerClassName,
});
// Create the container window to host the preview handler
containerHwnd = PInvoke.CreateWindowEx(
containerHwnd = PInvoke_FilePreviewer.CreateWindowEx(
WINDOW_EX_STYLE.WS_EX_LAYERED,
pContainerClassName,
null,
@@ -178,7 +177,7 @@ namespace Peek.FilePreviewer.Controls
HINSTANCE.Null);
// Allows the preview handlers to display properly
PInvoke.SetLayeredWindowAttributes(containerHwnd, default, byte.MaxValue, LAYERED_WINDOW_ATTRIBUTES_FLAGS.LWA_ALPHA);
PInvoke_FilePreviewer.SetLayeredWindowAttributes(containerHwnd, default, byte.MaxValue, LAYERED_WINDOW_ATTRIBUTES_FLAGS.LWA_ALPHA);
}
}
@@ -186,12 +185,12 @@ namespace Peek.FilePreviewer.Controls
{
EnsureContainerHwndCreated();
var dpi = (float)PInvoke.GetDpiForWindow(containerHwnd) / 96;
var dpi = (float)PInvoke_FilePreviewer.GetDpiForWindow(containerHwnd) / 96;
// Resize the container window
PInvoke.SetWindowPos(
PInvoke_FilePreviewer.SetWindowPos(
containerHwnd,
(HWND)0, // HWND_TOP
(HWND)(nint)0, // HWND_TOP
(int)(Math.Abs(args.EffectiveViewport.X) * dpi),
(int)(Math.Abs(args.EffectiveViewport.Y) * dpi),
(int)(ActualWidth * dpi),
@@ -210,7 +209,7 @@ namespace Peek.FilePreviewer.Controls
}
// Resizing the previewer might not always redraw itself
PInvoke.InvalidateRect(containerHwnd, (RECT*)null, false);
PInvoke_FilePreviewer.InvalidateRect(containerHwnd, (RECT*)null, false);
}
private void UserControl_GotFocus(object sender, RoutedEventArgs e)

View File

@@ -1,4 +1,5 @@
{
"$schema": "https://aka.ms/CsWin32.schema.json",
"public": false
"$schema": "https://aka.ms/CsWin32.schema.json",
"public": false,
"className": "PInvoke_FilePreviewer"
}

View File

@@ -53,9 +53,9 @@ namespace Peek.FilePreviewer.Previewers.Helpers
}
}
public void Seek(long dlibMove, STREAM_SEEK dwOrigin, [Optional] ulong* plibNewPosition)
public void Seek(long dlibMove, SeekOrigin dwOrigin, [Optional] ulong* plibNewPosition)
{
long position = Stream.Seek(dlibMove, (SeekOrigin)dwOrigin);
long position = Stream.Seek(dlibMove, dwOrigin);
if (plibNewPosition != null)
{
*plibNewPosition = (ulong)position;
@@ -82,7 +82,7 @@ namespace Peek.FilePreviewer.Previewers.Helpers
throw new NotSupportedException();
}
public void LockRegion(ulong libOffset, ulong cb, uint dwLockType)
public void LockRegion(ulong libOffset, ulong cb, LOCKTYPE dwLockType)
{
throw new NotSupportedException();
}
@@ -92,7 +92,7 @@ namespace Peek.FilePreviewer.Previewers.Helpers
throw new NotSupportedException();
}
public void Stat(STATSTG* pstatstg, uint grfStatFlag)
public void Stat(STATSTG* pstatstg, STATFLAG grfStatFlag)
{
throw new NotSupportedException();
}

View File

@@ -93,7 +93,7 @@ namespace Peek.FilePreviewer.Previewers
// 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);
var hr = PInvoke_FilePreviewer.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
@@ -149,7 +149,7 @@ namespace Peek.FilePreviewer.Previewers
}
else if (previewHandler is IInitializeWithItem initWithItem)
{
var hr = PInvoke.SHCreateItemFromParsingName(FileItem.Path, null, typeof(IShellItem).GUID, out var item);
var hr = PInvoke_FilePreviewer.SHCreateItemFromParsingName(FileItem.Path, null, typeof(IShellItem).GUID, out var item);
Marshal.ThrowExceptionForHR(hr);
initWithItem.Initialize((IShellItem)item, STGM_READ);

View File

@@ -48,21 +48,21 @@ namespace Peek.UI.Extensions
internal static HWND FindChildWindow(this HWND windowHandle, string className)
{
return PInvoke.FindWindowEx(windowHandle, HWND.Null, className, null);
return PInvoke_PeekUI.FindWindowEx(windowHandle, HWND.Null, className, null);
}
internal static Size GetMonitorSize(this HWND hwnd)
{
var monitor = PInvoke.MonitorFromWindow(hwnd, MONITOR_FROM_FLAGS.MONITOR_DEFAULTTONEAREST);
var monitor = PInvoke_PeekUI.MonitorFromWindow(hwnd, MONITOR_FROM_FLAGS.MONITOR_DEFAULTTONEAREST);
MONITORINFO info = default(MONITORINFO);
info.cbSize = 40;
PInvoke.GetMonitorInfo(monitor, ref info);
PInvoke_PeekUI.GetMonitorInfo(monitor, ref info);
return new Size(info.rcMonitor.Size.Width, info.rcMonitor.Size.Height);
}
internal static double GetMonitorScale(this HWND hwnd)
{
var dpi = PInvoke.GetDpiForWindow(hwnd);
var dpi = PInvoke_PeekUI.GetDpiForWindow(hwnd);
var scalingFactor = dpi / 96d;
return scalingFactor;
}

View File

@@ -28,12 +28,12 @@ namespace Peek.UI.Extensions
// If the window is maximized, restore to normal state before change its size
var placement = default(WINDOWPLACEMENT);
if (PInvoke.GetWindowPlacement(hwndToCenter, ref placement))
if (PInvoke_PeekUI.GetWindowPlacement(hwndToCenter, ref placement))
{
if (placement.showCmd == SHOW_WINDOW_CMD.SW_MAXIMIZE)
{
placement.showCmd = SHOW_WINDOW_CMD.SW_SHOWNORMAL;
if (!PInvoke.SetWindowPlacement(hwndToCenter, in placement))
if (!PInvoke_PeekUI.SetWindowPlacement(hwndToCenter, in placement))
{
Logger.LogError($"SetWindowPlacement failed with error {Marshal.GetLastWin32Error()}");
}
@@ -44,12 +44,12 @@ namespace Peek.UI.Extensions
Logger.LogError($"GetWindowPlacement failed with error {Marshal.GetLastWin32Error()}");
}
var monitor = PInvoke.MonitorFromWindow(hwndDesktop, MONITOR_FROM_FLAGS.MONITOR_DEFAULTTONEAREST);
var monitor = PInvoke_PeekUI.MonitorFromWindow(hwndDesktop, MONITOR_FROM_FLAGS.MONITOR_DEFAULTTONEAREST);
MONITORINFO info = default(MONITORINFO);
info.cbSize = 40;
PInvoke.GetMonitorInfo(monitor, ref info);
var dpi = PInvoke.GetDpiForWindow(new HWND(hwndDesktop));
PInvoke.GetWindowRect(hwndToCenter, out RECT windowRect);
PInvoke_PeekUI.GetMonitorInfo(monitor, ref info);
var dpi = PInvoke_PeekUI.GetDpiForWindow(new HWND((nint)hwndDesktop));
PInvoke_PeekUI.GetWindowRect(hwndToCenter, out RECT windowRect);
var scalingFactor = dpi / 96d;
var w = width.HasValue ? (int)(width * scalingFactor) : windowRect.right - windowRect.left;
var h = height.HasValue ? (int)(height * scalingFactor) : windowRect.bottom - windowRect.top;
@@ -63,7 +63,7 @@ namespace Peek.UI.Extensions
private static void SetWindowPosOrThrow(HWND hWnd, HWND hWndInsertAfter, int x, int y, int cx, int cy, SET_WINDOW_POS_FLAGS uFlags)
{
bool result = PInvoke.SetWindowPos(hWnd, hWndInsertAfter, x, y, cx, cy, uFlags);
bool result = PInvoke_PeekUI.SetWindowPos(hWnd, hWndInsertAfter, x, y, cx, cy, uFlags);
if (!result)
{
Marshal.ThrowExceptionForHR(Marshal.GetLastWin32Error());

View File

@@ -58,7 +58,7 @@ namespace Peek.UI.Helpers
object? oNull2 = null;
var serviceProvider = (IServiceProvider)shellWindows.FindWindowSW(ref oNull1, ref oNull2, SWC_DESKTOP, out int pHWND, SWFO_NEEDDISPATCH);
var shellBrowser = (IShellBrowser)serviceProvider.QueryService(PInvoke.SID_STopLevelBrowser, typeof(IShellBrowser).GUID);
var shellBrowser = (IShellBrowser)serviceProvider.QueryService(PInvoke_PeekUI.SID_STopLevelBrowser, typeof(IShellBrowser).GUID);
IShellItemArray? shellItemArray = GetShellItemArray(shellBrowser, onlySelectedFiles);
return shellItemArray;
@@ -81,7 +81,7 @@ namespace Peek.UI.Helpers
if (webBrowserApp.HWND == foregroundWindowHandle)
{
var serviceProvider = (IServiceProvider)webBrowserApp;
var shellBrowser = (IShellBrowser)serviceProvider.QueryService(PInvoke.SID_STopLevelBrowser, typeof(IShellBrowser).GUID);
var shellBrowser = (IShellBrowser)serviceProvider.QueryService(PInvoke_PeekUI.SID_STopLevelBrowser, typeof(IShellBrowser).GUID);
shellBrowser.GetWindow(out IntPtr shellBrowserHandle);
if (activeTab == shellBrowserHandle)
@@ -122,7 +122,7 @@ namespace Peek.UI.Helpers
GUITHREADINFO guiThreadInfo = new() { cbSize = (uint)Marshal.SizeOf<GUITHREADINFO>() };
// Get information for the foreground thread
if (PInvoke.GetGUIThreadInfo(0, ref guiThreadInfo))
if (PInvoke_PeekUI.GetGUIThreadInfo(0, ref guiThreadInfo))
{
return guiThreadInfo.hwndActive == hwnd && (guiThreadInfo.flags & GUITHREADINFO_FLAGS.GUI_CARETBLINKING) != 0;
}

View File

@@ -0,0 +1,5 @@
{
"$schema": "https://aka.ms/CsWin32.schema.json",
"public": false,
"className": "PInvoke_PeekUI"
}

View File

@@ -128,7 +128,7 @@ namespace Peek.UI
{
// Need to read the foreground HWND before activating Peek to avoid focus stealing
// Foreground HWND must always be Explorer or Desktop
var foregroundWindowHandle = Windows.Win32.PInvoke.GetForegroundWindow();
var foregroundWindowHandle = Windows.Win32.PInvoke_PeekUI.GetForegroundWindow();
bool firstActivation = false;

View File

@@ -213,7 +213,7 @@ namespace Peek.UI
/// <param name="e">PreviewSizeChangedArgs</param>
private void FilePreviewer_PreviewSizeChanged(object sender, PreviewSizeChangedArgs e)
{
var foregroundWindowHandle = Windows.Win32.PInvoke.GetForegroundWindow();
var foregroundWindowHandle = Windows.Win32.PInvoke_PeekUI.GetForegroundWindow();
var monitorSize = foregroundWindowHandle.GetMonitorSize();
var monitorScale = foregroundWindowHandle.GetMonitorScale();