Compare commits

...

8 Commits

Author SHA1 Message Date
Mike Griese
3bfc9e7148 RIIIIIIIIIGHT that's why this doesn't work 2025-04-15 12:55:21 -05:00
Mike Griese
f7cc54094b Merge branch 'dev/migrie/f/provider-settings' into dev/migrie/f/everyone-is-a-fallback 2025-04-15 06:57:22 -05:00
Mike Griese
95efc4eafe straggler? 2025-04-15 06:57:02 -05:00
Mike Griese
dfcbd28dc7 Merge remote-tracking branch 'origin/main' into dev/migrie/f/provider-settings 2025-04-15 06:56:55 -05:00
Mike Griese
bc85237594 What if every list page could be a fallback handler
This does that. If a top-level command is a FallbackHandler, then we
will yeet the SearchText at that command as we open it up.

This needs UI treatment, it needs all sorts of stuff, but overall - it's
close.
2025-04-15 06:04:02 -05:00
Mike Griese
7492662d5b Merge remote-tracking branch 'origin/main' into dev/migrie/f/provider-settings 2025-04-14 14:50:00 -05:00
Mike Griese
76556d2012 do the whole thing 2025-04-10 12:36:56 -05:00
Mike Griese
ca4c3b49ce in the SUI, but not done 2025-04-10 06:07:06 -05:00
23 changed files with 286 additions and 55 deletions

View File

@@ -169,13 +169,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;
};
@@ -194,21 +194,6 @@ public sealed class CommandProviderWrapper
}
}
/* This is a View/ExtensionHost piece
* public void AllowSetForeground(bool allow)
{
if (!IsExtension)
{
return;
}
var iextn = extensionWrapper?.GetExtensionObject();
unsafe
{
PInvoke.CoAllowSetForegroundWindow(iextn);
}
}*/
public override bool Equals(object? obj) => obj is CommandProviderWrapper wrapper && isValid == wrapper.isValid;
public override int GetHashCode() => _commandProvider.GetHashCode();

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

@@ -11,7 +11,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

@@ -142,8 +142,11 @@ public partial class MainListPage : DynamicListPage,
foreach (var command in commands)
{
var changedVisibility = command.SafeUpdateFallbackTextSynchronous(newSearch);
needsToUpdate = needsToUpdate || changedVisibility;
if (command.IsExplicitFallback)
{
var changedVisibility = command.SafeUpdateFallbackTextSynchronous(newSearch);
needsToUpdate = needsToUpdate || changedVisibility;
}
}
if (needsToUpdate)
@@ -181,7 +184,7 @@ public partial class MainListPage : DynamicListPage,
var extensionDisplayName = string.Empty;
if (topLevelOrAppItem is TopLevelViewModel topLevel)
{
isFallback = topLevel.IsFallback;
isFallback = topLevel.IsExplicitFallback || topLevel.IsImplicitFallback;
if (topLevel.HasAlias)
{
var alias = topLevel.AliasText;

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 /*command.IsExplicitFallback*/;
}
public void SetFallbackEnabled(TopLevelViewModel command, bool enabled)
{
FallbackCommands[command.Id] = enabled;
}
}

View File

@@ -23,7 +23,7 @@ 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 ? $"{ExtensionName}, {TopLevelCommands.Count + FallbackCommands.Count} commands" : Resources.builtin_disabled_extension;
[MemberNotNullWhen(true, nameof(Extension))]
public bool IsFromExtension => _provider.Extension != null;
@@ -89,5 +89,28 @@ public partial class ProviderSettingsViewModel(
return [.. providersCommands];
}
[field: AllowNull]
public List<TopLevelViewModel> FallbackCommands
{
get
{
if (field == null)
{
field = BuildFallbackViewModels();
}
return field;
}
}
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);
}

View File

@@ -140,6 +140,14 @@ public partial class ShellViewModel(IServiceProvider _serviceProvider, TaskSched
{
_mainListPage.UpdateHistory(listItem);
}
if (message.Context is TopLevelViewModel topLevel)
{
if (topLevel.IsImplicitFallback)
{
topLevel.SafeUpdateFallbackTextSynchronous(_mainListPage.SearchText);
}
}
}
public void SetActiveExtension(IExtensionWrapper? extension)

View File

