Skip to content

Feature: Added support for Windows App Actions #17178

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<TargetFrameworkVersion>net9.0</TargetFrameworkVersion>
<TargetWindowsVersion>10.0.22621.0</TargetWindowsVersion>
<MinimalWindowsVersion>10.0.19041.0</MinimalWindowsVersion>
<WindowsSdkPackageVersion>10.0.22621.57</WindowsSdkPackageVersion>
<WindowsSdkPackageVersion>10.0.26100.67-preview</WindowsSdkPackageVersion>
<WindowsTargetFramework>$(TargetFrameworkVersion)-windows$(TargetWindowsVersion)</WindowsTargetFramework>
</PropertyGroup>
</Project>
13 changes: 13 additions & 0 deletions src/Files.App/Data/Items/NavigationBarSuggestionItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,17 @@

using Files.App.Controls;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media;
using Windows.AI.Actions.Hosting;

namespace Files.App.Data.Items
{
[Obsolete("Remove once Omnibar goes out of experimental.")]
public sealed partial class NavigationBarSuggestionItem : ObservableObject, IOmnibarTextMemberPathProvider
{
private ImageSource? _ActionIconSource;
public ImageSource? ActionIconSource { get => _ActionIconSource; set => SetProperty(ref _ActionIconSource, value); }

private Style? _ThemedIconStyle;
public Style? ThemedIconStyle { get => _ThemedIconStyle; set => SetProperty(ref _ThemedIconStyle, value); }

Expand Down Expand Up @@ -47,6 +52,14 @@ public string? PrimaryDisplayPreMatched
private set => SetProperty(ref _PrimaryDisplayPreMatched, value);
}


private ActionInstance? _ActionInstance;
public ActionInstance? ActionInstance
{
get => _ActionInstance;
set => SetProperty(ref _ActionInstance, value);
}

private string? _PrimaryDisplayMatched;
public string? PrimaryDisplayMatched
{
Expand Down
56 changes: 56 additions & 0 deletions src/Files.App/Extensions/ActionManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using System.Runtime.InteropServices;
using Windows.AI.Actions;
using Windows.AI.Actions.Hosting;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.System.Com;
using WinRT;

namespace Files.App.Extensions
{
internal class ActionManager
{
internal static ActionManager Instance => _instance ??= new();

private static ActionManager? _instance;

// NOTE: This Guid is subject to change in the future
private static readonly Guid IActionRuntimeIID = Guid.Parse("206EFA2C-C909-508A-B4B0-9482BE96DB9C");

// Public API usage (ActionCatalog)
private const string ActionRuntimeClsidStr = "C36FEF7E-35F3-4192-9F2C-AF1FD425FB85";

internal ActionEntityFactory EntityFactory => ActionRuntime.EntityFactory;

internal ActionRuntime? ActionRuntime;
internal ActionCatalog ActionCatalog => ActionRuntime.ActionCatalog;

private ActionManager()
{
ActionRuntime = CreateActionRuntime();
}

public static unsafe Windows.AI.Actions.ActionRuntime? CreateActionRuntime()
{
IntPtr abiPtr = default;
try
{
Guid classId = Guid.Parse(ActionRuntimeClsidStr);
Guid iid = IActionRuntimeIID;

HRESULT hresult = PInvoke.CoCreateInstance(&classId, null, CLSCTX.CLSCTX_LOCAL_SERVER, &iid, (void**)&abiPtr);
Marshal.ThrowExceptionForHR((int)hresult);

return MarshalInterface<Windows.AI.Actions.ActionRuntime>.FromAbi(abiPtr);
}
catch
{
return null;
}
finally
{
MarshalInspectable<object>.DisposeAbi(abiPtr);
}
}
}
}
1 change: 1 addition & 0 deletions src/Files.App/UserControls/NavigationToolbar.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,7 @@
<FontIcon Foreground="{ThemeResource App.Theme.IconBaseBrush}" Glyph="{x:Bind Glyph}" />
</Viewbox>
<controls:ThemedIcon Style="{x:Bind ThemedIconStyle}" Visibility="{x:Bind ThemedIconStyle, Converter={StaticResource NullToVisibilityCollapsedConverter}}" />
<Image Source="{x:Bind ActionIconSource, Mode=OneWay}" />
</Grid>

<!-- Primary Title -->
Expand Down
44 changes: 33 additions & 11 deletions src/Files.App/UserControls/NavigationToolbar.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
using Windows.AI.Actions.Hosting;
using Windows.System;

namespace Files.App.UserControls
Expand Down Expand Up @@ -262,20 +263,41 @@ private async void Omnibar_QuerySubmitted(Omnibar sender, OmnibarQuerySubmittedE
}
else if (Omnibar.CurrentSelectedMode == OmnibarCommandPaletteMode)
{
if (args.Item is not NavigationBarSuggestionItem item || item.Text is not { } commandText)
if (args.Item is not NavigationBarSuggestionItem item)
return;

var command = Commands[commandText];
if (command == Commands.None)
await DialogDisplayHelper.ShowDialogAsync(Strings.InvalidCommand.GetLocalizedResource(),
string.Format(Strings.InvalidCommandContent.GetLocalizedResource(), commandText));
else if (!command.IsExecutable)
await DialogDisplayHelper.ShowDialogAsync(Strings.CommandNotExecutable.GetLocalizedResource(),
string.Format(Strings.CommandNotExecutableContent.GetLocalizedResource(), command.Code));
else
await command.ExecuteAsync();
// Try invoking built-in command
if (item.Text is { } commandText)
{
var command = Commands[commandText];
if (command == Commands.None)
await DialogDisplayHelper.ShowDialogAsync(Strings.InvalidCommand.GetLocalizedResource(),
string.Format(Strings.InvalidCommandContent.GetLocalizedResource(), commandText));
else if (!command.IsExecutable)
await DialogDisplayHelper.ShowDialogAsync(Strings.CommandNotExecutable.GetLocalizedResource(),
string.Format(Strings.CommandNotExecutableContent.GetLocalizedResource(), command.Code));
else
await command.ExecuteAsync();

ViewModel.OmnibarCurrentSelectedMode = OmnibarPathMode;
return;
}

ViewModel.OmnibarCurrentSelectedMode = OmnibarPathMode;
// Try invoking Windows app action
if (ActionManager.Instance.ActionRuntime is not null && item.ActionInstance is ActionInstance actionInstance)
{
// Workaround for https://github.com/microsoft/App-Actions-On-Windows-Samples/issues/7
var action = ActionManager.Instance.ActionRuntime.ActionCatalog.GetAllActions()
.FirstOrDefault(a => a.Id == actionInstance.Context.ActionId);

if (action is not null)
{
var overload = action.GetOverloads().FirstOrDefault();
await overload?.InvokeAsync(actionInstance.Context);
}

ViewModel.OmnibarCurrentSelectedMode = OmnibarPathMode;
}
}
else if (Omnibar.CurrentSelectedMode == OmnibarSearchMode)
{
Expand Down
70 changes: 56 additions & 14 deletions src/Files.App/ViewModels/UserControls/NavigationToolbarViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,12 @@
using Microsoft.UI.Xaml.Input;
using System.IO;
using System.Windows.Input;
using Windows.AI.Actions;
using Windows.AI.Actions.Hosting;
using Windows.ApplicationModel.DataTransfer;
using Microsoft.Windows.ApplicationModel.Resources;
using Windows.UI.Text;
using Microsoft.UI.Xaml.Media.Imaging;

namespace Files.App.ViewModels.UserControls
{
Expand Down Expand Up @@ -65,6 +69,8 @@ public sealed partial class NavigationToolbarViewModel : ObservableObject, IAddr

// Properties

internal static ActionRuntime? ActionRuntime { get; private set; }

public ObservableCollection<PathBoxItem> PathComponents { get; } = [];

public ObservableCollection<NavigationBarSuggestionItem> NavigationBarSuggestions { get; } = [];
Expand Down Expand Up @@ -248,7 +254,6 @@ public bool IsOmnibarFocused
_ = PopulateOmnibarSuggestionsForPathMode();
break;
case OmnibarPaletteModeName:
if (OmnibarCommandPaletteModeSuggestionItems.Count is 0)
PopulateOmnibarSuggestionsForCommandPaletteMode();
break;
case OmnibarSearchModeName:
Expand Down Expand Up @@ -1172,23 +1177,60 @@ public void PopulateOmnibarSuggestionsForCommandPaletteMode()
OmnibarCommandPaletteModeText ??= string.Empty;
OmnibarCommandPaletteModeSuggestionItems.Clear();

var suggestionItems = Commands.Where(command =>
command.IsExecutable &&
command.IsAccessibleGlobally &&
(command.Description.Contains(OmnibarCommandPaletteModeText, StringComparison.OrdinalIgnoreCase) ||
command.Code.ToString().Contains(OmnibarCommandPaletteModeText, StringComparison.OrdinalIgnoreCase)))
.Select(command => new NavigationBarSuggestionItem()
if (ContentPageContext.SelectedItems.Count == 1 && ContentPageContext.SelectedItem is not null && !ContentPageContext.SelectedItem.IsFolder)
{
ThemedIconStyle = command.Glyph.ToThemedIconStyle(),
Glyph = command.Glyph.BaseGlyph,
Text = command.Code.ToString(),
PrimaryDisplay = command.Description,
HotKeys = command.HotKeys,
SearchText = OmnibarCommandPaletteModeText,
});
var dispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue.GetForCurrentThread();

dispatcherQueue.TryEnqueue(() =>
{
var selectedItemPath = ContentPageContext.SelectedItem.ItemPath;
var fileActionEntity = ActionManager.Instance.EntityFactory.CreateFileEntity(selectedItemPath);
var actions = ActionManager.Instance.ActionRuntime.ActionCatalog.GetActionsForInputs(new[] { fileActionEntity });

foreach (var action in actions.Where(a => a.Definition.Description.Contains(OmnibarCommandPaletteModeText, StringComparison.OrdinalIgnoreCase)))
{
var newItem = new NavigationBarSuggestionItem
{
PrimaryDisplay = action.Definition.Description,
SearchText = OmnibarCommandPaletteModeText,
ActionInstance = action
};

if (Uri.TryCreate(action.Definition.IconFullPath, UriKind.RelativeOrAbsolute, out Uri? validUri))
{
try
{
newItem.ActionIconSource = new BitmapImage(validUri);
}
catch (Exception)
{
}
}

OmnibarCommandPaletteModeSuggestionItems.Add(newItem);
}
});
}

var suggestionItems = Commands
.Where(command => command.IsExecutable
&& command.IsAccessibleGlobally
&& (command.Description.Contains(OmnibarCommandPaletteModeText, StringComparison.OrdinalIgnoreCase)
|| command.Code.ToString().Contains(OmnibarCommandPaletteModeText, StringComparison.OrdinalIgnoreCase)))
.Select(command => new NavigationBarSuggestionItem
{
ThemedIconStyle = command.Glyph.ToThemedIconStyle(),
Glyph = command.Glyph.BaseGlyph,
Text = command.Code.ToString(),
PrimaryDisplay = command.Description,
HotKeys = command.HotKeys,
SearchText = OmnibarCommandPaletteModeText,
});

foreach (var item in suggestionItems)
{
OmnibarCommandPaletteModeSuggestionItems.Add(item);
}
}

[Obsolete("Remove once Omnibar goes out of experimental.")]
Expand Down
Loading