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