@@ -66,18 +66,6 @@ public partial class TopLevelCommandManager : ObservableObject,
await commandProvider.LoadTopLevelCommands(_serviceProvider, weakSelf);
var settings = _serviceProvider.GetService<SettingsModel>()!;
var makeAndAdd = (ICommandItem? i, bool fallback) =>
{
var commandItemViewModel = new CommandItemViewModel(new(i), weakSelf);
var topLevelViewModel = new TopLevelViewModel(commandItemViewModel, fallback, commandProvider.ExtensionHost, commandProvider.ProviderId, settings, _serviceProvider);
lock (TopLevelCommands)
{
TopLevelCommands.Add(topLevelViewModel);
}
};
await Task.Factory.StartNew(
() =>
{
@@ -90,7 +78,10 @@ public partial class TopLevelCommandManager : ObservableObject,
foreach (var item in commandProvider.FallbackItems)
{
TopLevelCommands.Add(item);
if (item.IsEnabled)
{
TopLevelCommands.Add(item);
}
}
}
},
@@ -160,7 +151,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;
@@ -30,7 +33,9 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem
private CommandAlias? Alias { get; set; }
public bool IsFallback { get; private set; }
public bool IsExplicitFallback { get; private set; }
public bool IsImplicitFallback { get; private set; }
[ObservableProperty]
public partial ObservableCollection<Tag> Tags { get; set; } = [];
@@ -66,6 +71,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;
@@ -119,20 +127,36 @@ 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;
IsFallback = isFallback;
IsExplicitFallback = isFallback;
ExtensionHost = extensionHost;
item.PropertyChanged += Item_PropertyChanged;
@@ -142,6 +166,39 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem
// UpdateTags();
}
internal void InitializeProperties()
{
ItemViewModel.SlowInitializeProperties();
if (IsExplicitFallback)
{
var model = _commandItemViewModel.Model.Unsafe;
// RPC to check type
if (model is IFallbackCommandItem fallback)
{
DisplayTitle = fallback.DisplayTitle;
}
}
if (!IsExplicitFallback)
{
var model = _commandItemViewModel.Model.Unsafe;
// RPC to check type
var isFallbackHandler = model is IFallbackHandler fallback;
var isList = model?.Command is IListPage list;
var isFallbackCommandItem = model is IFallbackCommandItem fb;
if (isFallbackHandler || isFallbackCommandItem)
{
IsImplicitFallback = true;
Logger.LogDebug($"Found implicit fallback: {Title} ({isFallbackHandler}, {isList}, {isFallbackCommandItem})");
}
Logger.LogDebug($"{Title}: ({isFallbackHandler}, {isList}, {isFallbackCommandItem})");
}
}
private void Item_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (!string.IsNullOrEmpty(e.PropertyName))
@@ -226,7 +283,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}";
}
@@ -244,14 +301,19 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem
internal bool SafeUpdateFallbackTextSynchronous(string newQuery)
{
if (!IsFallback)
if (!IsExplicitFallback && !IsImplicitFallback)
{
return false;
}
if (!IsEnabled)
{
return false;
}
try
{
return UnsafeUpdateFallbackSynchronous(newQuery);
return IsExplicitFallback ? UnsafeUpdateFallbackCommandItemSynchronous(newQuery) : UnsafeUpdateImplicitFallbackSynchronous(newQuery);
}
catch (Exception ex)
{
@@ -267,7 +329,7 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem
/// </summary>
/// <param name="newQuery">The new search text to pass to the extension</param>
/// <returns>true if our Title changed across this call</returns>
private bool UnsafeUpdateFallbackSynchronous(string newQuery)
private bool UnsafeUpdateFallbackCommandItemSynchronous(string newQuery)
{
var model = _commandItemViewModel.Model.Unsafe;
@@ -284,4 +346,22 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem
return false;
}
private bool UnsafeUpdateImplicitFallbackSynchronous(string newQuery)
{
var model = _commandItemViewModel.Model.Unsafe;
// RPC to check type
if (model is IFallbackHandler fallback)
{
var wasEmpty = string.IsNullOrEmpty(Title);
// RPC for method
fallback.UpdateQuery(newQuery);
var isEmpty = string.IsNullOrEmpty(Title);
return wasEmpty != isEmpty;
}
return false;
}
}

View File

@@ -63,7 +63,9 @@
<TextBlock x:Uid="ExtensionCommandsHeader" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<ItemsRepeater ItemsSource="{x:Bind ViewModel.TopLevelCommands, Mode=OneWay}" Layout="{StaticResource VerticalStackLayout}">
<ItemsRepeater
ItemsSource="{x:Bind ViewModel.TopLevelCommands, Mode=OneWay}"
Layout="{StaticResource VerticalStackLayout}">
<ItemsRepeater.ItemTemplate>
<DataTemplate x:DataType="viewmodels:TopLevelViewModel">
<controls:SettingsExpander
@@ -107,6 +109,38 @@
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
<TextBlock x:Uid="ExtensionFallbackCommandsHeader" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<ItemsRepeater
ItemsSource="{x:Bind ViewModel.FallbackCommands, Mode=OneWay}"
Layout="{StaticResource VerticalStackLayout}">
<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

@@ -140,11 +140,11 @@ public sealed partial class SaveCommand : InvokableCommand
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "This is sample code")]
internal sealed partial class FallbackCalculatorItem : FallbackCommandItem
{
private readonly CopyTextCommand _copyCommand = new(string.Empty);
private readonly CopyTextCommand _copyCommand = new(string.Empty) { Id = "com.microsoft.calculator.fallback" };
private static readonly IconInfo _cachedIcon = IconHelpers.FromRelativePath("Assets\\Calculator.svg");
public FallbackCalculatorItem()
: base(new NoOpCommand(), Resources.calculator_title)
: base(new NoOpCommand(), Resources.calculator_displayTitle_text)
{
Command = _copyCommand;
_copyCommand.Name = string.Empty;

View File

@@ -78,6 +78,15 @@ namespace Microsoft.CmdPal.Ext.Calc.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Quickly evaluate typed equations.
/// </summary>
public static string calculator_displayTitle_text {
get {
return ResourceManager.GetString("calculator_displayTitle_text", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Error: {0}.
/// </summary>

View File

@@ -140,4 +140,7 @@
<data name="calculator_copy_command_name" xml:space="preserve">
<value>Copy</value>
</data>
<data name="calculator_displayTitle_text" xml:space="preserve">
<value>Quickly evaluate typed equations</value>
</data>
</root>

View File

@@ -12,11 +12,14 @@ namespace Microsoft.CmdPal.Ext.Indexer;
internal sealed partial class FallbackOpenFileItem : FallbackCommandItem
{
private static readonly NoOpCommand _baseCommandWithId = new() { Id = "com.microsoft.indexer.fallback" };
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

@@ -14,7 +14,9 @@ internal sealed partial class FallbackExecuteItem : FallbackCommandItem
private readonly ExecuteItem _executeItem;
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)
{
_executeItem = (ExecuteItem)this.Command!;
Title = string.Empty;

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

@@ -3,7 +3,6 @@
// See the LICENSE file in the project root for more information.
using System;
using System.IO;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -21,7 +20,7 @@ public partial class WinGetExtensionCommandsProvider : CommandProvider
}
private readonly ICommandItem[] _commands = [
new ListItem(new WinGetExtensionPage()),
new FallbackListItem(new WinGetExtensionPage()),
new ListItem(
new WinGetExtensionPage(WinGetExtensionPage.ExtensionsTag) { Title = Properties.Resources.winget_install_extensions_title })
@@ -43,4 +42,20 @@ public partial class WinGetExtensionCommandsProvider : CommandProvider
public override void InitializeWithHost(IExtensionHost host) => WinGetExtensionHost.Instance.Initialize(host);
public void SetAllLookup(Func<string, ICommandItem?> callback) => WinGetStatics.AppSearchCallback = callback;
private sealed partial class FallbackListItem : ListItem, IFallbackHandler
{
public FallbackListItem(ListPage page)
: base(page)
{
}
public void UpdateQuery(string query)
{
if (Command is ListPage page)
{
page.SearchText = query;
}
}
}
}

View File

@@ -88,7 +88,7 @@ namespace Microsoft.CmdPal.Ext.WindowsServices.Properties {
}
/// <summary>
/// Looks up a localized string similar to Open services (Ctrl+O).
/// Looks up a localized string similar to Open services.
/// </summary>
internal static string wox_plugin_service_open_services {
get {
@@ -133,7 +133,7 @@ namespace Microsoft.CmdPal.Ext.WindowsServices.Properties {
}
/// <summary>
/// Looks up a localized string similar to Restart (Ctrl+R).
/// Looks up a localized string similar to Restart.
/// </summary>
internal static string wox_plugin_service_restart {
get {

View File

@@ -2,6 +2,7 @@
// 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 Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -21,10 +22,57 @@ public partial class SamplePagesCommandsProvider : CommandProvider
Title = "Sample Pages",
Subtitle = "View example commands",
},
new FallbackCommand(new FallbackPageSample(), title: "just be sure")
{
Title = "be sure for sure",
Subtitle = "You can use this to respond to the user's search",
},
];
public override ICommandItem[] TopLevelCommands()
{
return _commands;
}
private sealed partial class FallbackCommand : FallbackCommandItem
{
public FallbackCommand(ListPage page, string title = null)
: base(page, string.IsNullOrEmpty(title) ? title : page.Title)
{
}
public override void UpdateQuery(string query)
{
if (Command is ListPage page)
{
page.SearchText = query;
}
}
}
private sealed partial class FallbackPageSample : ListPage
{
private readonly List<ListItem> _items = [];
public FallbackPageSample()
{
Title = "Fallback sample";
EmptyContent = new CommandItem() { Title = "You invoked the fallback page directly" };
}
public override IListItem[] GetItems()
{
return _items.ToArray();
}
public override string SearchText
{
get => base.SearchText;
set
{
base.SearchText = value;
_items.Insert(0, new ListItem() { Title = value, Subtitle = "This is the text you had typed on the main page" });
}
}
}
}

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
{

View File

@@ -52,6 +52,11 @@ public partial class ListItem : CommandItem, IListItem
}
}
public ListItem()
: base()
{
}
public ListItem(ICommand command)
: base(command)
{