mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-03 17:56:44 +02:00
CmdPal: Add context commands for pinning nested commands (#45673)
_targets #45572_ This change allows our contact menu factory to actually create and add additional context menu commands for pinning commands to the top level. Now for any command provider built with the latest SDK that return subcommands with an ID, we will add additional context menu commands that allows you to pin that command to the top level. <img width="540" height="181" alt="image" src="https://github.com/user-attachments/assets/6c2cfe3c-4143-44d1-9308-bfc71db4c842" /> <img width="729" height="317" alt="image" src="https://github.com/user-attachments/assets/4ff75c9f-1f35-4c1e-a03e-6fab5cbab423" /> related to https://github.com/microsoft/PowerToys/issues/45191 related to https://github.com/microsoft/PowerToys/issues/45201 This PR notably does not remove pinning from the apps extension. I thought that made sense to do as a follow-up PR for the sake of reviewability. --- description from #45676 which was merged into this Removes the code that the apps provider was using to support pinning apps to the top level list of commands. Now the all apps provider just uses the global support for pinning commands to the top level. This does have the side effect of removing the separation of pinned apps from unpinned apps on the All Apps page. However, we all pretty much agree that wasn't a particularly widely used feature, and it's safe to remove. With this, we can finally call this issue done 🎉 closes https://github.com/microsoft/PowerToys/issues/45191
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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<IPageContext> context)
|
||||
: base(new(contextItem), context)
|
||||
: base(new(contextItem), context, contextMenuFactory: null)
|
||||
{
|
||||
Model = new(contextItem);
|
||||
IsContextMenuItem = true;
|
||||
|
||||
@@ -103,7 +103,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
public CommandItemViewModel(
|
||||
ExtensionObject<ICommandItem> item,
|
||||
WeakReference<IPageContext> 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();
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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 = "<EMPTY>" };
|
||||
private sealed class EmptyCommandProviderContext : ICommandProviderContext
|
||||
{
|
||||
public string ProviderId => "<EMPTY>";
|
||||
|
||||
public bool SupportsPinning => false;
|
||||
}
|
||||
}
|
||||
|
||||
public interface ICommandProviderContext
|
||||
{
|
||||
string ProviderId { get; }
|
||||
|
||||
bool SupportsPinning { get; }
|
||||
}
|
||||
|
||||
@@ -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<IPageContext> 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<IPageContext> pageContext)
|
||||
private void InitializeCommands(
|
||||
ICommandItem[] commands,
|
||||
IFallbackCommandItem[] fallbacks,
|
||||
ICommandItem[] pinnedCommands,
|
||||
IServiceProvider serviceProvider)
|
||||
{
|
||||
var settings = serviceProvider.GetService<SettingsModel>()!;
|
||||
var contextMenuFactory = serviceProvider.GetService<IContextMenuFactory>()!;
|
||||
var providerSettings = GetProviderSettings(settings);
|
||||
var ourContext = GetProviderContext();
|
||||
var pageContext = new WeakReference<IPageContext>(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<ICommandItem>();
|
||||
|
||||
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<SettingsModel>()!;
|
||||
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();
|
||||
|
||||
@@ -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<AppListItem>().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
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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*/
|
||||
}
|
||||
|
||||
18
src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Icons.cs
Normal file
18
src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Icons.cs
Normal file
@@ -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
|
||||
}
|
||||
@@ -63,7 +63,7 @@ public partial class ListItemViewModel : CommandItemViewModel
|
||||
}
|
||||
}
|
||||
|
||||
public ListItemViewModel(IListItem model, WeakReference<IPageContext> context, IContextMenuFactory? contextMenuFactory = null)
|
||||
public ListItemViewModel(IListItem model, WeakReference<IPageContext> context, IContextMenuFactory contextMenuFactory)
|
||||
: base(new(model), context, contextMenuFactory)
|
||||
{
|
||||
Model = new ExtensionObject<IListItem>(model);
|
||||
|
||||
@@ -36,7 +36,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
private readonly ExtensionObject<IListPage> _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();
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
@@ -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
|
||||
/// <param name="nested">Indicates whether the page is not the top-level page.</param>
|
||||
/// <param name="host">The command palette host that will host the page (for status messages)</param>
|
||||
/// <returns>A new instance of the page view model.</returns>
|
||||
PageViewModel? TryCreatePageViewModel(IPage page, bool nested, AppExtensionHost host, CommandProviderContext providerContext);
|
||||
PageViewModel? TryCreatePageViewModel(IPage page, bool nested, AppExtensionHost host, ICommandProviderContext providerContext);
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
public partial class TopLevelCommandManager : ObservableObject,
|
||||
IRecipient<ReloadCommandsMessage>,
|
||||
IRecipient<PinCommandItemMessage>,
|
||||
IPageContext,
|
||||
IRecipient<UnpinCommandItemMessage>,
|
||||
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<TaskScheduler>()!;
|
||||
WeakReferenceMessenger.Default.Register<ReloadCommandsMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<PinCommandItemMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<UnpinCommandItemMessage>(this);
|
||||
_reloadCommandsGate = new(ReloadAllCommandsAsyncCore);
|
||||
}
|
||||
|
||||
@@ -103,9 +102,7 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
// May be called from a background thread
|
||||
private async Task<IEnumerable<TopLevelViewModel>> LoadTopLevelCommandsFromProvider(CommandProviderWrapper commandProvider)
|
||||
{
|
||||
WeakReference<IPageContext> 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,
|
||||
/// <returns>an awaitable task</returns>
|
||||
private async Task UpdateCommandsForProvider(CommandProviderWrapper sender, IItemsChangedEventArgs args)
|
||||
{
|
||||
WeakReference<IPageContext> weakSelf = new(this);
|
||||
await sender.LoadTopLevelCommands(_serviceProvider, weakSelf);
|
||||
await sender.LoadTopLevelCommands(_serviceProvider);
|
||||
|
||||
List<TopLevelViewModel> 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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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<IContextItem> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -801,10 +801,26 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
||||
<value>K</value>
|
||||
<comment>Keyboard key</comment>
|
||||
</data>
|
||||
<data name="ConfigureShortcut" xml:space="preserve">
|
||||
<data name="ConfigureShortcut" xml:space="preserve">
|
||||
<value>Configure shortcut</value>
|
||||
</data>
|
||||
<data name="ConfigureShortcutText.Text" xml:space="preserve">
|
||||
<value>Assign shortcut</value>
|
||||
</data>
|
||||
<data name="top_level_pin_command_name" xml:space="preserve">
|
||||
<value>Pin to home</value>
|
||||
<comment>Command name for pinning an item to the top level list of commands</comment>
|
||||
</data>
|
||||
<data name="top_level_unpin_command_name" xml:space="preserve">
|
||||
<value>Unpin from home</value>
|
||||
<comment>Command name for unpinning an item from the top level list of commands</comment>
|
||||
</data>
|
||||
<data name="dock_pin_command_name" xml:space="preserve">
|
||||
<value>Pin to dock</value>
|
||||
<comment>Command name for pinning an item to the dock</comment>
|
||||
</data>
|
||||
<data name="dock_unpin_command_name" xml:space="preserve">
|
||||
<value>Unpin from dock</value>
|
||||
<comment>Command name for unpinning an item from the dock</comment>
|
||||
</data>
|
||||
</root>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<AppItem> allApps = new();
|
||||
var items = new List<AppListItem>();
|
||||
|
||||
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<AppListItem>();
|
||||
var unpinned = new List<AppListItem>();
|
||||
|
||||
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<AppListItem>(pinnedApps);
|
||||
var newUnpinned = new List<AppListItem>(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];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Task<Details>>(BuildDetails);
|
||||
_iconLoadTask = new Lazy<Task<IconInfo?>>(async () => await FetchIcon(useThumbnails).ConfigureAwait(false));
|
||||
@@ -237,35 +237,6 @@ public sealed partial class AppListItem : ListItem, IPrecomputedListItem
|
||||
return icon;
|
||||
}
|
||||
|
||||
private IContextItem[] AddPinCommands(List<IContextItem> commands, bool isPinned)
|
||||
{
|
||||
var newCommands = new List<IContextItem>();
|
||||
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<IconInfo?> TryLoadThumbnail(string path, bool jumbo, bool logOnFailure)
|
||||
{
|
||||
return await Task.Run(async () =>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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<string>), TypeInfoPropertyName = "StringList")]
|
||||
[JsonSourceGenerationOptions(UseStringEnumConverter = true, WriteIndented = true, IncludeFields = true, PropertyNameCaseInsensitive = true, AllowTrailingCommas = true)]
|
||||
internal sealed partial class JsonSerializationContext : JsonSerializerContext
|
||||
|
||||
@@ -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<string> 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<PinnedApps>(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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<PinnedAppsManager> _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<PinStateChangedEventArgs>? 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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user