From f1e045751ac13e41df7cb422a8f3c0ca5f613b42 Mon Sep 17 00:00:00 2001 From: Michael Jolley Date: Mon, 22 Dec 2025 17:08:15 -0600 Subject: [PATCH] CmdPal: Fallback ranking and global results (#43549) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit > [!IMPORTANT] > For extension developers, this release includes a new required `string Id` property for `FallbackCommandItem`. While your existing extensions will continue to work, without this `Id` being set, your fallbacks will not display and will not be rankable. > Before this is released, you will want to prepare your extension fallbacks. > > As an example, we are naming our built-in extensions as: > - Calculator extension provider Id: `com.microsoft.cmdpal.builtin.calculator` > - Calculator extension fallback: `com.microsoft.cmdpal.builtin.calculator.fallback` > > While the content of the Id isn't important, what is important is that it is unique to your extension and fallback to avoid conflicting with other extensions. Now the good stuff: ## What the heck does it do!? ### The backstory In PowerToys 0.95, we released performance improvements to Command Palette. One of the many ways we improved its speed is by no longer ranking fallback commands with other "top level" commands. Instead, all fallbacks would surface at the bottom of the results and be listed in the order they were registered with Command Palette. But this was only a temporary solution until the work included in this pull request was ready. In reality, not all fallbacks were treated equally. We marked the calculator and run fallbacks as "special." Special fallbacks **were** ranked like top-level commands and allowed to surface to the top of the results. ### The new "hotness" This PR brings the power of fallback management back to the people. In the Command Palette settings, you, dear user, can specify what order you want fallbacks to display in at the bottom of the results. This keeps those fallbacks unranked by Command Palette but displays them in an order that makes sense for you. But keep in mind, these will still live at the bottom of search results. But alas, we have also heard your cries that you'd like _some_ fallbacks to be ranked by Command Palette and surface to the top of the results. So, this PR allows you to mark any fallback as "special" by choosing to include them in the global results. Special (Global) fallbacks are treated like "top level" commands and appear in the search result based on their title & description. ### Screenshots/video image image image image ### GitHub issue maintenance details Closes #38312 Closes #38288 Closes #42524 Closes #41024 Closes #40351 Closes #41696 Closes #40193 --------- Co-authored-by: Niels Laute Co-authored-by: Jiří Polášek --- .github/actions/spell-check/expect.txt | 1 + .../CommandProviderWrapper.cs | 3 +- .../Commands/BuiltInsCommandProvider.cs | 9 +- .../Commands/FallbackLogItem.cs | 4 +- .../Commands/FallbackReloadItem.cs | 8 +- .../Commands/MainListPage.cs | 98 ++++++++++++------- .../Commands/MainListPageResultFactory.cs | 18 +++- .../FallbackSettings.cs | 30 ++++++ .../FallbackSettingsViewModel.cs | 86 ++++++++++++++++ .../Properties/Resources.Designer.cs | 24 ++--- .../Properties/Resources.resx | 3 + .../ProviderSettings.cs | 30 +++--- .../ProviderSettingsViewModel.cs | 62 +++++++----- .../SettingsModel.cs | 21 ++++ .../SettingsViewModel.cs | 57 +++++++++-- .../TopLevelViewModel.cs | 33 ++++--- .../Controls/FallbackRanker.xaml | 74 ++++++++++++++ .../Controls/FallbackRanker.xaml.cs | 31 ++++++ .../Controls/FallbackRankerDialog.xaml | 43 ++++++++ .../Controls/FallbackRankerDialog.xaml.cs | 33 +++++++ .../Microsoft.CmdPal.UI.csproj | 7 ++ .../PowerToysRootPageService.cs | 21 ++-- .../Settings/AppearancePage.xaml.cs | 5 +- .../Settings/ExtensionPage.xaml | 56 ++++++++--- .../Settings/ExtensionPage.xaml.cs | 5 + .../Settings/ExtensionsPage.xaml | 7 ++ .../Settings/ExtensionsPage.xaml.cs | 18 +++- .../Settings/GeneralPage.xaml.cs | 5 +- .../Strings/en-us/Resources.resw | 72 +++++++++----- .../MainListPageResultFactoryTests.cs | 10 +- .../Pages/FallbackCalculatorItem.cs | 3 +- .../FallbackOpenFileItem.cs | 5 +- .../Commands/FallbackRemoteDesktopItem.cs | 2 +- .../FallbackExecuteItem.cs | 6 +- .../FallbackSystemCommandItem.cs | 4 +- .../FallbackTimeDateItem.cs | 3 +- .../FallbackExecuteSearchItem.cs | 3 +- .../FallbackOpenURLItem.cs | 3 +- .../Pages/FallbackWindowsSettingsItem.cs | 7 +- .../FallbackCommandItem.cs | 15 ++- .../Microsoft.CommandPalette.Extensions.idl | 5 + 41 files changed, 738 insertions(+), 192 deletions(-) create mode 100644 src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/FallbackSettings.cs create mode 100644 src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/FallbackSettingsViewModel.cs create mode 100644 src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/FallbackRanker.xaml create mode 100644 src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/FallbackRanker.xaml.cs create mode 100644 src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/FallbackRankerDialog.xaml create mode 100644 src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/FallbackRankerDialog.xaml.cs diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index bdafb1c4ff..6a49d0a239 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -1218,6 +1218,7 @@ opensource openxmlformats ollama onnx +openurl OPTIMIZEFORINVOKE ORPHANEDDIALOGTITLE ORSCANS diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandProviderWrapper.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandProviderWrapper.cs index c59c90814b..939b42de14 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandProviderWrapper.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandProviderWrapper.cs @@ -188,11 +188,12 @@ public sealed class CommandProviderWrapper Func makeAndAdd = (ICommandItem? i, bool fallback) => { CommandItemViewModel commandItemViewModel = new(new(i), pageContext); - TopLevelViewModel topLevelViewModel = new(commandItemViewModel, fallback, ExtensionHost, ProviderId, settings, providerSettings, serviceProvider); + TopLevelViewModel topLevelViewModel = new(commandItemViewModel, fallback, ExtensionHost, ProviderId, settings, providerSettings, serviceProvider, i); topLevelViewModel.InitializeProperties(); return topLevelViewModel; }; + if (commands is not null) { TopLevelItems = commands diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/BuiltInsCommandProvider.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/BuiltInsCommandProvider.cs index 55581e65f3..0167f6f7a1 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/BuiltInsCommandProvider.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/BuiltInsCommandProvider.cs @@ -2,7 +2,6 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using Microsoft.CmdPal.Core.ViewModels.Messages; using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions.Toolkit; @@ -27,7 +26,13 @@ public sealed partial class BuiltInsCommandProvider : CommandProvider public override IFallbackCommandItem[] FallbackCommands() => [ - new FallbackCommandItem(quitCommand, displayTitle: Properties.Resources.builtin_quit_subtitle) { Subtitle = Properties.Resources.builtin_quit_subtitle }, + new FallbackCommandItem( + quitCommand, + Properties.Resources.builtin_quit_subtitle, + quitCommand.Id) + { + Subtitle = Properties.Resources.builtin_quit_subtitle, + }, _fallbackReloadItem, _fallbackLogItem, ]; diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/FallbackLogItem.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/FallbackLogItem.cs index a96d49ff79..2f44b018e1 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/FallbackLogItem.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/FallbackLogItem.cs @@ -13,8 +13,10 @@ internal sealed partial class FallbackLogItem : FallbackCommandItem { private readonly LogMessagesPage _logMessagesPage; + private const string _id = "com.microsoft.cmdpal.log"; + public FallbackLogItem() - : base(new LogMessagesPage() { Id = "com.microsoft.cmdpal.log" }, Resources.builtin_log_subtitle) + : base(new LogMessagesPage() { Id = _id }, Resources.builtin_log_subtitle, _id) { _logMessagesPage = (LogMessagesPage)Command!; Title = string.Empty; diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/FallbackReloadItem.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/FallbackReloadItem.cs index 37a37d9283..489c73a537 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/FallbackReloadItem.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/FallbackReloadItem.cs @@ -2,7 +2,6 @@ // 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 Microsoft.CommandPalette.Extensions.Toolkit; namespace Microsoft.CmdPal.UI.ViewModels.BuiltinCommands; @@ -11,10 +10,13 @@ internal sealed partial class FallbackReloadItem : FallbackCommandItem { private readonly ReloadExtensionsCommand _reloadCommand; + private const string _id = "com.microsoft.cmdpal.reload"; + public FallbackReloadItem() : base( - new ReloadExtensionsCommand() { Id = "com.microsoft.cmdpal.reload" }, - Properties.Resources.builtin_reload_display_title) + new ReloadExtensionsCommand() { Id = _id }, + Properties.Resources.builtin_reload_display_title, + _id) { _reloadCommand = (ReloadExtensionsCommand)Command!; Title = string.Empty; diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/MainListPage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/MainListPage.cs index 4118ac64db..2489cd0817 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/MainListPage.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/MainListPage.cs @@ -17,7 +17,6 @@ using Microsoft.CmdPal.UI.ViewModels.Messages; using Microsoft.CmdPal.UI.ViewModels.Properties; using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions.Toolkit; -using Microsoft.Extensions.DependencyInjection; namespace Microsoft.CmdPal.UI.ViewModels.MainPage; @@ -29,26 +28,17 @@ public partial class MainListPage : DynamicListPage, IRecipient, IRecipient, IDisposable { - private readonly string[] _specialFallbacks = [ - "com.microsoft.cmdpal.builtin.run", - "com.microsoft.cmdpal.builtin.calculator", - "com.microsoft.cmdpal.builtin.system", - "com.microsoft.cmdpal.builtin.core", - "com.microsoft.cmdpal.builtin.websearch", - "com.microsoft.cmdpal.builtin.windowssettings", - "com.microsoft.cmdpal.builtin.datetime", - "com.microsoft.cmdpal.builtin.remotedesktop", - ]; - - private readonly IServiceProvider _serviceProvider; private readonly TopLevelCommandManager _tlcManager; + private readonly AliasManager _aliasManager; + private readonly SettingsModel _settings; + private readonly AppStateModel _appStateModel; private List>? _filteredItems; private List>? _filteredApps; - private List>? _fallbackItems; // Keep as IEnumerable for deferred execution. Fallback item titles are updated // asynchronously, so scoring must happen lazily when GetItems is called. private IEnumerable>? _scoredFallbackItems; + private IEnumerable>? _fallbackItems; private bool _includeApps; private bool _filteredItemsIncludesApps; private int _appResultLimit = 10; @@ -58,14 +48,16 @@ public partial class MainListPage : DynamicListPage, private CancellationTokenSource? _cancellationTokenSource; - public MainListPage(IServiceProvider serviceProvider) + public MainListPage(TopLevelCommandManager topLevelCommandManager, SettingsModel settings, AliasManager aliasManager, AppStateModel appStateModel) { Title = Resources.builtin_home_name; Icon = IconHelpers.FromRelativePath("Assets\\StoreLogo.scale-200.png"); PlaceholderText = Properties.Resources.builtin_main_list_page_searchbar_placeholder; - _serviceProvider = serviceProvider; - _tlcManager = _serviceProvider.GetService()!; + _settings = settings; + _aliasManager = aliasManager; + _appStateModel = appStateModel; + _tlcManager = topLevelCommandManager; _tlcManager.PropertyChanged += TlcManager_PropertyChanged; _tlcManager.TopLevelCommands.CollectionChanged += Commands_CollectionChanged; @@ -83,7 +75,6 @@ public partial class MainListPage : DynamicListPage, WeakReferenceMessenger.Default.Register(this); WeakReferenceMessenger.Default.Register(this); - var settings = _serviceProvider.GetService()!; settings.SettingsChanged += SettingsChangedHandler; HotReloadSettings(settings); _includeApps = _tlcManager.IsProviderActive(AllAppsCommandProvider.WellKnownId); @@ -163,14 +154,29 @@ public partial class MainListPage : DynamicListPage, { // Either return the top-level commands (no search text), or the merged and // filtered results. - return string.IsNullOrEmpty(SearchText) - ? _tlcManager.TopLevelCommands.Where(tlc => !tlc.IsFallback && !string.IsNullOrEmpty(tlc.Title)).ToArray() - : MainListPageResultFactory.Create( + if (string.IsNullOrWhiteSpace(SearchText)) + { + return _tlcManager.TopLevelCommands + .Where(tlc => !tlc.IsFallback && !string.IsNullOrEmpty(tlc.Title)) + .ToArray(); + } + else + { + var validScoredFallbacks = _scoredFallbackItems? + .Where(s => !string.IsNullOrWhiteSpace(s.Item.Title)) + .ToList(); + + var validFallbacks = _fallbackItems? + .Where(s => !string.IsNullOrWhiteSpace(s.Item.Title)) + .ToList(); + + return MainListPageResultFactory.Create( _filteredItems, - _scoredFallbackItems?.ToList(), + validScoredFallbacks, _filteredApps, - _fallbackItems, + validFallbacks, _appResultLimit); + } } } @@ -200,7 +206,7 @@ public partial class MainListPage : DynamicListPage, // Handle changes to the filter text here if (!string.IsNullOrEmpty(SearchText)) { - var aliases = _serviceProvider.GetService()!; + var aliases = _aliasManager; if (token.IsCancellationRequested) { @@ -236,7 +242,8 @@ public partial class MainListPage : DynamicListPage, } // prefilter fallbacks - var specialFallbacks = new List(_specialFallbacks.Length); + var globalFallbacks = _settings.GetGlobalFallbacks(); + var specialFallbacks = new List(globalFallbacks.Length); var commonFallbacks = new List(); foreach (var s in commands) @@ -246,7 +253,7 @@ public partial class MainListPage : DynamicListPage, continue; } - if (_specialFallbacks.Contains(s.CommandProviderId)) + if (globalFallbacks.Contains(s.Id)) { specialFallbacks.Add(s); } @@ -369,7 +376,7 @@ public partial class MainListPage : DynamicListPage, } } - var history = _serviceProvider.GetService()!.RecentCommands!; + var history = _appStateModel.RecentCommands!; Func scoreItem = (a, b) => { return ScoreTopLevelItem(a, b, history); }; // Produce a list of everything that matches the current filter. @@ -380,7 +387,7 @@ public partial class MainListPage : DynamicListPage, return; } - IEnumerable newFallbacksForScoring = commands.Where(s => s.IsFallback && _specialFallbacks.Contains(s.CommandProviderId)); + IEnumerable newFallbacksForScoring = commands.Where(s => s.IsFallback && globalFallbacks.Contains(s.Id)); if (token.IsCancellationRequested) { @@ -394,8 +401,8 @@ public partial class MainListPage : DynamicListPage, return; } - // Defaulting scored to 1 but we'll eventually use user rankings - _fallbackItems = [.. newFallbacks.Select(f => new Scored { Item = f, Score = 1 })]; + Func scoreFallbackItem = (a, b) => { return ScoreFallbackItem(a, b, _settings.FallbackRanks); }; + _fallbackItems = [.. ListHelpers.FilterListWithScores(newFallbacks ?? [], SearchText, scoreFallbackItem)]; if (token.IsCancellationRequested) { @@ -464,9 +471,8 @@ public partial class MainListPage : DynamicListPage, private bool ActuallyLoading() { - var tlcManager = _serviceProvider.GetService()!; var allApps = AllAppsCommandProvider.Page; - return allApps.IsLoading || tlcManager.IsLoading; + return allApps.IsLoading || _tlcManager.IsLoading; } // Almost verbatim ListHelpers.ScoreListItem, but also accounting for the @@ -558,13 +564,30 @@ public partial class MainListPage : DynamicListPage, return (int)finalScore; } + internal static int ScoreFallbackItem(string query, IListItem topLevelOrAppItem, string[] fallbackRanks) + { + // Default to 1 so it always shows in list. + var finalScore = 1; + + if (topLevelOrAppItem is TopLevelViewModel topLevelViewModel) + { + var index = Array.IndexOf(fallbackRanks, topLevelViewModel.Id); + + if (index >= 0) + { + finalScore = fallbackRanks.Length - index + 1; + } + } + + return finalScore; + } + public void UpdateHistory(IListItem topLevelOrAppItem) { var id = IdForTopLevelOrAppItem(topLevelOrAppItem); - var state = _serviceProvider.GetService()!; - var history = state.RecentCommands; + var history = _appStateModel.RecentCommands; history.AddHistoryItem(id); - AppStateModel.SaveState(state); + AppStateModel.SaveState(_appStateModel); } private static string IdForTopLevelOrAppItem(IListItem topLevelOrAppItem) @@ -596,10 +619,9 @@ public partial class MainListPage : DynamicListPage, _tlcManager.PropertyChanged -= TlcManager_PropertyChanged; _tlcManager.TopLevelCommands.CollectionChanged -= Commands_CollectionChanged; - var settings = _serviceProvider.GetService(); - if (settings is not null) + if (_settings is not null) { - settings.SettingsChanged -= SettingsChangedHandler; + _settings.SettingsChanged -= SettingsChangedHandler; } WeakReferenceMessenger.Default.UnregisterAll(this); diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/MainListPageResultFactory.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/MainListPageResultFactory.cs index f1bddf5197..d63c0e4f90 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/MainListPageResultFactory.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/MainListPageResultFactory.cs @@ -29,13 +29,19 @@ internal static class MainListPageResultFactory } int len1 = filteredItems?.Count ?? 0; + + // Empty fallbacks are removed prior to this merge. int len2 = scoredFallbackItems?.Count ?? 0; // Apps are pre-sorted, so we just need to take the top N, limited by appResultLimit. int len3 = Math.Min(filteredApps?.Count ?? 0, appResultLimit); + int nonEmptyFallbackCount = fallbackItems?.Count ?? 0; + // Allocate the exact size of the result array. - int totalCount = len1 + len2 + len3 + GetNonEmptyFallbackItemsCount(fallbackItems); + // We'll add an extra slot for the fallbacks section header if needed. + int totalCount = len1 + len2 + len3 + nonEmptyFallbackCount + (nonEmptyFallbackCount > 0 ? 1 : 0); + var result = new IListItem[totalCount]; // Three-way stable merge of already-sorted lists. @@ -119,9 +125,15 @@ internal static class MainListPageResultFactory } // Append filtered fallback items. Fallback items are added post-sort so they are - // always at the end of the list and eventually ordered based on user preference. + // always at the end of the list and are sorted by user settings. if (fallbackItems is not null) { + // Create the fallbacks section header + if (fallbackItems.Count > 0) + { + result[writePos++] = new Separator(Properties.Resources.fallbacks); + } + for (int i = 0; i < fallbackItems.Count; i++) { var item = fallbackItems[i].Item; @@ -143,7 +155,7 @@ internal static class MainListPageResultFactory { for (int i = 0; i < fallbackItems.Count; i++) { - if (!string.IsNullOrEmpty(fallbackItems[i].Item.Title)) + if (!string.IsNullOrWhiteSpace(fallbackItems[i].Item.Title)) { fallbackItemsCount++; } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/FallbackSettings.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/FallbackSettings.cs new file mode 100644 index 0000000000..38b76957c3 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/FallbackSettings.cs @@ -0,0 +1,30 @@ +// 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.Text.Json.Serialization; + +namespace Microsoft.CmdPal.UI.ViewModels; + +public class FallbackSettings +{ + public bool IsEnabled { get; set; } = true; + + public bool IncludeInGlobalResults { get; set; } + + public FallbackSettings() + { + } + + public FallbackSettings(bool isBuiltIn) + { + IncludeInGlobalResults = isBuiltIn; + } + + [JsonConstructor] + public FallbackSettings(bool isEnabled, bool includeInGlobalResults) + { + IsEnabled = isEnabled; + IncludeInGlobalResults = includeInGlobalResults; + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/FallbackSettingsViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/FallbackSettingsViewModel.cs new file mode 100644 index 0000000000..fbba4ce3f4 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/FallbackSettingsViewModel.cs @@ -0,0 +1,86 @@ +// 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 CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Messaging; +using Microsoft.CmdPal.Core.ViewModels; +using Microsoft.CmdPal.UI.ViewModels.Messages; + +namespace Microsoft.CmdPal.UI.ViewModels; + +public partial class FallbackSettingsViewModel : ObservableObject +{ + private readonly SettingsModel _settings; + private readonly FallbackSettings _fallbackSettings; + + public string DisplayName { get; private set; } = string.Empty; + + public IconInfoViewModel Icon { get; private set; } = new(null); + + public string Id { get; private set; } = string.Empty; + + public bool IsEnabled + { + get => _fallbackSettings.IsEnabled; + set + { + if (value != _fallbackSettings.IsEnabled) + { + _fallbackSettings.IsEnabled = value; + + if (!_fallbackSettings.IsEnabled) + { + _fallbackSettings.IncludeInGlobalResults = false; + } + + Save(); + OnPropertyChanged(nameof(IsEnabled)); + } + } + } + + public bool IncludeInGlobalResults + { + get => _fallbackSettings.IncludeInGlobalResults; + set + { + if (value != _fallbackSettings.IncludeInGlobalResults) + { + _fallbackSettings.IncludeInGlobalResults = value; + + if (!_fallbackSettings.IsEnabled) + { + _fallbackSettings.IsEnabled = true; + } + + Save(); + OnPropertyChanged(nameof(IncludeInGlobalResults)); + } + } + } + + public FallbackSettingsViewModel( + TopLevelViewModel fallback, + FallbackSettings fallbackSettings, + SettingsModel settingsModel, + ProviderSettingsViewModel providerSettings) + { + _settings = settingsModel; + _fallbackSettings = fallbackSettings; + + Id = fallback.Id; + DisplayName = string.IsNullOrWhiteSpace(fallback.DisplayTitle) + ? (string.IsNullOrWhiteSpace(fallback.Title) ? providerSettings.DisplayName : fallback.Title) + : fallback.DisplayTitle; + + Icon = new(fallback.InitialIcon); + Icon.InitializeProperties(); + } + + private void Save() + { + SettingsModel.SaveSettings(_settings); + WeakReferenceMessenger.Default.Send(new()); + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Properties/Resources.Designer.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Properties/Resources.Designer.cs index 8bc2a42a92..8d9ff6d6bb 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Properties/Resources.Designer.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Properties/Resources.Designer.cs @@ -205,7 +205,7 @@ namespace Microsoft.CmdPal.UI.ViewModels.Properties { } /// - /// Looks up a localized string similar to Create a new extension. + /// Looks up a localized string similar to Create extension. /// public static string builtin_create_extension_title { get { @@ -349,7 +349,7 @@ namespace Microsoft.CmdPal.UI.ViewModels.Properties { } /// - /// Looks up a localized string similar to Creates a project for a new Command Palette extension. + /// Looks up a localized string similar to Generate a new Command Palette extension project. /// public static string builtin_new_extension_subtitle { get { @@ -358,7 +358,7 @@ namespace Microsoft.CmdPal.UI.ViewModels.Properties { } /// - /// Looks up a localized string similar to Open Settings. + /// Looks up a localized string similar to Open Command Palette settings. /// public static string builtin_open_settings_name { get { @@ -366,15 +366,6 @@ namespace Microsoft.CmdPal.UI.ViewModels.Properties { } } - /// - /// Looks up a localized string similar to Open Command Palette settings. - /// - public static string builtin_open_settings_subtitle { - get { - return ResourceManager.GetString("builtin_open_settings_subtitle", resourceCulture); - } - } - /// /// Looks up a localized string similar to Exit Command Palette. /// @@ -437,5 +428,14 @@ namespace Microsoft.CmdPal.UI.ViewModels.Properties { return ResourceManager.GetString("builtin_settings_extension_n_extensions_installed", resourceCulture); } } + + /// + /// Looks up a localized string similar to Fallbacks. + /// + public static string fallbacks { + get { + return ResourceManager.GetString("fallbacks", resourceCulture); + } + } } } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Properties/Resources.resx b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Properties/Resources.resx index bb7637e133..9f6b68c1bd 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Properties/Resources.resx +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Properties/Resources.resx @@ -242,4 +242,7 @@ Pick background image + + Fallbacks + \ No newline at end of file diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ProviderSettings.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ProviderSettings.cs index 1e20040d57..3bb9a43360 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ProviderSettings.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ProviderSettings.cs @@ -8,9 +8,15 @@ namespace Microsoft.CmdPal.UI.ViewModels; public class ProviderSettings { + // List of built-in fallbacks that should not have global results enabled by default + private readonly string[] _excludedBuiltInFallbacks = [ + "com.microsoft.cmdpal.builtin.indexer.fallback", + "com.microsoft.cmdpal.builtin.calculator.fallback", + ]; + public bool IsEnabled { get; set; } = true; - public Dictionary FallbackCommands { get; set; } = []; + public Dictionary FallbackCommands { get; set; } = new(); [JsonIgnore] public string ProviderDisplayName { get; set; } = string.Empty; @@ -39,19 +45,21 @@ public class ProviderSettings ProviderDisplayName = wrapper.DisplayName; + if (wrapper.FallbackItems.Length > 0) + { + foreach (var fallback in wrapper.FallbackItems) + { + if (!FallbackCommands.ContainsKey(fallback.Id)) + { + var enableGlobalResults = IsBuiltin && !_excludedBuiltInFallbacks.Contains(fallback.Id); + FallbackCommands[fallback.Id] = new FallbackSettings(enableGlobalResults); + } + } + } + if (string.IsNullOrEmpty(ProviderId)) { throw new InvalidDataException("Did you add a built-in command and forget to set the Id? Make sure you do that!"); } } - - public bool IsFallbackEnabled(TopLevelViewModel command) - { - return FallbackCommands.TryGetValue(command.Id, out var enabled) ? enabled : true; - } - - public void SetFallbackEnabled(TopLevelViewModel command, bool enabled) - { - FallbackCommands[command.Id] = enabled; - } } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ProviderSettingsViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ProviderSettingsViewModel.cs index 52b2bea003..68e554e463 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ProviderSettingsViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ProviderSettingsViewModel.cs @@ -9,26 +9,39 @@ using Microsoft.CmdPal.Core.Common.Services; using Microsoft.CmdPal.Core.ViewModels; using Microsoft.CmdPal.UI.ViewModels.Messages; using Microsoft.CmdPal.UI.ViewModels.Properties; -using Microsoft.Extensions.DependencyInjection; namespace Microsoft.CmdPal.UI.ViewModels; -public partial class ProviderSettingsViewModel( - CommandProviderWrapper _provider, - ProviderSettings _providerSettings, - IServiceProvider _serviceProvider) : ObservableObject +public partial class ProviderSettingsViewModel : ObservableObject { - private readonly SettingsModel _settings = _serviceProvider.GetService()!; + private readonly CommandProviderWrapper _provider; + private readonly ProviderSettings _providerSettings; + private readonly SettingsModel _settings; + private readonly Lock _initializeSettingsLock = new(); private Task? _initializeSettingsTask; + public ProviderSettingsViewModel( + CommandProviderWrapper provider, + ProviderSettings providerSettings, + SettingsModel settings) + { + _provider = provider; + _providerSettings = providerSettings; + _settings = settings; + + LoadingSettings = _provider.Settings?.HasSettings ?? false; + + BuildFallbackViewModels(); + } + public string DisplayName => _provider.DisplayName; public string ExtensionName => _provider.Extension?.ExtensionDisplayName ?? "Built-in"; public string ExtensionSubtext => IsEnabled ? HasFallbackCommands ? - $"{ExtensionName}, {TopLevelCommands.Count} commands, {FallbackCommands.Count} fallback commands" : + $"{ExtensionName}, {TopLevelCommands.Count} commands, {_provider.FallbackItems?.Length} fallback commands" : $"{ExtensionName}, {TopLevelCommands.Count} commands" : Resources.builtin_disabled_extension; @@ -42,7 +55,7 @@ public partial class ProviderSettingsViewModel( public IconInfoViewModel Icon => _provider.Icon; [ObservableProperty] - public partial bool LoadingSettings { get; set; } = _provider.Settings?.HasSettings ?? false; + public partial bool LoadingSettings { get; set; } public bool IsEnabled { @@ -145,28 +158,29 @@ public partial class ProviderSettingsViewModel( } [field: AllowNull] - public List FallbackCommands - { - get - { - if (field is null) - { - field = BuildFallbackViewModels(); - } - - return field; - } - } + public List FallbackCommands { get; set; } = []; public bool HasFallbackCommands => _provider.FallbackItems?.Length > 0; - private List BuildFallbackViewModels() + private void BuildFallbackViewModels() { var thisProvider = _provider; - var providersCommands = thisProvider.FallbackItems; + var providersFallbackCommands = thisProvider.FallbackItems; - // Remember! This comes in on the UI thread! - return [.. providersCommands]; + List fallbackViewModels = new(providersFallbackCommands.Length); + foreach (var fallbackItem in providersFallbackCommands) + { + if (_providerSettings.FallbackCommands.TryGetValue(fallbackItem.Id, out var fallbackSettings)) + { + fallbackViewModels.Add(new FallbackSettingsViewModel(fallbackItem, fallbackSettings, _settings, this)); + } + else + { + fallbackViewModels.Add(new FallbackSettingsViewModel(fallbackItem, new(), _settings, this)); + } + } + + FallbackCommands = fallbackViewModels; } private void Save() => SettingsModel.SaveSettings(_settings); diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsModel.cs index e210359f76..483fe3fdc3 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsModel.cs @@ -50,6 +50,8 @@ public partial class SettingsModel : ObservableObject public Dictionary ProviderSettings { get; set; } = []; + public string[] FallbackRanks { get; set; } = []; + public Dictionary Aliases { get; set; } = []; public List CommandHotkeys { get; set; } = []; @@ -107,6 +109,25 @@ public partial class SettingsModel : ObservableObject return settings; } + public string[] GetGlobalFallbacks() + { + var globalFallbacks = new HashSet(); + + foreach (var provider in ProviderSettings.Values) + { + foreach (var fallback in provider.FallbackCommands) + { + var fallbackSetting = fallback.Value; + if (fallbackSetting.IsEnabled && fallbackSetting.IncludeInGlobalResults) + { + globalFallbacks.Add(fallback.Key); + } + } + } + + return globalFallbacks.ToArray(); + } + public static SettingsModel LoadSettings() { if (string.IsNullOrEmpty(FilePath)) diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsViewModel.cs index 6ac9acacc4..947a025e69 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsViewModel.cs @@ -4,10 +4,9 @@ using System.Collections.ObjectModel; using System.ComponentModel; -using Microsoft.CmdPal.Core.Common.Services; using Microsoft.CmdPal.UI.ViewModels.Services; using Microsoft.CmdPal.UI.ViewModels.Settings; -using Microsoft.Extensions.DependencyInjection; +using Microsoft.CommandPalette.Extensions.Toolkit; namespace Microsoft.CmdPal.UI.ViewModels; @@ -27,7 +26,7 @@ public partial class SettingsViewModel : INotifyPropertyChanged ]; private readonly SettingsModel _settings; - private readonly IServiceProvider _serviceProvider; + private readonly TopLevelCommandManager _topLevelCommandManager; public event PropertyChangedEventHandler? PropertyChanged; @@ -174,38 +173,76 @@ public partial class SettingsViewModel : INotifyPropertyChanged } } - public ObservableCollection CommandProviders { get; } = []; + public ObservableCollection CommandProviders { get; } = new(); + + public ObservableCollection FallbackRankings { get; set; } = new(); public SettingsExtensionsViewModel Extensions { get; } - public SettingsViewModel(SettingsModel settings, IServiceProvider serviceProvider, TaskScheduler scheduler) + public SettingsViewModel(SettingsModel settings, TopLevelCommandManager topLevelCommandManager, TaskScheduler scheduler, IThemeService themeService) { _settings = settings; - _serviceProvider = serviceProvider; + _topLevelCommandManager = topLevelCommandManager; - var themeService = serviceProvider.GetRequiredService(); Appearance = new AppearanceSettingsViewModel(themeService, _settings); var activeProviders = GetCommandProviders(); var allProviderSettings = _settings.ProviderSettings; + var fallbacks = new List(); + var currentRankings = _settings.FallbackRanks; + var needsSave = false; + foreach (var item in activeProviders) { var providerSettings = settings.GetProviderSettings(item); - var settingsModel = new ProviderSettingsViewModel(item, providerSettings, _serviceProvider); + var settingsModel = new ProviderSettingsViewModel(item, providerSettings, _settings); CommandProviders.Add(settingsModel); + + fallbacks.AddRange(settingsModel.FallbackCommands); } + var fallbackRankings = new List>(fallbacks.Count); + foreach (var fallback in fallbacks) + { + var index = currentRankings.IndexOf(fallback.Id); + var score = fallbacks.Count; + + if (index >= 0) + { + score = index; + } + + fallbackRankings.Add(new Scored() { Item = fallback, Score = score }); + + if (index == -1) + { + needsSave = true; + } + } + + FallbackRankings = new ObservableCollection(fallbackRankings.OrderBy(o => o.Score).Select(fr => fr.Item)); Extensions = new SettingsExtensionsViewModel(CommandProviders, scheduler); + + if (needsSave) + { + ApplyFallbackSort(); + } } private IEnumerable GetCommandProviders() { - var manager = _serviceProvider.GetService()!; - var allProviders = manager.CommandProviders; + var allProviders = _topLevelCommandManager.CommandProviders; return allProviders; } + public void ApplyFallbackSort() + { + _settings.FallbackRanks = FallbackRankings.Select(s => s.Id).ToArray(); + Save(); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FallbackRankings))); + } + private void Save() => SettingsModel.SaveSettings(_settings); } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelViewModel.cs index fcba26ade8..6d7f830658 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelViewModel.cs @@ -4,11 +4,9 @@ using System.Collections.ObjectModel; using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Messaging; using ManagedCommon; using Microsoft.CmdPal.Core.ViewModels; using Microsoft.CmdPal.Core.ViewModels.Messages; -using Microsoft.CmdPal.UI.ViewModels.Messages; using Microsoft.CmdPal.UI.ViewModels.Settings; using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions.Toolkit; @@ -27,7 +25,9 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx private readonly string _commandProviderId; - private string IdFromModel => _commandItemViewModel.Command.Id; + private string IdFromModel => IsFallback && !string.IsNullOrWhiteSpace(_fallbackId) ? _fallbackId : _commandItemViewModel.Command.Id; + + private string _fallbackId = string.Empty; private string _generatedId = string.Empty; @@ -41,7 +41,7 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx [ObservableProperty] public partial ObservableCollection Tags { get; set; } = []; - public string Id => string.IsNullOrEmpty(IdFromModel) ? _generatedId : IdFromModel; + public string Id => string.IsNullOrWhiteSpace(IdFromModel) ? _generatedId : IdFromModel; public CommandPaletteHost ExtensionHost { get; private set; } @@ -158,14 +158,20 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx public bool IsEnabled { - get => _providerSettings.IsFallbackEnabled(this); - set + get { - if (value != IsEnabled) + if (IsFallback) { - _providerSettings.SetFallbackEnabled(this, value); - Save(); - WeakReferenceMessenger.Default.Send(new()); + if (_providerSettings.FallbackCommands.TryGetValue(_fallbackId, out var fallbackSettings)) + { + return fallbackSettings.IsEnabled; + } + + return true; + } + else + { + return _providerSettings.IsEnabled; } } } @@ -177,7 +183,8 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx string commandProviderId, SettingsModel settings, ProviderSettings providerSettings, - IServiceProvider serviceProvider) + IServiceProvider serviceProvider, + ICommandItem? commandItem) { _serviceProvider = serviceProvider; _settings = settings; @@ -187,6 +194,10 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx IsFallback = isFallback; ExtensionHost = extensionHost; + if (isFallback && commandItem is FallbackCommandItem fallback) + { + _fallbackId = fallback.Id; + } item.PropertyChanged += Item_PropertyChanged; diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/FallbackRanker.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/FallbackRanker.xaml new file mode 100644 index 0000000000..89610e53d4 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/FallbackRanker.xaml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/FallbackRanker.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/FallbackRanker.xaml.cs new file mode 100644 index 0000000000..57ad2c1d8c --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/FallbackRanker.xaml.cs @@ -0,0 +1,31 @@ +// 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 Microsoft.CmdPal.UI.ViewModels; +using Microsoft.CmdPal.UI.ViewModels.Services; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.UI.Xaml.Controls; + +namespace Microsoft.CmdPal.UI.Controls; + +public sealed partial class FallbackRanker : UserControl +{ + private readonly TaskScheduler _mainTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext(); + private SettingsViewModel? viewModel; + + public FallbackRanker() + { + this.InitializeComponent(); + + var settings = App.Current.Services.GetService()!; + var topLevelCommandManager = App.Current.Services.GetService()!; + var themeService = App.Current.Services.GetService()!; + viewModel = new SettingsViewModel(settings, topLevelCommandManager, _mainTaskScheduler, themeService); + } + + private void ListView_DragItemsCompleted(ListViewBase sender, DragItemsCompletedEventArgs args) + { + viewModel?.ApplyFallbackSort(); + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/FallbackRankerDialog.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/FallbackRankerDialog.xaml new file mode 100644 index 0000000000..bbdf2f85ca --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/FallbackRankerDialog.xaml @@ -0,0 +1,43 @@ + + + + + + + + 800 + + + + + + + + + + + + + diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/FallbackRankerDialog.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/FallbackRankerDialog.xaml.cs new file mode 100644 index 0000000000..a5186609be --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/FallbackRankerDialog.xaml.cs @@ -0,0 +1,33 @@ +// 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.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices.WindowsRuntime; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Controls.Primitives; +using Microsoft.UI.Xaml.Data; +using Microsoft.UI.Xaml.Input; +using Microsoft.UI.Xaml.Media; +using Microsoft.UI.Xaml.Navigation; +using Windows.Foundation; +using Windows.Foundation.Collections; + +namespace Microsoft.CmdPal.UI.Controls; + +public sealed partial class FallbackRankerDialog : UserControl +{ + public FallbackRankerDialog() + { + InitializeComponent(); + } + + public IAsyncOperation ShowAsync() + { + return FallbackRankerContentDialog!.ShowAsync()!; + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj b/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj index 54961a5828..79ce3ff5ee 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj @@ -71,6 +71,7 @@ + @@ -231,6 +232,12 @@ + + + MSBuild:Compile + + + Designer diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/PowerToysRootPageService.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/PowerToysRootPageService.cs index d14dd391bd..9a877358f0 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/PowerToysRootPageService.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/PowerToysRootPageService.cs @@ -10,7 +10,6 @@ using Microsoft.CmdPal.Core.ViewModels; using Microsoft.CmdPal.UI.ViewModels; using Microsoft.CmdPal.UI.ViewModels.MainPage; using Microsoft.CommandPalette.Extensions; -using Microsoft.Extensions.DependencyInjection; using WinRT; // To learn more about WinUI, the WinUI project structure, @@ -19,24 +18,24 @@ namespace Microsoft.CmdPal.UI; internal sealed class PowerToysRootPageService : IRootPageService { - private readonly IServiceProvider _serviceProvider; + private readonly TopLevelCommandManager _tlcManager; + private IExtensionWrapper? _activeExtension; private Lazy _mainListPage; - public PowerToysRootPageService(IServiceProvider serviceProvider) + public PowerToysRootPageService(TopLevelCommandManager topLevelCommandManager, SettingsModel settings, AliasManager aliasManager, AppStateModel appStateModel) { - _serviceProvider = serviceProvider; + _tlcManager = topLevelCommandManager; _mainListPage = new Lazy(() => { - return new MainListPage(_serviceProvider); + return new MainListPage(_tlcManager, settings, aliasManager, appStateModel); }); } public async Task PreLoadAsync() { - var tlcManager = _serviceProvider.GetService()!; - await tlcManager.LoadBuiltinsAsync(); + await _tlcManager.LoadBuiltinsAsync(); } public Microsoft.CommandPalette.Extensions.IPage GetRootPage() @@ -46,13 +45,11 @@ internal sealed class PowerToysRootPageService : IRootPageService public async Task PostLoadRootPageAsync() { - var tlcManager = _serviceProvider.GetService()!; - // After loading built-ins, and starting navigation, kick off a thread to load extensions. - tlcManager.LoadExtensionsCommand.Execute(null); + _tlcManager.LoadExtensionsCommand.Execute(null); - await tlcManager.LoadExtensionsCommand.ExecutionTask!; - if (tlcManager.LoadExtensionsCommand.ExecutionTask.Status != TaskStatus.RanToCompletion) + await _tlcManager.LoadExtensionsCommand.ExecutionTask!; + if (_tlcManager.LoadExtensionsCommand.ExecutionTask.Status != TaskStatus.RanToCompletion) { // TODO: Handle failure case } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/AppearancePage.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/AppearancePage.xaml.cs index 39a8ea4ae1..c2fd4a1b29 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/AppearancePage.xaml.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/AppearancePage.xaml.cs @@ -5,6 +5,7 @@ using System.Diagnostics; using ManagedCommon; using Microsoft.CmdPal.UI.ViewModels; +using Microsoft.CmdPal.UI.ViewModels.Services; using Microsoft.Extensions.DependencyInjection; using Microsoft.UI; using Microsoft.UI.Xaml; @@ -28,7 +29,9 @@ public sealed partial class AppearancePage : Page InitializeComponent(); var settings = App.Current.Services.GetService()!; - ViewModel = new SettingsViewModel(settings, App.Current.Services, _mainTaskScheduler); + var themeService = App.Current.Services.GetRequiredService(); + var topLevelCommandManager = App.Current.Services.GetService()!; + ViewModel = new SettingsViewModel(settings, topLevelCommandManager, _mainTaskScheduler, themeService); } private async void PickBackgroundImage_Click(object sender, RoutedEventArgs e) diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/ExtensionPage.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/ExtensionPage.xaml index 72b51fd724..b5b219fe43 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/ExtensionPage.xaml +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/ExtensionPage.xaml @@ -122,33 +122,65 @@ - + + + + + + + + + + + + + + + - - - + + + - - + + - + + + + + + + @@ -198,7 +230,6 @@ Text="{x:Bind ViewModel.ExtensionVersion}" /> - @@ -217,5 +248,6 @@ + diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/ExtensionPage.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/ExtensionPage.xaml.cs index 2bfdc1bcb3..ac05f356b3 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/ExtensionPage.xaml.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/ExtensionPage.xaml.cs @@ -25,4 +25,9 @@ public sealed partial class ExtensionPage : Page ? vm : throw new ArgumentException($"{nameof(ExtensionPage)} navigation args should be passed a {nameof(ProviderSettingsViewModel)}"); } + + private async void RankButton_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) + { + await FallbackRankerDialog.ShowAsync(); + } } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/ExtensionsPage.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/ExtensionsPage.xaml index 9e9600bd6e..af529a1bab 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/ExtensionsPage.xaml +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/ExtensionsPage.xaml @@ -177,6 +177,12 @@ + + + + + + + diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/ExtensionsPage.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/ExtensionsPage.xaml.cs index e99296bad9..f19be9f0cf 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/ExtensionsPage.xaml.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/ExtensionsPage.xaml.cs @@ -4,7 +4,9 @@ using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.WinUI.Controls; +using ManagedCommon; using Microsoft.CmdPal.UI.ViewModels; +using Microsoft.CmdPal.UI.ViewModels.Services; using Microsoft.Extensions.DependencyInjection; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; @@ -23,7 +25,9 @@ public sealed partial class ExtensionsPage : Page this.InitializeComponent(); var settings = App.Current.Services.GetService()!; - viewModel = new SettingsViewModel(settings, App.Current.Services, _mainTaskScheduler); + var topLevelCommandManager = App.Current.Services.GetService()!; + var themeService = App.Current.Services.GetService()!; + viewModel = new SettingsViewModel(settings, topLevelCommandManager, _mainTaskScheduler, themeService); } private void SettingsCard_Click(object sender, RoutedEventArgs e) @@ -42,4 +46,16 @@ public sealed partial class ExtensionsPage : Page SearchBox?.Focus(FocusState.Keyboard); args.Handled = true; } + + private async void MenuFlyoutItem_OnClick(object sender, RoutedEventArgs e) + { + try + { + await FallbackRankerDialog!.ShowAsync(); + } + catch (Exception ex) + { + Logger.LogError("Error when showing FallbackRankerDialog", ex); + } + } } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/GeneralPage.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/GeneralPage.xaml.cs index d732600c4e..e2a5b7938c 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/GeneralPage.xaml.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/GeneralPage.xaml.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using Microsoft.CmdPal.UI.ViewModels; +using Microsoft.CmdPal.UI.ViewModels.Services; using Microsoft.Extensions.DependencyInjection; using Microsoft.UI.Xaml.Controls; using Windows.ApplicationModel; @@ -20,7 +21,9 @@ public sealed partial class GeneralPage : Page this.InitializeComponent(); var settings = App.Current.Services.GetService()!; - viewModel = new SettingsViewModel(settings, App.Current.Services, _mainTaskScheduler); + var topLevelCommandManager = App.Current.Services.GetService()!; + var themeService = App.Current.Services.GetService()!; + viewModel = new SettingsViewModel(settings, topLevelCommandManager, _mainTaskScheduler, themeService); } public string ApplicationVersion diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Strings/en-us/Resources.resw b/src/modules/cmdpal/Microsoft.CmdPal.UI/Strings/en-us/Resources.resw index b2c0260a94..5de0447d35 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Strings/en-us/Resources.resw +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Strings/en-us/Resources.resw @@ -1,17 +1,17 @@  - @@ -706,4 +706,22 @@ Right-click to remove the key combination, thereby deactivating the shortcut. Use system settings + + Include in the Global result + + + Show results on queries without direct activation command + + + Manage fallback order + + + Manage fallback order + + + Drag items to set which fallback commands run first; commands at the top take priority. + + + Manage fallback order + \ No newline at end of file diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.UI.ViewModels.UnitTests/MainListPageResultFactoryTests.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.UI.ViewModels.UnitTests/MainListPageResultFactoryTests.cs index 624fa2da73..5a1e4bff54 100644 --- a/src/modules/cmdpal/Tests/Microsoft.CmdPal.UI.ViewModels.UnitTests/MainListPageResultFactoryTests.cs +++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.UI.ViewModels.UnitTests/MainListPageResultFactoryTests.cs @@ -96,7 +96,7 @@ public partial class MainListPageResultFactoryTests var titles = result.Select(r => r.Title).ToArray(); #pragma warning disable CA1861 // Avoid constant arrays as arguments CollectionAssert.AreEqual( - new[] { "F1", "SF1", "A1", "SF2", "A2", "F2", "FB1", "FB2" }, + new[] { "F1", "SF1", "A1", "SF2", "A2", "F2", "Fallbacks", "FB1", "FB2" }, titles); #pragma warning restore CA1861 // Avoid constant arrays as arguments } @@ -129,7 +129,6 @@ public partial class MainListPageResultFactoryTests var fallbacks = new List> { S("FB1", 0), - S(string.Empty, 0), S("FB3", 0), }; @@ -140,9 +139,10 @@ public partial class MainListPageResultFactoryTests fallbacks, appResultLimit: 10); - Assert.AreEqual(2, result.Length); - Assert.AreEqual("FB1", result[0].Title); - Assert.AreEqual("FB3", result[1].Title); + Assert.AreEqual(3, result.Length); + Assert.AreEqual("Fallbacks", result[0].Title); + Assert.AreEqual("FB1", result[1].Title); + Assert.AreEqual("FB3", result[2].Title); } [TestMethod] diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Pages/FallbackCalculatorItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Pages/FallbackCalculatorItem.cs index 4367c67810..ebf33094f2 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Pages/FallbackCalculatorItem.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Pages/FallbackCalculatorItem.cs @@ -10,11 +10,12 @@ namespace Microsoft.CmdPal.Ext.Calc.Pages; public sealed partial class FallbackCalculatorItem : FallbackCommandItem { + private const string _id = "com.microsoft.cmdpal.builtin.calculator.fallback"; private readonly CopyTextCommand _copyCommand = new(string.Empty); private readonly ISettingsInterface _settings; public FallbackCalculatorItem(ISettingsInterface settings) - : base(new NoOpCommand(), Resources.calculator_title) + : base(new NoOpCommand(), Resources.calculator_title, _id) { Command = _copyCommand; _copyCommand.Name = string.Empty; diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/FallbackOpenFileItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/FallbackOpenFileItem.cs index 9e2302c630..967e962085 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/FallbackOpenFileItem.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/FallbackOpenFileItem.cs @@ -16,7 +16,8 @@ namespace Microsoft.CmdPal.Ext.Indexer; internal sealed partial class FallbackOpenFileItem : FallbackCommandItem, System.IDisposable { - private static readonly NoOpCommand _baseCommandWithId = new() { Id = "com.microsoft.indexer.fallback" }; + private const string _id = "com.microsoft.cmdpal.builtin.indexer.fallback"; + private static readonly NoOpCommand _baseCommandWithId = new() { Id = _id }; private readonly CompositeFormat fallbackItemSearchPageTitleCompositeFormat = CompositeFormat.Parse(Resources.Indexer_fallback_searchPage_title); @@ -27,7 +28,7 @@ internal sealed partial class FallbackOpenFileItem : FallbackCommandItem, System private Func _suppressCallback; public FallbackOpenFileItem() - : base(_baseCommandWithId, Resources.Indexer_Find_Path_fallback_display_title) + : base(_baseCommandWithId, Resources.Indexer_Find_Path_fallback_display_title, _id) { Title = string.Empty; Subtitle = string.Empty; diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Commands/FallbackRemoteDesktopItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Commands/FallbackRemoteDesktopItem.cs index e3692a763b..287a697c31 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Commands/FallbackRemoteDesktopItem.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Commands/FallbackRemoteDesktopItem.cs @@ -28,7 +28,7 @@ internal sealed partial class FallbackRemoteDesktopItem : FallbackCommandItem private readonly NoOpCommand _emptyCommand = new NoOpCommand(); public FallbackRemoteDesktopItem(IRdpConnectionsManager rdpConnectionsManager) - : base(Resources.remotedesktop_title) + : base(Resources.remotedesktop_title, _id) { _rdpConnectionsManager = rdpConnectionsManager; diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/FallbackExecuteItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/FallbackExecuteItem.cs index ab557ba258..46e86bfeed 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/FallbackExecuteItem.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/FallbackExecuteItem.cs @@ -11,6 +11,7 @@ namespace Microsoft.CmdPal.Ext.Shell; internal sealed partial class FallbackExecuteItem : FallbackCommandItem, IDisposable { + private const string _id = "com.microsoft.cmdpal.builtin.shell.fallback"; private static readonly char[] _systemDirectoryRoots = ['\\', '/']; private readonly Action? _addToHistory; @@ -19,8 +20,9 @@ internal sealed partial class FallbackExecuteItem : FallbackCommandItem, IDispos public FallbackExecuteItem(SettingsManager settings, Action? addToHistory, ITelemetryService telemetryService) : base( - new NoOpCommand() { Id = "com.microsoft.run.fallback" }, - ResourceLoaderInstance.GetString("shell_command_display_title")) + new NoOpCommand() { Id = _id }, + ResourceLoaderInstance.GetString("shell_command_display_title"), + _id) { Title = string.Empty; Subtitle = ResourceLoaderInstance.GetString("generic_run_command"); diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/FallbackSystemCommandItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/FallbackSystemCommandItem.cs index adaa9f7c26..8624953891 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/FallbackSystemCommandItem.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/FallbackSystemCommandItem.cs @@ -12,8 +12,10 @@ namespace Microsoft.CmdPal.Ext.System; internal sealed partial class FallbackSystemCommandItem : FallbackCommandItem { + private const string _id = "com.microsoft.cmdpal.builtin.system.fallback"; + public FallbackSystemCommandItem(ISettingsInterface settings) - : base(new NoOpCommand(), Resources.Microsoft_plugin_ext_fallback_display_title) + : base(new NoOpCommand(), Resources.Microsoft_plugin_ext_fallback_display_title, _id) { Title = string.Empty; Subtitle = string.Empty; diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/FallbackTimeDateItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/FallbackTimeDateItem.cs index d4b9994339..899163e621 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/FallbackTimeDateItem.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/FallbackTimeDateItem.cs @@ -13,12 +13,13 @@ namespace Microsoft.CmdPal.Ext.TimeDate; internal sealed partial class FallbackTimeDateItem : FallbackCommandItem { + private const string _id = "com.microsoft.cmdpal.builtin.timedate.fallback"; private readonly HashSet _validOptions; private ISettingsInterface _settingsManager; private DateTime? _timestamp; public FallbackTimeDateItem(ISettingsInterface settings, DateTime? timestamp = null) - : base(new NoOpCommand(), Resources.Microsoft_plugin_timedate_fallback_display_title) + : base(new NoOpCommand(), Resources.Microsoft_plugin_timedate_fallback_display_title, _id) { Title = string.Empty; Subtitle = string.Empty; diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/FallbackExecuteSearchItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/FallbackExecuteSearchItem.cs index 61557d996a..8ce2349e3c 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/FallbackExecuteSearchItem.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/FallbackExecuteSearchItem.cs @@ -13,6 +13,7 @@ namespace Microsoft.CmdPal.Ext.WebSearch.Commands; internal sealed partial class FallbackExecuteSearchItem : FallbackCommandItem { + private const string _id = "com.microsoft.cmdpal.builtin.websearch.execute.fallback"; private readonly SearchWebCommand _executeItem; private static readonly CompositeFormat PluginOpen = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_open); private static readonly CompositeFormat SubtitleText = System.Text.CompositeFormat.Parse(Properties.Resources.web_search_fallback_subtitle); @@ -20,7 +21,7 @@ internal sealed partial class FallbackExecuteSearchItem : FallbackCommandItem private readonly IBrowserInfoService _browserInfoService; public FallbackExecuteSearchItem(ISettingsInterface settings, IBrowserInfoService browserInfoService) - : base(new SearchWebCommand(string.Empty, settings, browserInfoService) { Id = "com.microsoft.websearch.fallback" }, Resources.command_item_title) + : base(new SearchWebCommand(string.Empty, settings, browserInfoService) { Id = _id }, Resources.command_item_title, _id) { _executeItem = (SearchWebCommand)Command!; _browserInfoService = browserInfoService; diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/FallbackOpenURLItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/FallbackOpenURLItem.cs index 7feb53b1de..cbb83c121d 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/FallbackOpenURLItem.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/FallbackOpenURLItem.cs @@ -15,13 +15,14 @@ namespace Microsoft.CmdPal.Ext.WebSearch; internal sealed partial class FallbackOpenURLItem : FallbackCommandItem { + private const string _id = "com.microsoft.cmdpal.builtin.websearch.openurl.fallback"; private readonly IBrowserInfoService _browserInfoService; private readonly OpenURLCommand _executeItem; private static readonly CompositeFormat PluginOpenURL = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_open_url); private static readonly CompositeFormat PluginOpenUrlInBrowser = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_open_url_in_browser); public FallbackOpenURLItem(ISettingsInterface settings, IBrowserInfoService browserInfoService) - : base(new OpenURLCommand(string.Empty, browserInfoService), Resources.open_url_fallback_title) + : base(new OpenURLCommand(string.Empty, browserInfoService), Resources.open_url_fallback_title, _id) { ArgumentNullException.ThrowIfNull(browserInfoService); diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsSettings/Pages/FallbackWindowsSettingsItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsSettings/Pages/FallbackWindowsSettingsItem.cs index a63a2965bd..dc7d320a75 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsSettings/Pages/FallbackWindowsSettingsItem.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsSettings/Pages/FallbackWindowsSettingsItem.cs @@ -2,11 +2,8 @@ // 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.Linq; -using System.Xml.Linq; -using Microsoft.CmdPal.Ext.WindowsSettings.Classes; using Microsoft.CmdPal.Ext.WindowsSettings.Commands; using Microsoft.CmdPal.Ext.WindowsSettings.Helpers; using Microsoft.CmdPal.Ext.WindowsSettings.Properties; @@ -16,13 +13,15 @@ namespace Microsoft.CmdPal.Ext.WindowsSettings.Pages; internal sealed partial class FallbackWindowsSettingsItem : FallbackCommandItem { + private const string _id = "com.microsoft.cmdpal.builtin.windows.settings.fallback"; + private readonly Classes.WindowsSettings _windowsSettings; private readonly string _title = Resources.settings_fallback_title; private readonly string _subtitle = Resources.settings_fallback_subtitle; public FallbackWindowsSettingsItem(Classes.WindowsSettings windowsSettings) - : base(new NoOpCommand(), Resources.settings_title) + : base(new NoOpCommand(), Resources.settings_title, _id) { Icon = Icons.WindowsSettingsIcon; _windowsSettings = windowsSettings; diff --git a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/FallbackCommandItem.cs b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/FallbackCommandItem.cs index 1c0b5c18d0..0e14ce570f 100644 --- a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/FallbackCommandItem.cs +++ b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/FallbackCommandItem.cs @@ -4,18 +4,25 @@ namespace Microsoft.CommandPalette.Extensions.Toolkit; -public partial class FallbackCommandItem : CommandItem, IFallbackCommandItem, IFallbackHandler +public partial class FallbackCommandItem : CommandItem, IFallbackCommandItem, IFallbackHandler, IFallbackCommandItem2 { private readonly IFallbackHandler? _fallbackHandler; - public FallbackCommandItem(string displayTitle) + public FallbackCommandItem(string displayTitle, string id) { DisplayTitle = displayTitle; + Id = id; } - public FallbackCommandItem(ICommand command, string displayTitle) + public FallbackCommandItem(ICommand command, string displayTitle, string id) : base(command) { + if (string.IsNullOrWhiteSpace(id)) + { + throw new ArgumentException("A non-empty or whitespace Id must be provided.", nameof(id)); + } + + Id = id; DisplayTitle = displayTitle; if (command is IFallbackHandler f) { @@ -29,6 +36,8 @@ public partial class FallbackCommandItem : CommandItem, IFallbackCommandItem, IF init => _fallbackHandler = value; } + public virtual string Id { get; } + public virtual string DisplayTitle { get; } public virtual void UpdateQuery(string query) => _fallbackHandler?.UpdateQuery(query); diff --git a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions/Microsoft.CommandPalette.Extensions.idl b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions/Microsoft.CommandPalette.Extensions.idl index b302ce6d75..d7eed3bc15 100644 --- a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions/Microsoft.CommandPalette.Extensions.idl +++ b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions/Microsoft.CommandPalette.Extensions.idl @@ -371,6 +371,11 @@ namespace Microsoft.CommandPalette.Extensions IFallbackHandler FallbackHandler{ get; }; String DisplayTitle { get; }; }; + + [contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)] + interface IFallbackCommandItem2 requires IFallbackCommandItem { + String Id { get; }; + }; [contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)] interface ICommandProvider requires Windows.Foundation.IClosable, INotifyItemsChanged