CmdPal: Add settings to control which fallbacks are enabled (#40505)

This adds settings to each provider to allow us to control if individual
fallback items are enabled or not, regardless of the provider being
enabled.

This is relevant to _all the threads where disabling fallback commands
came up_

This just adds another section to each provider's settings page, with a
list of the fallback commands.

This also has nothing to do with the "top-level apps search", which is
not really a fallback command - it's its own thing.

Ref #38288. Doesn't close that, because this only controls
enable/disable, not ranking.

From here, we should be able to add a dedicated page in the SUI that
shows all the fallbacks across all providers. That's where we'll enable
the ordering.
This commit is contained in:
Mike Griese
2025-07-09 22:01:38 -05:00
committed by GitHub
parent 5c6166bc9f
commit cfa5f75862
18 changed files with 178 additions and 26 deletions

View File

@@ -177,13 +177,13 @@ public sealed class CommandProviderWrapper
private void InitializeCommands(ICommandItem[] commands, IFallbackCommandItem[] fallbacks, IServiceProvider serviceProvider, WeakReference<IPageContext> pageContext)
{
var settings = serviceProvider.GetService<SettingsModel>()!;
var providerSettings = GetProviderSettings(settings);
Func<ICommandItem?, bool, TopLevelViewModel> makeAndAdd = (ICommandItem? i, bool fallback) =>
{
CommandItemViewModel commandItemViewModel = new(new(i), pageContext);
TopLevelViewModel topLevelViewModel = new(commandItemViewModel, fallback, ExtensionHost, ProviderId, settings, serviceProvider);
topLevelViewModel.ItemViewModel.SlowInitializeProperties();
TopLevelViewModel topLevelViewModel = new(commandItemViewModel, fallback, ExtensionHost, ProviderId, settings, providerSettings, serviceProvider);
topLevelViewModel.InitializeProperties();
return topLevelViewModel;
};

View File

@@ -13,7 +13,7 @@ internal sealed partial class FallbackLogItem : FallbackCommandItem
private readonly LogMessagesPage _logMessagesPage;
public FallbackLogItem()
: base(new LogMessagesPage(), Resources.builtin_log_subtitle)
: base(new LogMessagesPage() { Id = "com.microsoft.cmdpal.log" }, Resources.builtin_log_subtitle)
{
_logMessagesPage = (LogMessagesPage)Command!;
Title = string.Empty;

View File

@@ -12,7 +12,9 @@ internal sealed partial class FallbackReloadItem : FallbackCommandItem
private readonly ReloadExtensionsCommand _reloadCommand;
public FallbackReloadItem()
: base(new ReloadExtensionsCommand(), Properties.Resources.builtin_reload_display_title)
: base(
new ReloadExtensionsCommand() { Id = "com.microsoft.cmdpal.reload" },
Properties.Resources.builtin_reload_display_title)
{
_reloadCommand = (ReloadExtensionsCommand)Command!;
Title = string.Empty;

View File

@@ -13,6 +13,7 @@ public partial class QuitCommand : InvokableCommand, IFallbackHandler
{
public QuitCommand()
{
Id = "com.microsoft.cmdpal.quit";
Icon = new IconInfo("\uE711");
}

View File

@@ -10,6 +10,8 @@ public class ProviderSettings
{
public bool IsEnabled { get; set; } = true;
public Dictionary<string, bool> FallbackCommands { get; set; } = [];
[JsonIgnore]
public string ProviderDisplayName { get; set; } = string.Empty;
@@ -42,4 +44,14 @@ public class ProviderSettings
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;
}
}

View File

@@ -25,7 +25,11 @@ public partial class ProviderSettingsViewModel(
public string ExtensionName => _provider.Extension?.ExtensionDisplayName ?? "Built-in";
public string ExtensionSubtext => IsEnabled ? $"{ExtensionName}, {TopLevelCommands.Count} commands" : Resources.builtin_disabled_extension;
public string ExtensionSubtext => IsEnabled ?
HasFallbackCommands ?
$"{ExtensionName}, {TopLevelCommands.Count} commands, {FallbackCommands.Count} fallback commands" :
$"{ExtensionName}, {TopLevelCommands.Count} commands" :
Resources.builtin_disabled_extension;
[MemberNotNullWhen(true, nameof(Extension))]
public bool IsFromExtension => _provider.Extension != null;
@@ -139,6 +143,31 @@ public partial class ProviderSettingsViewModel(
return [.. providersCommands];
}
[field: AllowNull]
public List<TopLevelViewModel> FallbackCommands
{
get
{
if (field == null)
{
field = BuildFallbackViewModels();
}
return field;
}
}
public bool HasFallbackCommands => _provider.FallbackItems?.Length > 0;
private List<TopLevelViewModel> BuildFallbackViewModels()
{
var thisProvider = _provider;
var providersCommands = thisProvider.FallbackItems;
// Remember! This comes in on the UI thread!
return [.. providersCommands];
}
private void Save() => SettingsModel.SaveSettings(_settings);
private void InitializeSettingsPage()

View File

@@ -98,19 +98,28 @@ public partial class TopLevelCommandManager : ObservableObject,
await commandProvider.LoadTopLevelCommands(_serviceProvider, weakSelf);
var settings = _serviceProvider.GetService<SettingsModel>()!;
var commands = await Task.Factory.StartNew(
() =>
{
List<TopLevelViewModel> commands = [];
foreach (var item in commandProvider.TopLevelItems)
{
TopLevelCommands.Add(item);
}
List<TopLevelViewModel> commands = [];
foreach (var item in commandProvider.FallbackItems)
{
if (item.IsEnabled)
{
TopLevelCommands.Add(item);
}
}
foreach (var item in commandProvider.TopLevelItems)
{
commands.Add(item);
}
foreach (var item in commandProvider.FallbackItems)
{
commands.Add(item);
}
return commands;
},
CancellationToken.None,
TaskCreationOptions.None,
_taskScheduler);
commandProvider.CommandsChanged -= CommandProvider_CommandsChanged;
commandProvider.CommandsChanged += CommandProvider_CommandsChanged;
@@ -176,7 +185,10 @@ public partial class TopLevelCommandManager : ObservableObject,
foreach (var i in sender.FallbackItems)
{
newItems.Add(i);
if (i.IsEnabled)
{
newItems.Add(i);
}
}
// Slice out the old commands

View File

@@ -4,7 +4,9 @@
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using ManagedCommon;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.UI.ViewModels.Settings;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -17,6 +19,7 @@ namespace Microsoft.CmdPal.UI.ViewModels;
public sealed partial class TopLevelViewModel : ObservableObject, IListItem
{
private readonly SettingsModel _settings;
private readonly ProviderSettings _providerSettings;
private readonly IServiceProvider _serviceProvider;
private readonly CommandItemViewModel _commandItemViewModel;
@@ -77,6 +80,9 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem
////// INotifyPropChanged
public event TypedEventHandler<object, IPropChangedEventArgs>? PropChanged;
// Fallback items
public string DisplayTitle { get; private set; } = string.Empty;
public HotkeySettings? Hotkey
{
get => _hotkey;
@@ -133,16 +139,32 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem
}
}
public bool IsEnabled
{
get => _providerSettings.IsFallbackEnabled(this);
set
{
if (value != IsEnabled)
{
_providerSettings.SetFallbackEnabled(this, value);
Save();
WeakReferenceMessenger.Default.Send<ReloadCommandsMessage>(new());
}
}
}
public TopLevelViewModel(
CommandItemViewModel item,
bool isFallback,
CommandPaletteHost extensionHost,
string commandProviderId,
SettingsModel settings,
ProviderSettings providerSettings,
IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
_settings = settings;
_providerSettings = providerSettings;
_commandProviderId = commandProviderId;
_commandItemViewModel = item;
@@ -156,6 +178,22 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem
// UpdateTags();
}
internal void InitializeProperties()
{
ItemViewModel.SlowInitializeProperties();
if (IsFallback)
{
var model = _commandItemViewModel.Model.Unsafe;
// RPC to check type
if (model is IFallbackCommandItem fallback)
{
DisplayTitle = fallback.DisplayTitle;
}
}
}
private void Item_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (!string.IsNullOrEmpty(e.PropertyName))
@@ -240,7 +278,7 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem
{
// Use WyHash64 to generate stable ID hashes.
// manually seeding with 0, so that the hash is stable across launches
var result = WyHash64.ComputeHash64(_commandProviderId + Title + Subtitle, seed: 0);
var result = WyHash64.ComputeHash64(_commandProviderId + DisplayTitle + Title + Subtitle, seed: 0);
_generatedId = $"{_commandProviderId}{result}";
}
@@ -263,6 +301,11 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem
return false;
}
if (!IsEnabled)
{
return false;
}
try
{
return UnsafeUpdateFallbackSynchronous(newQuery);

View File

@@ -106,6 +106,40 @@
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
<TextBlock
x:Uid="ExtensionFallbackCommandsHeader"
Style="{StaticResource SettingsSectionHeaderTextBlockStyle}"
Visibility="{x:Bind ViewModel.HasFallbackCommands}" />
<ItemsRepeater
ItemsSource="{x:Bind ViewModel.FallbackCommands, Mode=OneWay}"
Layout="{StaticResource VerticalStackLayout}"
Visibility="{x:Bind ViewModel.HasFallbackCommands}">
<ItemsRepeater.ItemTemplate>
<DataTemplate x:DataType="viewModels:TopLevelViewModel">
<controls:SettingsCard DataContext="{x:Bind}" Header="{x:Bind DisplayTitle, Mode=OneWay}">
<controls:SettingsCard.HeaderIcon>
<cpcontrols:ContentIcon>
<cpcontrols:ContentIcon.Content>
<cpcontrols:IconBox
Width="20"
Height="20"
AutomationProperties.AccessibilityView="Raw"
SourceKey="{x:Bind Icon, Mode=OneWay}"
SourceRequested="{x:Bind helpers:IconCacheProvider.SourceRequested}" />
</cpcontrols:ContentIcon.Content>
</cpcontrols:ContentIcon>
</controls:SettingsCard.HeaderIcon>
<!-- Content goes here -->
<ToggleSwitch IsOn="{x:Bind IsEnabled, Mode=TwoWay}" />
</controls:SettingsCard>
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
<TextBlock
x:Uid="ExtensionSettingsHeader"
Style="{StaticResource SettingsSectionHeaderTextBlockStyle}"

View File

@@ -241,6 +241,10 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
<value>Commands</value>
<comment>A section header for information about the app</comment>
</data>
<data name="ExtensionFallbackCommandsHeader.Text" xml:space="preserve">
<value>Fallback commands</value>
<comment>A section header for information about the commands presented to the user when the search text doesn't exactly match the name of a command.</comment>
</data>
<data name="ExtensionDisabledHeader.Text" xml:space="preserve">
<value>This extension is disabled</value>
<comment>A header to inform the user that an extension is not currently active</comment>

View File

@@ -15,6 +15,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 readonly CompositeFormat fallbackItemSearchPageTitleCompositeFormat = CompositeFormat.Parse(Resources.Indexer_fallback_searchPage_title);
private readonly SearchEngine _searchEngine = new();
@@ -22,10 +24,11 @@ internal sealed partial class FallbackOpenFileItem : FallbackCommandItem, System
private uint _queryCookie = 10;
public FallbackOpenFileItem()
: base(new NoOpCommand(), Resources.Indexer_Find_Path_fallback_display_title)
: base(_baseCommandWithId, Resources.Indexer_Find_Path_fallback_display_title)
{
Title = string.Empty;
Subtitle = string.Empty;
Icon = Icons.FileExplorer;
}
public override void UpdateQuery(string query)

View File

@@ -6,7 +6,6 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
using ManagedCommon;
using Microsoft.CmdPal.Ext.Indexer.Commands;
using Microsoft.CmdPal.Ext.Indexer.Data;

View File

@@ -15,7 +15,9 @@ internal sealed partial class FallbackExecuteItem : FallbackCommandItem
private readonly SettingsManager _settings;
public FallbackExecuteItem(SettingsManager settings)
: base(new ExecuteItem(string.Empty, settings), Resources.shell_command_display_title)
: base(
new ExecuteItem(string.Empty, settings) { Id = "com.microsoft.run.fallback" },
Resources.shell_command_display_title)
{
_settings = settings;
_executeItem = (ExecuteItem)this.Command!;

View File

@@ -17,7 +17,7 @@ internal sealed partial class FallbackExecuteSearchItem : FallbackCommandItem
private static readonly CompositeFormat PluginOpen = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_open);
public FallbackExecuteSearchItem(SettingsManager settings)
: base(new SearchWebCommand(string.Empty, settings), Resources.command_item_title)
: base(new SearchWebCommand(string.Empty, settings) { Id = "com.microsoft.websearch.fallback" }, Resources.command_item_title)
{
_executeItem = (SearchWebCommand)this.Command!;
Title = string.Empty;

View File

@@ -7,7 +7,6 @@ using System.Globalization;
using System.Text;
using Microsoft.CmdPal.Ext.WebSearch.Commands;
using Microsoft.CmdPal.Ext.WebSearch.Helpers;
using Microsoft.CmdPal.Ext.WebSearch.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit;
using BrowserInfo = Microsoft.CmdPal.Ext.WebSearch.Helpers.DefaultBrowserInfo;
@@ -20,7 +19,7 @@ internal sealed partial class FallbackOpenURLItem : FallbackCommandItem
private static readonly CompositeFormat PluginOpenUrlInBrowser = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_open_url_in_browser);
public FallbackOpenURLItem(SettingsManager settings)
: base(new OpenURLCommand(string.Empty, settings), string.Empty)
: base(new OpenURLCommand(string.Empty, settings), Properties.Resources.open_url_fallback_title)
{
_executeItem = (OpenURLCommand)this.Command!;
Title = string.Empty;

View File

@@ -132,6 +132,15 @@ namespace Microsoft.CmdPal.Ext.WebSearch.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Open URL.
/// </summary>
public static string open_url_fallback_title {
get {
return ResourceManager.GetString("open_url_fallback_title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to the default browser.
/// </summary>

View File

@@ -178,4 +178,7 @@
<data name="settings_page_name" xml:space="preserve">
<value>Settings</value>
</data>
<data name="open_url_fallback_title" xml:space="preserve">
<value>Open URL</value>
</data>
</root>

View File

@@ -18,7 +18,7 @@ public partial class Command : BaseObservable, ICommand
= string.Empty;
public virtual string Id { get; protected set; } = string.Empty;
public virtual string Id { get; set; } = string.Empty;
public virtual IconInfo Icon
{