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

@@ -221,11 +221,10 @@ public class UWPApplication : IProgram
var capacity = 1024U;
PWSTR outBuffer = new PWSTR((char*)(void*)Marshal.AllocHGlobal((int)capacity * sizeof(char)));
var source = $"@{{{packageFullName}? {parsed}}}";
void* reserved = null;
try
{
PInvoke.SHLoadIndirectString(source, outBuffer, capacity, ref reserved).ThrowOnFailure();
PInvoke.SHLoadIndirectString(source, outBuffer.AsSpan()).ThrowOnFailure();
var loaded = outBuffer.ToString();
return string.IsNullOrEmpty(loaded) ? string.Empty : loaded;
@@ -235,7 +234,7 @@ public class UWPApplication : IProgram
try
{
var sourceFallback = $"@{{{packageFullName}?{parsedFallback}}}";
PInvoke.SHLoadIndirectString(sourceFallback, outBuffer, capacity, ref reserved).ThrowOnFailure();
PInvoke.SHLoadIndirectString(sourceFallback, outBuffer.AsSpan()).ThrowOnFailure();
var loaded = outBuffer.ToString();
return string.IsNullOrEmpty(loaded) ? string.Empty : loaded;
}

View File

@@ -0,0 +1,42 @@
// 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 System.Runtime.InteropServices;
using Microsoft.CmdPal.Ext.Indexer.Native;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.System.Com;
using WinRT;
using WinRT.Interop;
namespace Microsoft.CmdPal.Ext.Indexer.Data;
internal static class ActionRuntimeFactory
{
private const string ActionRuntimeClsidStr = "C36FEF7E-35F3-4192-9F2C-AF1FD425FB85";
// typeof(Windows.AI.Actions.IActionRuntime).GUID
private static readonly Guid IActionRuntimeIID = Guid.Parse("206EFA2C-C909-508A-B4B0-9482BE96DB9C");
public static unsafe global::Windows.AI.Actions.ActionRuntime CreateActionRuntime()
{
IntPtr abiPtr = default;
try
{
Guid classId = Guid.Parse(ActionRuntimeClsidStr);
Guid iid = IActionRuntimeIID;
var hresult = NativeMethods.CoCreateInstance(ref Unsafe.AsRef(in classId), IntPtr.Zero, NativeHelpers.CLSCTXLOCALSERVER, ref iid, out abiPtr);
Marshal.ThrowExceptionForHR((int)hresult);
return MarshalInterface<global::Windows.AI.Actions.ActionRuntime>.FromAbi(abiPtr);
}
finally
{
MarshalInspectable<object>.DisposeAbi(abiPtr);
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@@ -0,0 +1,43 @@
// 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.Threading.Tasks;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.AI.Actions.Hosting;
namespace Microsoft.CmdPal.Ext.Indexer.Commands;
internal sealed partial class ExecuteActionCommand : InvokableCommand
{
private readonly ActionInstance actionInstance;
internal ExecuteActionCommand(ActionInstance actionInstance)
{
this.actionInstance = actionInstance;
this.Name = actionInstance.DisplayInfo.Description;
this.Icon = new IconInfo(actionInstance.Definition.IconFullPath);
}
public override CommandResult Invoke()
{
var task = Task.Run(InvokeAsync);
task.Wait();
return task.Result;
}
private async Task<CommandResult> InvokeAsync()
{
try
{
await actionInstance.InvokeAsync();
return CommandResult.GoHome();
}
catch (Exception ex)
{
return CommandResult.ShowToast("Failed to invoke action " + actionInstance.Definition.Id + ": " + ex.Message);
}
}
}

View File

@@ -4,8 +4,11 @@
using System.Collections.Generic;
using Microsoft.CmdPal.Ext.Indexer.Commands;
using Microsoft.CmdPal.Ext.Indexer.Pages;
using Microsoft.CmdPal.Ext.Indexer.Properties;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.Foundation.Metadata;
namespace Microsoft.CmdPal.Ext.Indexer.Data;
@@ -38,9 +41,24 @@ internal sealed partial class IndexerListItem : ListItem
}
}
MoreCommands = [
IContextItem[] moreCommands = [
..context,
new CommandContextItem(new OpenWithCommand(indexerItem)),
new CommandContextItem(new OpenWithCommand(indexerItem))];
if (ApiInformation.IsApiContractPresent("Windows.AI.Actions.ActionsContract", 4))
{
var actionsListContextItem = new ActionsListContextItem(indexerItem.FullPath);
if (actionsListContextItem.AnyActions())
{
moreCommands = [
.. moreCommands,
actionsListContextItem
];
}
}
MoreCommands = [
.. moreCommands,
new CommandContextItem(new ShowFileInFolderCommand(indexerItem.FullPath) { Name = Resources.Indexer_Command_ShowInFolder }),
new CommandContextItem(new CopyPathCommand(indexerItem)),
new CommandContextItem(new OpenInConsoleCommand(indexerItem)),

View File

@@ -34,6 +34,9 @@
<Content Update="Assets\FileExplorer.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Actions.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="Assets\FileExplorer.svg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>

View File

@@ -11,6 +11,7 @@ public sealed partial class NativeHelpers
public const uint SEEMASKINVOKEIDLIST = 12;
public const uint CLSCTXINPROCALL = 0x17;
public const uint CLSCTXLOCALSERVER = 0x4;
public struct PropertyKeys
{

View File

@@ -1,6 +1,7 @@
{
"$schema": "https://aka.ms/CsWin32.schema.json",
"allowMarshaling": false,
"emitSingleFile": false,
"public": true
"comInterop": {
"preserveSigMethods": [ "*" ]
}
}

View File

@@ -0,0 +1,93 @@
// 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.Collections.Generic;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CmdPal.Ext.Indexer.Commands;
using Microsoft.CmdPal.Ext.Indexer.Data;
using Microsoft.CmdPal.Ext.Indexer.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.AI.Actions;
using Windows.System;
namespace Microsoft.CmdPal.Ext.Indexer.Pages;
internal sealed partial class ActionsListContextItem : CommandContextItem
{
private readonly string fullPath;
private readonly List<CommandContextItem> actions = [];
private static readonly Lock UpdateMoreCommandsLock = new();
private static ActionRuntime actionRuntime;
public ActionsListContextItem(string fullPath)
: base(new NoOpCommand())
{
Title = Resources.Indexer_Command_Actions;
Icon = Icons.Actions;
RequestedShortcut = KeyChordHelpers.FromModifiers(alt: true, vkey: VirtualKey.A);
this.fullPath = fullPath;
UpdateMoreCommands();
}
public bool AnyActions() => actions.Count != 0;
private void ActionCatalog_Changed(global::Windows.AI.Actions.Hosting.ActionCatalog sender, object args)
{
UpdateMoreCommands();
}
private void UpdateMoreCommands()
{
try
{
lock (UpdateMoreCommandsLock)
{
if (actionRuntime == null)
{
actionRuntime = ActionRuntimeFactory.CreateActionRuntime();
Task.Delay(500).Wait();
}
actionRuntime.ActionCatalog.Changed -= ActionCatalog_Changed;
actionRuntime.ActionCatalog.Changed += ActionCatalog_Changed;
}
var extension = System.IO.Path.GetExtension(fullPath).ToLower(CultureInfo.InvariantCulture);
ActionEntity entity = null;
if (extension != null)
{
if (extension == ".jpg" || extension == ".jpeg" || extension == ".png")
{
entity = actionRuntime.EntityFactory.CreatePhotoEntity(fullPath);
}
else if (extension == ".docx" || extension == ".doc" || extension == ".pdf" || extension == ".txt")
{
entity = actionRuntime.EntityFactory.CreateDocumentEntity(fullPath);
}
}
if (entity == null)
{
entity = actionRuntime.EntityFactory.CreateFileEntity(fullPath);
}
lock (actions)
{
actions.Clear();
foreach (var actionInstance in actionRuntime.ActionCatalog.GetActionsForInputs([entity]))
{
actions.Add(new CommandContextItem(new ExecuteActionCommand(actionInstance)));
}
MoreCommands = [.. actions];
}
}
catch
{
actionRuntime = null;
}
}
}

View File

@@ -12,6 +12,8 @@ internal sealed class Icons
internal static IconInfo FileExplorer { get; } = IconHelpers.FromRelativePath("Assets\\FileExplorer.png");
internal static IconInfo Actions { get; } = IconHelpers.FromRelativePath("Assets\\Actions.png");
internal static IconInfo OpenFile { get; } = new("\uE8E5"); // OpenFile
internal static IconInfo Document { get; } = new("\uE8A5"); // Document

View File

@@ -60,6 +60,15 @@ namespace Microsoft.CmdPal.Ext.Indexer.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Actions.
/// </summary>
internal static string Indexer_Command_Actions {
get {
return ResourceManager.GetString("Indexer_Command_Actions", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Browse.
/// </summary>

View File

@@ -138,6 +138,9 @@
<data name="Indexer_Command_OpenWith" xml:space="preserve">
<value>Open with</value>
</data>
<data name="Indexer_Command_Actions" xml:space="preserve">
<value>Actions...</value>
</data>
<data name="Indexer_Command_ShowInFolder" xml:space="preserve">
<value>Show in folder</value>
</data>

View File

@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using ManagedCommon;
@@ -188,17 +189,20 @@ public static class DefaultBrowserInfo
{
var buffer = stackalloc char[128];
var capacity = 128;
void* reserved = null;
var firstChar = str[0];
var strPtr = &firstChar;
// S_OK == 0
if (global::Windows.Win32.PInvoke.SHLoadIndirectString(
str,
fixed (char* pszSourceLocal = str)
{
if (global::Windows.Win32.PInvoke.SHLoadIndirectString(
pszSourceLocal,
buffer,
(uint)capacity,
ref reserved)
== 0)
{
return new string(buffer);
default) == 0)
{
return new string(buffer);
}
}
}