diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/AppExtensionHost.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/AppExtensionHost.cs index c3405ff84b..edb12b6d4b 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/AppExtensionHost.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/AppExtensionHost.cs @@ -166,5 +166,5 @@ public interface IAppHostService AppExtensionHost GetHostForCommand(object? context, AppExtensionHost? currentHost); - CommandProviderContext GetProviderContextForCommand(object? command, CommandProviderContext? currentContext); + ICommandProviderContext GetProviderContextForCommand(object? command, ICommandProviderContext? currentContext); } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandContextItemViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandContextItemViewModel.cs index c919e3131f..dd22d57887 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandContextItemViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandContextItemViewModel.cs @@ -22,7 +22,7 @@ public partial class CommandContextItemViewModel : CommandItemViewModel, IContex public bool HasRequestedShortcut => RequestedShortcut is not null && (RequestedShortcut.Value != nullKeyChord); public CommandContextItemViewModel(ICommandContextItem contextItem, WeakReference context) - : base(new(contextItem), context) + : base(new(contextItem), context, contextMenuFactory: null) { Model = new(contextItem); IsContextMenuItem = true; diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandItemViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandItemViewModel.cs index 42d8272349..821c762a09 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandItemViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandItemViewModel.cs @@ -103,7 +103,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa public CommandItemViewModel( ExtensionObject item, WeakReference errorContext, - IContextMenuFactory? contextMenuFactory = null) + IContextMenuFactory? contextMenuFactory) : base(errorContext) { _commandItemModel = item; @@ -464,6 +464,30 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa .ForEach(c => c.SafeCleanup()); } + public void RefreshMoreCommands() + { + Task.Run(RefreshMoreCommandsSynchronous); + } + + private void RefreshMoreCommandsSynchronous() + { + try + { + BuildAndInitMoreCommands(); + UpdateProperty(nameof(MoreCommands)); + UpdateProperty(nameof(AllCommands)); + UpdateProperty(nameof(SecondaryCommand)); + UpdateProperty(nameof(SecondaryCommandName)); + UpdateProperty(nameof(HasMoreCommands)); + } + catch (Exception ex) + { + // Handle any exceptions that might occur during the refresh process + CoreLogger.LogError("Error refreshing MoreCommands in CommandItemViewModel", ex); + ShowException(ex, _commandItemModel?.Unsafe?.Title); + } + } + protected override void UnsafeCleanup() { base.UnsafeCleanup(); diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandPaletteContentPageViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandPaletteContentPageViewModel.cs index deae412657..b0b65e7f70 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandPaletteContentPageViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandPaletteContentPageViewModel.cs @@ -8,7 +8,7 @@ namespace Microsoft.CmdPal.UI.ViewModels; public partial class CommandPaletteContentPageViewModel : ContentPageViewModel { - public CommandPaletteContentPageViewModel(IContentPage model, TaskScheduler scheduler, AppExtensionHost host, CommandProviderContext providerContext) + public CommandPaletteContentPageViewModel(IContentPage model, TaskScheduler scheduler, AppExtensionHost host, ICommandProviderContext providerContext) : base(model, scheduler, host, providerContext) { } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandPalettePageViewModelFactory.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandPalettePageViewModelFactory.cs index a8bd6899e2..83f89b007c 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandPalettePageViewModelFactory.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandPalettePageViewModelFactory.cs @@ -10,15 +10,15 @@ public class CommandPalettePageViewModelFactory : IPageViewModelFactoryService { private readonly TaskScheduler _scheduler; - private readonly IContextMenuFactory? _contextMenuFactory; + private readonly IContextMenuFactory _contextMenuFactory; - public CommandPalettePageViewModelFactory(TaskScheduler scheduler, IContextMenuFactory? contextMenuFactory) + public CommandPalettePageViewModelFactory(TaskScheduler scheduler, IContextMenuFactory contextMenuFactory) { _scheduler = scheduler; _contextMenuFactory = contextMenuFactory; } - public PageViewModel? TryCreatePageViewModel(IPage page, bool nested, AppExtensionHost host, CommandProviderContext providerContext) + public PageViewModel? TryCreatePageViewModel(IPage page, bool nested, AppExtensionHost host, ICommandProviderContext providerContext) { return page switch { diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandProviderContext.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandProviderContext.cs index 125d841a63..ce42487fe1 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandProviderContext.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandProviderContext.cs @@ -4,9 +4,21 @@ namespace Microsoft.CmdPal.UI.ViewModels; -public sealed class CommandProviderContext +public static class CommandProviderContext { - public required string ProviderId { get; init; } + public static ICommandProviderContext Empty { get; } = new EmptyCommandProviderContext(); - public static CommandProviderContext Empty { get; } = new() { ProviderId = "" }; + private sealed class EmptyCommandProviderContext : ICommandProviderContext + { + public string ProviderId => ""; + + public bool SupportsPinning => false; + } +} + +public interface ICommandProviderContext +{ + string ProviderId { get; } + + bool SupportsPinning { get; } } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandProviderWrapper.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandProviderWrapper.cs index 2785dc1d9e..9b31c83f26 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandProviderWrapper.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandProviderWrapper.cs @@ -13,7 +13,7 @@ using Windows.Foundation; namespace Microsoft.CmdPal.UI.ViewModels; -public sealed class CommandProviderWrapper +public sealed class CommandProviderWrapper : ICommandProviderContext { public bool IsExtension => Extension is not null; @@ -47,12 +47,17 @@ public sealed class CommandProviderWrapper public string ProviderId => string.IsNullOrEmpty(Extension?.ExtensionUniqueId) ? Id : Extension.ExtensionUniqueId; + public bool SupportsPinning { get; private set; } + + public TopLevelItemPageContext TopLevelPageContext { get; } + public CommandProviderWrapper(ICommandProvider provider, TaskScheduler mainThread) { // This ctor is only used for in-proc builtin commands. So the Unsafe! // calls are pretty dang safe actually. _commandProvider = new(provider); _taskScheduler = mainThread; + TopLevelPageContext = new TopLevelItemPageContext(this, _taskScheduler); // Hook the extension back into us ExtensionHost = new CommandPaletteHost(provider); @@ -77,6 +82,7 @@ public sealed class CommandProviderWrapper { _taskScheduler = mainThread; _commandProviderCache = commandProviderCache; + TopLevelPageContext = new TopLevelItemPageContext(this, _taskScheduler); Extension = extension; ExtensionHost = new CommandPaletteHost(extension); @@ -121,7 +127,7 @@ public sealed class CommandProviderWrapper return settings.GetProviderSettings(this); } - public async Task LoadTopLevelCommands(IServiceProvider serviceProvider, WeakReference pageContext) + public async Task LoadTopLevelCommands(IServiceProvider serviceProvider) { if (!isValid) { @@ -157,8 +163,14 @@ public sealed class CommandProviderWrapper UnsafePreCacheApiAdditions(two); } - // Load pinned commands from saved settings - var pinnedCommands = LoadPinnedCommands(model, providerSettings); + ICommandItem[] pinnedCommands = []; + if (model is ICommandProvider4 four) + { + SupportsPinning = true; + + // Load pinned commands from saved settings + pinnedCommands = LoadPinnedCommands(four, providerSettings); + } Id = model.Id; DisplayName = model.DisplayName; @@ -177,7 +189,7 @@ public sealed class CommandProviderWrapper Settings = new(model.Settings, this, _taskScheduler); // We do need to explicitly initialize commands though - InitializeCommands(commands, fallbacks, pinnedCommands, serviceProvider, pageContext); + InitializeCommands(commands, fallbacks, pinnedCommands, serviceProvider); Logger.LogDebug($"Loaded commands from {DisplayName} ({ProviderId})"); } @@ -208,14 +220,20 @@ public sealed class CommandProviderWrapper } } - private void InitializeCommands(ICommandItem[] commands, IFallbackCommandItem[] fallbacks, ICommandItem[] pinnedCommands, IServiceProvider serviceProvider, WeakReference pageContext) + private void InitializeCommands( + ICommandItem[] commands, + IFallbackCommandItem[] fallbacks, + ICommandItem[] pinnedCommands, + IServiceProvider serviceProvider) { var settings = serviceProvider.GetService()!; + var contextMenuFactory = serviceProvider.GetService()!; var providerSettings = GetProviderSettings(settings); var ourContext = GetProviderContext(); + var pageContext = new WeakReference(TopLevelPageContext); var makeAndAdd = (ICommandItem? i, bool fallback) => { - CommandItemViewModel commandItemViewModel = new(new(i), pageContext); + CommandItemViewModel commandItemViewModel = new(new(i), pageContext, contextMenuFactory: contextMenuFactory); TopLevelViewModel topLevelViewModel = new(commandItemViewModel, fallback, ExtensionHost, ourContext, settings, providerSettings, serviceProvider, i); topLevelViewModel.InitializeProperties(); @@ -244,27 +262,24 @@ public sealed class CommandProviderWrapper } } - private ICommandItem[] LoadPinnedCommands(ICommandProvider model, ProviderSettings providerSettings) + private ICommandItem[] LoadPinnedCommands(ICommandProvider4 model, ProviderSettings providerSettings) { var pinnedItems = new List(); - if (model is ICommandProvider4 provider4) + foreach (var pinnedId in providerSettings.PinnedCommandIds) { - foreach (var pinnedId in providerSettings.PinnedCommandIds) + try { - try + var commandItem = model.GetCommandItem(pinnedId); + if (commandItem is not null) { - var commandItem = provider4.GetCommandItem(pinnedId); - if (commandItem is not null) - { - pinnedItems.Add(commandItem); - } - } - catch (Exception e) - { - Logger.LogError($"Failed to load pinned command {pinnedId}: {e.Message}"); + pinnedItems.Add(commandItem); } } + catch (Exception e) + { + Logger.LogError($"Failed to load pinned command {pinnedId}: {e.Message}"); + } } return pinnedItems.ToArray(); @@ -298,11 +313,22 @@ public sealed class CommandProviderWrapper } } - public CommandProviderContext GetProviderContext() + public void UnpinCommand(string commandId, IServiceProvider serviceProvider) { - return new() { ProviderId = ProviderId }; + var settings = serviceProvider.GetService()!; + var providerSettings = GetProviderSettings(settings); + + if (providerSettings.PinnedCommandIds.Remove(commandId)) + { + SettingsModel.SaveSettings(settings); + + // Raise CommandsChanged so the TopLevelCommandManager reloads our commands + this.CommandsChanged?.Invoke(this, new ItemsChangedEventArgs(-1)); + } } + public ICommandProviderContext GetProviderContext() => this; + public override bool Equals(object? obj) => obj is CommandProviderWrapper wrapper && isValid == wrapper.isValid; public override int GetHashCode() => _commandProvider.GetHashCode(); 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 5001dc2386..45bb9ac14a 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/MainListPage.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/MainListPage.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation +// 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. @@ -11,7 +11,6 @@ using Microsoft.CmdPal.Common.Helpers; using Microsoft.CmdPal.Common.Text; using Microsoft.CmdPal.Ext.Apps; using Microsoft.CmdPal.Ext.Apps.Programs; -using Microsoft.CmdPal.Ext.Apps.State; using Microsoft.CmdPal.UI.ViewModels.Commands; using Microsoft.CmdPal.UI.ViewModels.Messages; using Microsoft.CmdPal.UI.ViewModels.Properties; @@ -409,11 +408,13 @@ public sealed partial class MainListPage : DynamicListPage, var allNewApps = AllAppsCommandProvider.Page.GetItems().Cast().ToList(); // We need to remove pinned apps from allNewApps so they don't show twice. - var pinnedApps = PinnedAppsManager.Instance.GetPinnedAppIdentifiers(); + // Pinned app command IDs are stored in ProviderSettings.PinnedCommandIds. + _settings.ProviderSettings.TryGetValue(AllAppsCommandProvider.WellKnownId, out var providerSettings); + var pinnedCommandIds = providerSettings?.PinnedCommandIds; - if (pinnedApps.Length > 0) + if (pinnedCommandIds is not null && pinnedCommandIds.Count > 0) { - newApps = allNewApps.Where(w => pinnedApps.IndexOf(w.AppIdentifier) < 0); + newApps = allNewApps.Where(li => li.Command != null && !pinnedCommandIds.Contains(li.Command.Id)); } else { diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContentPageViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContentPageViewModel.cs index 9be0209144..da34915712 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContentPageViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContentPageViewModel.cs @@ -47,7 +47,7 @@ public partial class ContentPageViewModel : PageViewModel, ICommandBarContext // Remember - "observable" properties from the model (via PropChanged) // cannot be marked [ObservableProperty] - public ContentPageViewModel(IContentPage model, TaskScheduler scheduler, AppExtensionHost host, CommandProviderContext providerContext) + public ContentPageViewModel(IContentPage model, TaskScheduler scheduler, AppExtensionHost host, ICommandProviderContext providerContext) : base(model, scheduler, host, providerContext) { _model = new(model); diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/GlobalLogPageContext.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/GlobalLogPageContext.cs index fde1a36817..4425661d3a 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/GlobalLogPageContext.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/GlobalLogPageContext.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation +// 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. @@ -8,6 +8,8 @@ public class GlobalLogPageContext : IPageContext { public TaskScheduler Scheduler { get; private init; } + ICommandProviderContext IPageContext.ProviderContext => CommandProviderContext.Empty; + public void ShowException(Exception ex, string? extensionHint) { /*do nothing*/ } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Icons.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Icons.cs new file mode 100644 index 0000000000..1950c62060 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Icons.cs @@ -0,0 +1,18 @@ +// 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.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.UI.ViewModels; + +public static class Icons +{ + public static IconInfo PinIcon => new("\uE718"); // Pin icon + + public static IconInfo UnpinIcon => new("\uE77A"); // Unpin icon + + public static IconInfo SettingsIcon => new("\uE713"); // Settings icon + + public static IconInfo EditIcon => new("\uE70F"); // Edit icon +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ListItemViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ListItemViewModel.cs index dd7e4a4096..21cdba9797 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ListItemViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ListItemViewModel.cs @@ -63,7 +63,7 @@ public partial class ListItemViewModel : CommandItemViewModel } } - public ListItemViewModel(IListItem model, WeakReference context, IContextMenuFactory? contextMenuFactory = null) + public ListItemViewModel(IListItem model, WeakReference context, IContextMenuFactory contextMenuFactory) : base(new(model), context, contextMenuFactory) { Model = new ExtensionObject(model); diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ListViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ListViewModel.cs index 9ee59ae7de..099a3490fc 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ListViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ListViewModel.cs @@ -36,7 +36,7 @@ public partial class ListViewModel : PageViewModel, IDisposable private readonly ExtensionObject _model; private readonly Lock _listLock = new(); - private readonly IContextMenuFactory? _contextMenuFactory; + private readonly IContextMenuFactory _contextMenuFactory; private InterlockedBoolean _isLoading; private bool _isFetching; @@ -96,12 +96,12 @@ public partial class ListViewModel : PageViewModel, IDisposable } } - public ListViewModel(IListPage model, TaskScheduler scheduler, AppExtensionHost host, CommandProviderContext providerContext, IContextMenuFactory? contextMenuFactory) + public ListViewModel(IListPage model, TaskScheduler scheduler, AppExtensionHost host, ICommandProviderContext providerContext, IContextMenuFactory contextMenuFactory) : base(model, scheduler, host, providerContext) { _model = new(model); _contextMenuFactory = contextMenuFactory; - EmptyContent = new(new(null), PageContext, _contextMenuFactory); + EmptyContent = new(new(null), PageContext, contextMenuFactory: null); } private void FiltersPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) @@ -243,7 +243,7 @@ public partial class ListViewModel : PageViewModel, IDisposable continue; } - var viewModel = new ListItemViewModel(item, new(this)); + var viewModel = new ListItemViewModel(item, new(this), _contextMenuFactory); // If an item fails to load, silently ignore it. if (viewModel.SafeFastInit()) @@ -636,7 +636,7 @@ public partial class ListViewModel : PageViewModel, IDisposable UpdateProperty(nameof(SearchText)); UpdateProperty(nameof(InitialSearchText)); - EmptyContent = new(new(model.EmptyContent), PageContext); + EmptyContent = new(new(model.EmptyContent), PageContext, _contextMenuFactory); EmptyContent.SlowInitializeProperties(); Filters?.PropertyChanged -= FiltersPropertyChanged; @@ -732,7 +732,7 @@ public partial class ListViewModel : PageViewModel, IDisposable SearchText = model.SearchText; break; case nameof(EmptyContent): - EmptyContent = new(new(model.EmptyContent), PageContext); + EmptyContent = new(new(model.EmptyContent), PageContext, contextMenuFactory: null); EmptyContent.SlowInitializeProperties(); break; case nameof(Filters): @@ -806,7 +806,7 @@ public partial class ListViewModel : PageViewModel, IDisposable base.UnsafeCleanup(); EmptyContent?.SafeCleanup(); - EmptyContent = new(new(null), PageContext); // necessary? + EmptyContent = new(new(null), PageContext, contextMenuFactory: null); // necessary? _cancellationTokenSource?.Cancel(); filterCancellationTokenSource?.Cancel(); diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/UnpinCommandItemMessage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/UnpinCommandItemMessage.cs new file mode 100644 index 0000000000..25371516b3 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/UnpinCommandItemMessage.cs @@ -0,0 +1,9 @@ +// 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. + +namespace Microsoft.CmdPal.UI.ViewModels.Messages; + +public record UnpinCommandItemMessage(string ProviderId, string CommandId) +{ +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/PageViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/PageViewModel.cs index df3090d51c..a56b9fcc76 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/PageViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/PageViewModel.cs @@ -76,9 +76,9 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext public IconInfoViewModel Icon { get; protected set; } - public CommandProviderContext ProviderContext { get; protected set; } + public ICommandProviderContext ProviderContext { get; protected set; } - public PageViewModel(IPage? model, TaskScheduler scheduler, AppExtensionHost extensionHost, CommandProviderContext providerContext) + public PageViewModel(IPage? model, TaskScheduler scheduler, AppExtensionHost extensionHost, ICommandProviderContext providerContext) : base(scheduler) { InitializeSelfAsPageContext(); @@ -267,6 +267,8 @@ public interface IPageContext void ShowException(Exception ex, string? extensionHint = null); TaskScheduler Scheduler { get; } + + ICommandProviderContext ProviderContext { get; } } public interface IPageViewModelFactoryService @@ -278,5 +280,5 @@ public interface IPageViewModelFactoryService /// Indicates whether the page is not the top-level page. /// The command palette host that will host the page (for status messages) /// A new instance of the page view model. - PageViewModel? TryCreatePageViewModel(IPage page, bool nested, AppExtensionHost host, CommandProviderContext providerContext); + PageViewModel? TryCreatePageViewModel(IPage page, bool nested, AppExtensionHost host, ICommandProviderContext providerContext); } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandManager.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandManager.cs index 5b26bab952..a2526bd3f4 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandManager.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandManager.cs @@ -22,7 +22,7 @@ namespace Microsoft.CmdPal.UI.ViewModels; public partial class TopLevelCommandManager : ObservableObject, IRecipient, IRecipient, - IPageContext, + IRecipient, IDisposable { private readonly IServiceProvider _serviceProvider; @@ -34,8 +34,6 @@ public partial class TopLevelCommandManager : ObservableObject, private readonly Lock _commandProvidersLock = new(); private readonly SupersedingAsyncGate _reloadCommandsGate; - TaskScheduler IPageContext.Scheduler => _taskScheduler; - public TopLevelCommandManager(IServiceProvider serviceProvider, ICommandProviderCache commandProviderCache) { _serviceProvider = serviceProvider; @@ -43,6 +41,7 @@ public partial class TopLevelCommandManager : ObservableObject, _taskScheduler = _serviceProvider.GetService()!; WeakReferenceMessenger.Default.Register(this); WeakReferenceMessenger.Default.Register(this); + WeakReferenceMessenger.Default.Register(this); _reloadCommandsGate = new(ReloadAllCommandsAsyncCore); } @@ -103,9 +102,7 @@ public partial class TopLevelCommandManager : ObservableObject, // May be called from a background thread private async Task> LoadTopLevelCommandsFromProvider(CommandProviderWrapper commandProvider) { - WeakReference weakSelf = new(this); - - await commandProvider.LoadTopLevelCommands(_serviceProvider, weakSelf); + await commandProvider.LoadTopLevelCommands(_serviceProvider); var commands = await Task.Factory.StartNew( () => @@ -152,8 +149,7 @@ public partial class TopLevelCommandManager : ObservableObject, /// an awaitable task private async Task UpdateCommandsForProvider(CommandProviderWrapper sender, IItemsChangedEventArgs args) { - WeakReference weakSelf = new(this); - await sender.LoadTopLevelCommands(_serviceProvider, weakSelf); + await sender.LoadTopLevelCommands(_serviceProvider); List newItems = [.. sender.TopLevelItems]; foreach (var i in sender.FallbackItems) @@ -421,7 +417,13 @@ public partial class TopLevelCommandManager : ObservableObject, wrapper?.PinCommand(message.CommandId, _serviceProvider); } - private CommandProviderWrapper? LookupProvider(string providerId) + public void Receive(UnpinCommandItemMessage message) + { + var wrapper = LookupProvider(message.ProviderId); + wrapper?.UnpinCommand(message.CommandId, _serviceProvider); + } + + public CommandProviderWrapper? LookupProvider(string providerId) { lock (_commandProvidersLock) { @@ -430,12 +432,6 @@ public partial class TopLevelCommandManager : ObservableObject, } } - void IPageContext.ShowException(Exception ex, string? extensionHint) - { - var message = DiagnosticsHelper.BuildExceptionMessage(ex, extensionHint ?? "TopLevelCommandManager"); - CommandPaletteHost.Instance.Log(message); - } - internal bool IsProviderActive(string id) { lock (_commandProvidersLock) diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelItemPageContext.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelItemPageContext.cs new file mode 100644 index 0000000000..ef65fb336a --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelItemPageContext.cs @@ -0,0 +1,36 @@ +// 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.Common.Helpers; + +namespace Microsoft.CmdPal.UI.ViewModels; + +/// +/// Used as the PageContext for top-level items. Top level items are displayed +/// on the MainListPage, which _we_ own. We need to have a placeholder page +/// context for each provider that still connects those top-level items to the +/// CommandProvider they came from. +/// +public partial class TopLevelItemPageContext : IPageContext +{ + public TaskScheduler Scheduler { get; private set; } + + public ICommandProviderContext ProviderContext { get; private set; } + + TaskScheduler IPageContext.Scheduler => Scheduler; + + ICommandProviderContext IPageContext.ProviderContext => ProviderContext; + + internal TopLevelItemPageContext(CommandProviderWrapper provider, TaskScheduler scheduler) + { + ProviderContext = provider.GetProviderContext(); + Scheduler = scheduler; + } + + public void ShowException(Exception ex, string? extensionHint = null) + { + var message = DiagnosticsHelper.BuildExceptionMessage(ex, extensionHint ?? $"TopLevelItemPageContext({ProviderContext.ProviderId})"); + CommandPaletteHost.Instance.Log(message); + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelViewModel.cs index 650c62fc77..ee419364b2 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelViewModel.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation +// 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. @@ -26,7 +26,7 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx private readonly IServiceProvider _serviceProvider; private readonly CommandItemViewModel _commandItemViewModel; - public CommandProviderContext ProviderContext { get; private set; } + public ICommandProviderContext ProviderContext { get; private set; } private string IdFromModel => IsFallback && !string.IsNullOrWhiteSpace(_fallbackId) ? _fallbackId : _commandItemViewModel.Command.Id; @@ -189,7 +189,7 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx CommandItemViewModel item, bool isFallback, CommandPaletteHost extensionHost, - CommandProviderContext commandProviderContext, + ICommandProviderContext commandProviderContext, SettingsModel settings, ProviderSettings providerSettings, IServiceProvider serviceProvider, diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/CommandPaletteContextMenuFactory.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/CommandPaletteContextMenuFactory.cs index 684d39299a..36f1c1b8c8 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/CommandPaletteContextMenuFactory.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/CommandPaletteContextMenuFactory.cs @@ -2,8 +2,13 @@ // 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.Messaging; +using ManagedCommon; using Microsoft.CmdPal.UI.ViewModels; +using Microsoft.CmdPal.UI.ViewModels.Messages; using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; +using RS_ = Microsoft.CmdPal.UI.Helpers.ResourceLoaderInstance; namespace Microsoft.CmdPal.UI; @@ -24,8 +29,164 @@ internal sealed partial class CommandPaletteContextMenuFactory : IContextMenuFac { var results = DefaultContextMenuFactory.Instance.UnsafeBuildAndInitMoreCommands(items, commandItem); - // TODO: #45201 Here, we'll want to add pin/unpin commands for pinning - // items to the top-level or to the dock. + List moreCommands = []; + var itemId = commandItem.Command.Id; + + if (commandItem.PageContext.TryGetTarget(out var page) && + page.ProviderContext.SupportsPinning && + !string.IsNullOrEmpty(itemId)) + { + // Add pin/unpin commands for pinning items to the top-level or to + // the dock. + var providerId = page.ProviderContext.ProviderId; + if (_topLevelCommandManager.LookupProvider(providerId) is CommandProviderWrapper provider) + { + var providerSettings = _settingsModel.GetProviderSettings(provider); + + var alreadyPinnedToTopLevel = providerSettings.PinnedCommandIds.Contains(itemId); + + // Don't add pin/unpin commands for items displayed as + // TopLevelViewModels that aren't already pinned. + // + // We can't look up if this command item is in the top level + // items in the manager, because we are being called _before_ we + // get added to the manager's list of commands. + var isTopLevelItem = page is TopLevelItemPageContext; + + if (!isTopLevelItem || alreadyPinnedToTopLevel) + { + var pinToTopLevelCommand = new PinToCommand( + commandId: itemId, + providerId: providerId, + pin: !alreadyPinnedToTopLevel, + PinLocation.TopLevel, + _settingsModel, + _topLevelCommandManager); + + var contextItem = new PinToContextItem(pinToTopLevelCommand, commandItem); + moreCommands.Add(contextItem); + } + } + } + + if (moreCommands.Count > 0) + { + moreCommands.Insert(0, new Separator()); + var moreResults = DefaultContextMenuFactory.Instance.UnsafeBuildAndInitMoreCommands(moreCommands.ToArray(), commandItem); + results.AddRange(moreResults); + } + return results; } + + internal enum PinLocation + { + TopLevel, + Dock, + } + + private sealed partial class PinToContextItem : CommandContextItem + { + private readonly PinToCommand _command; + private readonly CommandItemViewModel _commandItem; + + public PinToContextItem(PinToCommand command, CommandItemViewModel commandItem) + : base(command) + { + _command = command; + _commandItem = commandItem; + command.PinStateChanged += this.OnPinStateChanged; + } + + private void OnPinStateChanged(object? sender, EventArgs e) + { + // update our MoreCommands + _commandItem.RefreshMoreCommands(); + } + + ~PinToContextItem() + { + _command.PinStateChanged -= this.OnPinStateChanged; + } + } + + private sealed partial class PinToCommand : InvokableCommand + { + private readonly string _commandId; + private readonly string _providerId; + private readonly SettingsModel _settings; + private readonly TopLevelCommandManager _topLevelCommandManager; + private readonly bool _pin; + private readonly PinLocation _pinLocation; + + public override IconInfo Icon => _pin ? Icons.PinIcon : Icons.UnpinIcon; + + public override string Name => _pin ? RS_.GetString("top_level_pin_command_name") : RS_.GetString("top_level_unpin_command_name"); + + internal event EventHandler? PinStateChanged; + + public PinToCommand( + string commandId, + string providerId, + bool pin, + PinLocation pinLocation, + SettingsModel settings, + TopLevelCommandManager topLevelCommandManager) + { + _commandId = commandId; + _providerId = providerId; + _pinLocation = pinLocation; + _settings = settings; + _topLevelCommandManager = topLevelCommandManager; + _pin = pin; + } + + public override CommandResult Invoke() + { + Logger.LogDebug($"PinTo{_pinLocation}Command.Invoke({_pin}): {_providerId}/{_commandId}"); + if (_pin) + { + switch (_pinLocation) + { + case PinLocation.TopLevel: + PinToTopLevel(); + break; + + // TODO: After dock is added: + // case PinLocation.Dock: + // PinToDock(); + // break; + } + } + else + { + switch (_pinLocation) + { + case PinLocation.TopLevel: + UnpinFromTopLevel(); + break; + + // case PinLocation.Dock: + // UnpinFromDock(); + // break; + } + } + + PinStateChanged?.Invoke(this, EventArgs.Empty); + + return CommandResult.KeepOpen(); + } + + private void PinToTopLevel() + { + PinCommandItemMessage message = new(_providerId, _commandId); + WeakReferenceMessenger.Default.Send(message); + } + + private void UnpinFromTopLevel() + { + UnpinCommandItemMessage message = new(_providerId, _commandId); + WeakReferenceMessenger.Default.Send(message); + } + } } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/PowerToysAppHostService.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/PowerToysAppHostService.cs index 1b4e617939..9d6a13d2db 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/PowerToysAppHostService.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/PowerToysAppHostService.cs @@ -26,9 +26,9 @@ internal sealed class PowerToysAppHostService : IAppHostService return topLevelHost ?? currentHost ?? CommandPaletteHost.Instance; } - public CommandProviderContext GetProviderContextForCommand(object? command, CommandProviderContext? currentContext) + public ICommandProviderContext GetProviderContextForCommand(object? command, ICommandProviderContext? currentContext) { - CommandProviderContext? topLevelId = null; + ICommandProviderContext? topLevelId = null; if (command is TopLevelViewModel topLevelViewModel) { topLevelId = topLevelViewModel.ProviderContext; 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 d82aa20b96..5c53fef5a7 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 @@ -801,10 +801,26 @@ Right-click to remove the key combination, thereby deactivating the shortcut.K Keyboard key - + Configure shortcut Assign shortcut + + Pin to home + Command name for pinning an item to the top level list of commands + + + Unpin from home + Command name for unpinning an item from the top level list of commands + + + Pin to dock + Command name for pinning an item to the dock + + + Unpin from dock + Command name for unpinning an item from the dock + \ No newline at end of file diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Apps.UnitTests/AllAppsPageTests.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Apps.UnitTests/AllAppsPageTests.cs index 3ac1eaff68..3dcb802362 100644 --- a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Apps.UnitTests/AllAppsPageTests.cs +++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Apps.UnitTests/AllAppsPageTests.cs @@ -73,25 +73,4 @@ public class AllAppsPageTests : AppsTestBase Assert.IsTrue(items.Any(i => i.Title == "Notepad")); Assert.IsTrue(items.Any(i => i.Title == "Calculator")); } - - [TestMethod] - public async Task AllAppsPage_GetPinnedApps_ReturnsEmptyWhenNoAppsArePinned() - { - // Arrange - var mockCache = new MockAppCache(); - var app = TestDataHelper.CreateTestWin32Program("TestApp", "C:\\TestApp.exe"); - mockCache.AddWin32Program(app); - - var page = new AllAppsPage(mockCache); - - // Wait a bit for initialization to complete - await Task.Delay(100); - - // Act - var pinnedApps = page.GetPinnedApps(); - - // Assert - Assert.IsNotNull(pinnedApps); - Assert.AreEqual(0, pinnedApps.Length); - } } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AllAppsCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AllAppsCommandProvider.cs index 6d46a3bb7b..22236428fe 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AllAppsCommandProvider.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AllAppsCommandProvider.cs @@ -7,7 +7,6 @@ using System.Collections.Generic; using Microsoft.CmdPal.Ext.Apps.Helpers; using Microsoft.CmdPal.Ext.Apps.Programs; using Microsoft.CmdPal.Ext.Apps.Properties; -using Microsoft.CmdPal.Ext.Apps.State; using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions.Toolkit; @@ -40,14 +39,11 @@ public partial class AllAppsCommandProvider : CommandProvider { MoreCommands = [new CommandContextItem(AllAppsSettings.Instance.Settings.SettingsPage)], }; - - // Subscribe to pin state changes to refresh the command provider - PinnedAppsManager.Instance.PinStateChanged += OnPinStateChanged; } public static int TopLevelResultLimit => AllAppsSettings.Instance.SearchResultLimit ?? DefaultResultLimit; - public override ICommandItem[] TopLevelCommands() => [_listItem, .. _page.GetPinnedApps()]; + public override ICommandItem[] TopLevelCommands() => [_listItem]; public ICommandItem? LookupAppByPackageFamilyName(string packageFamilyName, bool requireSingleMatch) { @@ -178,9 +174,4 @@ public partial class AllAppsCommandProvider : CommandProvider return null; } - - private void OnPinStateChanged(object? sender, System.EventArgs e) - { - RaiseItemsChanged(0); - } } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AllAppsPage.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AllAppsPage.cs index 2a264f70c2..83ce3397ca 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AllAppsPage.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AllAppsPage.cs @@ -10,7 +10,6 @@ using System.Threading.Tasks; using ManagedCommon; using Microsoft.CmdPal.Ext.Apps.Programs; using Microsoft.CmdPal.Ext.Apps.Properties; -using Microsoft.CmdPal.Ext.Apps.State; using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions.Toolkit; @@ -21,9 +20,7 @@ public sealed partial class AllAppsPage : ListPage private readonly Lock _listLock = new(); private readonly IAppCache _appCache; - private AppItem[] allApps = []; - private AppListItem[] unpinnedApps = []; - private AppListItem[] pinnedApps = []; + private AppListItem[] allAppListItems = []; public AllAppsPage() : this(AppCache.Instance.Value) @@ -39,9 +36,6 @@ public sealed partial class AllAppsPage : ListPage this.IsLoading = true; this.PlaceholderText = Resources.search_installed_apps_placeholder; - // Subscribe to pin state changes to refresh the command provider - PinnedAppsManager.Instance.PinStateChanged += OnPinStateChanged; - Task.Run(() => { lock (_listLock) @@ -51,24 +45,17 @@ public sealed partial class AllAppsPage : ListPage }); } - internal AppListItem[] GetPinnedApps() - { - BuildListItems(); - return pinnedApps; - } - public override IListItem[] GetItems() { // Build or update the list if needed BuildListItems(); - AppListItem[] allApps = [.. pinnedApps, .. unpinnedApps]; - return allApps; + return allAppListItems; } private void BuildListItems() { - if (allApps.Length == 0 || _appCache.ShouldReload()) + if (allAppListItems.Length == 0 || _appCache.ShouldReload()) { lock (_listLock) { @@ -77,10 +64,7 @@ public sealed partial class AllAppsPage : ListPage Stopwatch stopwatch = new(); stopwatch.Start(); - var apps = GetPrograms(); - this.allApps = apps.AllApps; - this.pinnedApps = apps.PinnedItems; - this.unpinnedApps = apps.UnpinnedItems; + this.allAppListItems = GetPrograms(); this.IsLoading = false; @@ -92,15 +76,15 @@ public sealed partial class AllAppsPage : ListPage } } - private AppItem[] GetAllApps() + private AppListItem[] GetPrograms() { - List allApps = new(); + var items = new List(); foreach (var uwpApp in _appCache.UWPs) { if (uwpApp.Enabled) { - allApps.Add(uwpApp.ToAppItem()); + items.Add(new AppListItem(uwpApp.ToAppItem(), true)); } } @@ -108,101 +92,12 @@ public sealed partial class AllAppsPage : ListPage { if (win32App.Enabled && win32App.Valid) { - allApps.Add(win32App.ToAppItem()); + items.Add(new AppListItem(win32App.ToAppItem(), true)); } } - return [.. allApps]; - } + items.Sort((a, b) => string.Compare(a.Title, b.Title, StringComparison.Ordinal)); - internal (AppItem[] AllApps, AppListItem[] PinnedItems, AppListItem[] UnpinnedItems) GetPrograms() - { - var allApps = GetAllApps(); - var pinned = new List(); - var unpinned = new List(); - - foreach (var app in allApps) - { - var isPinned = PinnedAppsManager.Instance.IsAppPinned(app.AppIdentifier); - var appListItem = new AppListItem(app, true, isPinned); - - if (isPinned) - { - appListItem.Tags = [.. appListItem.Tags, new Tag() { Icon = Icons.PinIcon }]; - pinned.Add(appListItem); - } - else - { - unpinned.Add(appListItem); - } - } - - pinned.Sort((a, b) => string.Compare(a.Title, b.Title, StringComparison.Ordinal)); - unpinned.Sort((a, b) => string.Compare(a.Title, b.Title, StringComparison.Ordinal)); - - return ( - allApps, - pinned.ToArray(), - unpinned.ToArray() - ); - } - - private void OnPinStateChanged(object? sender, PinStateChangedEventArgs e) - { - /* - * Rebuilding all the lists is pretty expensive. - * So, instead, we'll just compare pinned items to move existing - * items between the two lists. - */ - AppItem? existingAppItem = null; - - foreach (var app in allApps) - { - if (app.AppIdentifier == e.AppIdentifier) - { - existingAppItem = app; - break; - } - } - - if (existingAppItem is not null) - { - var appListItem = new AppListItem(existingAppItem, true, e.IsPinned); - - var newPinned = new List(pinnedApps); - var newUnpinned = new List(unpinnedApps); - - if (e.IsPinned) - { - newPinned.Add(appListItem); - - foreach (var app in newUnpinned) - { - if (app.AppIdentifier == e.AppIdentifier) - { - newUnpinned.Remove(app); - break; - } - } - } - else - { - newUnpinned.Add(appListItem); - - foreach (var app in newPinned) - { - if (app.AppIdentifier == e.AppIdentifier) - { - newPinned.Remove(app); - break; - } - } - } - - pinnedApps = newPinned.ToArray(); - unpinnedApps = newUnpinned.ToArray(); - } - - RaiseItemsChanged(0); + return [.. items]; } } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AppListItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AppListItem.cs index 3e551d7b5b..426bfcccba 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AppListItem.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AppListItem.cs @@ -87,7 +87,7 @@ public sealed partial class AppListItem : ListItem, IPrecomputedListItem public AppItem App => _app; - public AppListItem(AppItem app, bool useThumbnails, bool isPinned) + public AppListItem(AppItem app, bool useThumbnails) { Command = _appCommand = new AppCommand(app); _app = app; @@ -95,7 +95,7 @@ public sealed partial class AppListItem : ListItem, IPrecomputedListItem Subtitle = app.Subtitle; Icon = Icons.GenericAppIcon; - MoreCommands = AddPinCommands(_app.Commands!, isPinned); + MoreCommands = _app.Commands?.ToArray() ?? []; _detailsLoadTask = new Lazy>(BuildDetails); _iconLoadTask = new Lazy>(async () => await FetchIcon(useThumbnails).ConfigureAwait(false)); @@ -237,35 +237,6 @@ public sealed partial class AppListItem : ListItem, IPrecomputedListItem return icon; } - private IContextItem[] AddPinCommands(List commands, bool isPinned) - { - var newCommands = new List(); - newCommands.AddRange(commands); - - newCommands.Add(new Separator()); - - if (isPinned) - { - newCommands.Add( - new CommandContextItem( - new UnpinAppCommand(this.AppIdentifier)) - { - RequestedShortcut = KeyChords.TogglePin, - }); - } - else - { - newCommands.Add( - new CommandContextItem( - new PinAppCommand(this.AppIdentifier)) - { - RequestedShortcut = KeyChords.TogglePin, - }); - } - - return newCommands.ToArray(); - } - private async Task TryLoadThumbnail(string path, bool jumbo, bool logOnFailure) { return await Task.Run(async () => diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Commands/PinAppCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Commands/PinAppCommand.cs deleted file mode 100644 index 8311e36cfc..0000000000 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Commands/PinAppCommand.cs +++ /dev/null @@ -1,28 +0,0 @@ -// 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.Diagnostics.CodeAnalysis; -using Microsoft.CmdPal.Ext.Apps.Properties; -using Microsoft.CmdPal.Ext.Apps.State; -using Microsoft.CommandPalette.Extensions.Toolkit; - -namespace Microsoft.CmdPal.Ext.Apps.Commands; - -internal sealed partial class PinAppCommand : InvokableCommand -{ - private readonly string _appIdentifier; - - public PinAppCommand(string appIdentifier) - { - _appIdentifier = appIdentifier; - Name = Resources.pin_app; - Icon = Icons.PinIcon; - } - - public override CommandResult Invoke() - { - PinnedAppsManager.Instance.PinApp(_appIdentifier); - return CommandResult.KeepOpen(); - } -} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Commands/UnpinAppCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Commands/UnpinAppCommand.cs deleted file mode 100644 index fcba03f3d3..0000000000 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Commands/UnpinAppCommand.cs +++ /dev/null @@ -1,27 +0,0 @@ -// 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.Ext.Apps.Properties; -using Microsoft.CmdPal.Ext.Apps.State; -using Microsoft.CommandPalette.Extensions.Toolkit; - -namespace Microsoft.CmdPal.Ext.Apps.Commands; - -internal sealed partial class UnpinAppCommand : InvokableCommand -{ - private readonly string _appIdentifier; - - public UnpinAppCommand(string appIdentifier) - { - _appIdentifier = appIdentifier; - Name = Resources.unpin_app; - Icon = Icons.UnpinIcon; - } - - public override CommandResult Invoke() - { - PinnedAppsManager.Instance.UnpinApp(_appIdentifier); - return CommandResult.KeepOpen(); - } -} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/JsonSerializationContext.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/JsonSerializationContext.cs index 0db868222c..5cc4b98b3e 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/JsonSerializationContext.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/JsonSerializationContext.cs @@ -4,12 +4,10 @@ using System.Collections.Generic; using System.Text.Json.Serialization; -using Microsoft.CmdPal.Ext.Apps.State; namespace Microsoft.CmdPal.Ext.Apps; [JsonSerializable(typeof(string))] -[JsonSerializable(typeof(PinnedApps))] [JsonSerializable(typeof(List), TypeInfoPropertyName = "StringList")] [JsonSourceGenerationOptions(UseStringEnumConverter = true, WriteIndented = true, IncludeFields = true, PropertyNameCaseInsensitive = true, AllowTrailingCommas = true)] internal sealed partial class JsonSerializationContext : JsonSerializerContext diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/State/PinnedApps.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/State/PinnedApps.cs deleted file mode 100644 index ff76043bf1..0000000000 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/State/PinnedApps.cs +++ /dev/null @@ -1,47 +0,0 @@ -// 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.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Text.Json; - -namespace Microsoft.CmdPal.Ext.Apps.State; - -public sealed class PinnedApps -{ - public List PinnedAppIdentifiers { get; set; } = []; - - public static PinnedApps ReadFromFile(string path) - { - if (!File.Exists(path)) - { - return new PinnedApps(); - } - - try - { - var jsonString = File.ReadAllText(path); - var result = JsonSerializer.Deserialize(jsonString, JsonSerializationContext.Default.PinnedApps); - return result ?? new PinnedApps(); - } - catch - { - return new PinnedApps(); - } - } - - public static void WriteToFile(string path, PinnedApps data) - { - try - { - var jsonString = JsonSerializer.Serialize(data, JsonSerializationContext.Default.PinnedApps); - File.WriteAllText(path, jsonString); - } - catch - { - // Silently fail - we don't want pinning issues to crash the extension - } - } -} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/State/PinnedAppsManager.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/State/PinnedAppsManager.cs deleted file mode 100644 index 0fdc0a934c..0000000000 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/State/PinnedAppsManager.cs +++ /dev/null @@ -1,80 +0,0 @@ -// 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.IO; -using ManagedCommon; -using Microsoft.CommandPalette.Extensions.Toolkit; - -namespace Microsoft.CmdPal.Ext.Apps.State; - -public sealed class PinnedAppsManager -{ - private static readonly Lazy _instance = new(() => new PinnedAppsManager()); - private readonly string _pinnedAppsFilePath; - - public static PinnedAppsManager Instance => _instance.Value; - - private PinnedApps _pinnedApps = new(); - - // Add event for when pinning state changes - public event EventHandler? PinStateChanged; - - private PinnedAppsManager() - { - _pinnedAppsFilePath = GetPinnedAppsFilePath(); - LoadPinnedApps(); - } - - public bool IsAppPinned(string appIdentifier) - { - return _pinnedApps.PinnedAppIdentifiers.IndexOf(appIdentifier) >= 0; - } - - public void PinApp(string appIdentifier) - { - if (!IsAppPinned(appIdentifier)) - { - _pinnedApps.PinnedAppIdentifiers.Add(appIdentifier); - SavePinnedApps(); - Logger.LogTrace($"Pinned app: {appIdentifier}"); - PinStateChanged?.Invoke(this, new PinStateChangedEventArgs(appIdentifier, true)); - } - } - - public string[] GetPinnedAppIdentifiers() - { - return _pinnedApps.PinnedAppIdentifiers.ToArray(); - } - - public void UnpinApp(string appIdentifier) - { - var removed = _pinnedApps.PinnedAppIdentifiers.RemoveAll(id => - string.Equals(id, appIdentifier, StringComparison.OrdinalIgnoreCase)); - - if (removed > 0) - { - SavePinnedApps(); - Logger.LogTrace($"Unpinned app: {appIdentifier}"); - PinStateChanged?.Invoke(this, new PinStateChangedEventArgs(appIdentifier, false)); - } - } - - private void LoadPinnedApps() - { - _pinnedApps = PinnedApps.ReadFromFile(_pinnedAppsFilePath); - } - - private void SavePinnedApps() - { - PinnedApps.WriteToFile(_pinnedAppsFilePath, _pinnedApps); - } - - private static string GetPinnedAppsFilePath() - { - var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal"); - Directory.CreateDirectory(directory); - return Path.Combine(directory, "apps.pinned.json"); - } -}