Compare commits

...

19 Commits

Author SHA1 Message Date
Mike Griese
b326300537 cleanup for review 2026-02-18 13:30:01 -06:00
Mike Griese
88ed8cc9bc Merge branch 'dev/migrie/f/pincushion-context-factory' into dev/migrie/f/pincushion-actual-implementation 2026-02-18 13:03:02 -06:00
Mike Griese
f47e8b5dd8 Merge branch 'dev/migrie/f/pincushion' into dev/migrie/f/pincushion-context-factory 2026-02-18 13:01:09 -06:00
Mike Griese
c7edda29d8 Merge remote-tracking branch 'origin/main' into dev/migrie/f/pincushion 2026-02-18 13:00:51 -06:00
Mike Griese
d7c0b0b9d1 spel 2026-02-18 13:00:43 -06:00
Mike Griese
1a4e971092 I think this is everything working now 2026-02-18 10:13:12 -06:00
Mike Griese
fcf4b0727b Successfully load the additional commands on top level items 2026-02-18 09:40:50 -06:00
Mike Griese
d7d72fe2f4 Almost - top level items can pin/unpin _after_ a reload??
Like, we're not actually realizing that command needed us to build a
context menu for it until we re-retrieve it. dafuq
2026-02-18 06:36:56 -06:00
Mike Griese
fd31b286ac Make the commands refresh on pin 2026-02-18 06:00:25 -06:00
Mike Griese
1fb7261544 Unpinning _does work_ 2026-02-17 16:03:57 -06:00
Mike Griese
a6df9aca56 Actually wire up pinning commands
this took a lot more wiring than I thought
2026-02-17 15:50:59 -06:00
Mike Griese
dcbd17989d Merge branch 'dev/migrie/f/pincushion' into dev/migrie/f/pincushion-context-factory 2026-02-13 06:12:42 -06:00
Mike Griese
c9b2fa1d2d winrt isn't gonna like that 2026-02-13 06:06:25 -06:00
Mike Griese
b1b14fb0b0 nits 2026-02-13 06:04:09 -06:00
Mike Griese
35bbddb929 Merge remote-tracking branch 'origin/main' into dev/migrie/f/pincushion 2026-02-13 05:57:19 -06:00
Mike Griese
f726f186d4 CmdPal: Use a factory for creating context menu items 2026-02-12 13:00:41 -06:00
Mike Griese
27ec533bc0 you can now pin apps and system commands right from your json 2026-02-12 10:55:19 -06:00
Mike Griese
64872a5467 Just like I drew it up 2026-02-12 10:03:48 -06:00
Mike Griese
50e702ad9b Start working on pinning any commands 2026-02-12 09:46:30 -06:00
35 changed files with 745 additions and 133 deletions

View File

@@ -165,4 +165,6 @@ public interface IAppHostService
AppExtensionHost GetDefaultHost();
AppExtensionHost GetHostForCommand(object? context, AppExtensionHost? currentHost);
ICommandProviderContext GetProviderContextForCommand(object? command, ICommandProviderContext? currentContext);
}

View File

@@ -5,16 +5,15 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.CmdPal.Core.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Core.ViewModels;
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
public partial class CommandContextItemViewModel(ICommandContextItem contextItem, WeakReference<IPageContext> context) : CommandItemViewModel(new(contextItem), context), IContextItemViewModel
public partial class CommandContextItemViewModel : CommandItemViewModel, IContextItemViewModel
{
private readonly KeyChord nullKeyChord = new(0, 0, 0);
public new ExtensionObject<ICommandContextItem> Model { get; } = new(contextItem);
public new ExtensionObject<ICommandContextItem> Model { get; }
public bool IsCritical { get; private set; }
@@ -22,6 +21,13 @@ public partial class CommandContextItemViewModel(ICommandContextItem contextItem
public bool HasRequestedShortcut => RequestedShortcut is not null && (RequestedShortcut.Value != nullKeyChord);
public CommandContextItemViewModel(ICommandContextItem contextItem, WeakReference<IPageContext> context)
: base(new(contextItem), context, contextMenuFactory: null)
{
Model = new(contextItem);
IsContextMenuItem = true;
}
public override void InitializeProperties()
{
if (IsInitialized)

View File

@@ -19,6 +19,8 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
{
public ExtensionObject<ICommandItem> Model => _commandItemModel;
private readonly IContextMenuFactory? _contextMenuFactory;
private ExtensionObject<IExtendedAttributesProvider>? ExtendedAttributesProvider { get; set; }
private readonly ExtensionObject<ICommandItem> _commandItemModel = new(null);
@@ -35,6 +37,8 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
protected bool IsSelectedInitialized => IsInErrorState || Initialized.HasFlag(InitializedState.SelectionInitialized);
public bool IsContextMenuItem { get; protected init; }
public bool IsInErrorState => Initialized.HasFlag(InitializedState.Error);
// These are properties that are "observable" from the extension object
@@ -96,10 +100,14 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
_errorIcon.InitializeProperties();
}
public CommandItemViewModel(ExtensionObject<ICommandItem> item, WeakReference<IPageContext> errorContext)
public CommandItemViewModel(
ExtensionObject<ICommandItem> item,
WeakReference<IPageContext> errorContext,
IContextMenuFactory? contextMenuFactory)
: base(errorContext)
{
_commandItemModel = item;
_contextMenuFactory = contextMenuFactory;
Command = new(null, errorContext);
}
@@ -197,26 +205,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
return;
}
var more = model.MoreCommands;
if (more is not null)
{
MoreCommands = more
.Select<IContextItem, IContextItemViewModel>(item =>
{
return item is ICommandContextItem contextItem ? new CommandContextItemViewModel(contextItem, PageContext) : new SeparatorViewModel();
})
.ToList();
}
// Here, we're already theoretically in the async context, so we can
// use Initialize straight up
MoreCommands
.OfType<CommandContextItemViewModel>()
.ToList()
.ForEach(contextItem =>
{
contextItem.SlowInitializeProperties();
});
BuildAndInitMoreCommands();
if (!string.IsNullOrEmpty(model.Command?.Name))
{
@@ -370,36 +359,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
break;
case nameof(model.MoreCommands):
var more = model.MoreCommands;
if (more is not null)
{
var newContextMenu = more
.Select<IContextItem, IContextItemViewModel>(item =>
{
return item is ICommandContextItem contextItem ? new CommandContextItemViewModel(contextItem, PageContext) : new SeparatorViewModel();
})
.ToList();
lock (MoreCommands)
{
ListHelpers.InPlaceUpdateList(MoreCommands, newContextMenu);
}
newContextMenu
.OfType<CommandContextItemViewModel>()
.ToList()
.ForEach(contextItem =>
{
contextItem.InitializeProperties();
});
}
else
{
lock (MoreCommands)
{
MoreCommands.Clear();
}
}
BuildAndInitMoreCommands();
UpdateProperty(nameof(SecondaryCommand));
UpdateProperty(nameof(SecondaryCommandName));
UpdateProperty(nameof(HasMoreCommands));
@@ -477,6 +437,57 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
public FuzzyTarget GetSubtitleTarget(IPrecomputedFuzzyMatcher matcher)
=> _subtitleCache.GetOrUpdate(matcher, Subtitle);
/// <remarks>
/// * Does call SlowInitializeProperties on the created items.
/// * does NOT call UpdateProperty ; caller must do that.
/// </remarks>
private void BuildAndInitMoreCommands()
{
var model = _commandItemModel.Unsafe;
if (model is null)
{
return;
}
var more = model.MoreCommands;
var factory = _contextMenuFactory ?? DefaultContextMenuFactory.Instance;
var results = factory.UnsafeBuildAndInitMoreCommands(more, this);
List<IContextItemViewModel>? freedItems;
lock (MoreCommands)
{
ListHelpers.InPlaceUpdateList(MoreCommands, results, out freedItems);
}
freedItems.OfType<CommandContextItemViewModel>()
.ToList()
.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();

View File

@@ -0,0 +1,24 @@
// 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.Core.ViewModels;
public static class CommandProviderContext
{
public static ICommandProviderContext Empty { get; } = new EmptyCommandProviderContext();
private sealed class EmptyCommandProviderContext : ICommandProviderContext
{
public string ProviderId => "<EMPTY>";
public bool SupportsPinning => false;
}
}
public interface ICommandProviderContext
{
string ProviderId { get; }
bool SupportsPinning { get; }
}

View File

@@ -47,8 +47,8 @@ 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)
: base(model, scheduler, host)
public ContentPageViewModel(IContentPage model, TaskScheduler scheduler, AppExtensionHost host, ICommandProviderContext providerContext)
: base(model, scheduler, host, providerContext)
{
_model = new(model);
}

View File

@@ -0,0 +1,43 @@
// 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;
namespace Microsoft.CmdPal.Core.ViewModels;
public partial class DefaultContextMenuFactory : IContextMenuFactory
{
public static readonly DefaultContextMenuFactory Instance = new();
private DefaultContextMenuFactory()
{
}
public List<IContextItemViewModel> UnsafeBuildAndInitMoreCommands(
IContextItem[] items,
CommandItemViewModel commandItem)
{
List<IContextItemViewModel> results = [];
if (items is null)
{
return results;
}
foreach (var item in items)
{
if (item is ICommandContextItem contextItem)
{
var contextItemViewModel = new CommandContextItemViewModel(contextItem, commandItem.PageContext);
contextItemViewModel.SlowInitializeProperties();
results.Add(contextItemViewModel);
}
else
{
results.Add(new SeparatorViewModel());
}
}
return results;
}
}

View File

@@ -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*/
}

View File

@@ -0,0 +1,12 @@
// 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;
namespace Microsoft.CmdPal.Core.ViewModels;
public interface IContextMenuFactory
{
List<IContextItemViewModel> UnsafeBuildAndInitMoreCommands(IContextItem[] items, CommandItemViewModel commandItem);
}

View File

@@ -63,8 +63,8 @@ public partial class ListItemViewModel : CommandItemViewModel
}
}
public ListItemViewModel(IListItem model, WeakReference<IPageContext> context)
: base(new(model), context)
public ListItemViewModel(IListItem model, WeakReference<IPageContext> context, IContextMenuFactory contextMenuFactory)
: base(new(model), context, contextMenuFactory)
{
Model = new ExtensionObject<IListItem>(model);
}

View File

@@ -3,7 +3,6 @@
// See the LICENSE file in the project root for more information.
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.CmdPal.Core.Common.Helpers;
@@ -33,6 +32,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
private readonly ExtensionObject<IListPage> _model;
private readonly Lock _listLock = new();
private readonly IContextMenuFactory _contextMenuFactory;
private InterlockedBoolean _isLoading;
private bool _isFetching;
@@ -89,11 +89,12 @@ public partial class ListViewModel : PageViewModel, IDisposable
}
}
public ListViewModel(IListPage model, TaskScheduler scheduler, AppExtensionHost host)
: base(model, scheduler, host)
public ListViewModel(IListPage model, TaskScheduler scheduler, AppExtensionHost host, ICommandProviderContext providerContext, IContextMenuFactory contextMenuFactory)
: base(model, scheduler, host, providerContext)
{
_model = new(model);
EmptyContent = new(new(null), PageContext);
_contextMenuFactory = contextMenuFactory;
EmptyContent = new(new(null), PageContext, contextMenuFactory: null);
}
private void FiltersPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
@@ -233,7 +234,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
return;
}
ListItemViewModel viewModel = new(item, new(this));
ListItemViewModel viewModel = new(item, new(this), _contextMenuFactory);
// If an item fails to load, silently ignore it.
if (viewModel.SafeFastInit())
@@ -603,7 +604,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;
@@ -699,7 +700,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):
@@ -769,7 +770,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();

View File

@@ -9,7 +9,7 @@ namespace Microsoft.CmdPal.Core.ViewModels;
public partial class LoadingPageViewModel : PageViewModel
{
public LoadingPageViewModel(IPage? model, TaskScheduler scheduler, AppExtensionHost host)
: base(model, scheduler, host)
: base(model, scheduler, host, CommandProviderContext.Empty)
{
ModelIsLoading = true;
IsInitialized = false;

View File

@@ -5,4 +5,4 @@
namespace Microsoft.CmdPal.Core.ViewModels;
internal sealed partial class NullPageViewModel(TaskScheduler scheduler, AppExtensionHost extensionHost)
: PageViewModel(null, scheduler, extensionHost);
: PageViewModel(null, scheduler, extensionHost, CommandProviderContext.Empty);

View File

@@ -76,13 +76,16 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext
public IconInfoViewModel Icon { get; protected set; }
public PageViewModel(IPage? model, TaskScheduler scheduler, AppExtensionHost extensionHost)
public ICommandProviderContext ProviderContext { get; protected set; }
public PageViewModel(IPage? model, TaskScheduler scheduler, AppExtensionHost extensionHost, ICommandProviderContext providerContext)
: base(scheduler)
{
InitializeSelfAsPageContext();
_pageModel = new(model);
Scheduler = scheduler;
ExtensionHost = extensionHost;
ProviderContext = providerContext;
Icon = new(null);
ExtensionHost.StatusMessages.CollectionChanged += StatusMessages_CollectionChanged;
@@ -264,6 +267,8 @@ public interface IPageContext
void ShowException(Exception ex, string? extensionHint = null);
TaskScheduler Scheduler { get; }
ICommandProviderContext ProviderContext { get; }
}
public interface IPageViewModelFactoryService
@@ -275,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);
PageViewModel? TryCreatePageViewModel(IPage page, bool nested, AppExtensionHost host, ICommandProviderContext providerContext);
}

View File

@@ -258,6 +258,7 @@ public partial class ShellViewModel : ObservableObject,
}
var host = _appHostService.GetHostForCommand(message.Context, CurrentPage.ExtensionHost);
var providerContext = _appHostService.GetProviderContextForCommand(message.Context, CurrentPage.ProviderContext);
_rootPageService.OnPerformCommand(message.Context, !CurrentPage.IsNested, host);
@@ -273,15 +274,15 @@ public partial class ShellViewModel : ObservableObject,
// Telemetry: Track extension page navigation for session metrics
if (host is not null)
{
string extensionId = host.GetExtensionDisplayName() ?? "builtin";
string commandId = command?.Id ?? "unknown";
string commandName = command?.Name ?? "unknown";
var extensionId = host.GetExtensionDisplayName() ?? "builtin";
var commandId = command?.Id ?? "unknown";
var commandName = command?.Name ?? "unknown";
WeakReferenceMessenger.Default.Send<TelemetryExtensionInvokedMessage>(
new(extensionId, commandId, commandName, true, 0));
}
// Construct our ViewModel of the appropriate type and pass it the UI Thread context.
var pageViewModel = _pageViewModelFactory.TryCreatePageViewModel(page, _isNested, host!);
var pageViewModel = _pageViewModelFactory.TryCreatePageViewModel(page, _isNested, host!, providerContext);
if (pageViewModel is null)
{
CoreLogger.LogError($"Failed to create ViewModel for page {page.GetType().Name}");
@@ -352,10 +353,10 @@ public partial class ShellViewModel : ObservableObject,
// Telemetry: Track command execution time and success
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
var command = message.Command.Unsafe;
string extensionId = host?.GetExtensionDisplayName() ?? "builtin";
string commandId = command?.Id ?? "unknown";
string commandName = command?.Name ?? "unknown";
bool success = false;
var extensionId = host?.GetExtensionDisplayName() ?? "builtin";
var commandId = command?.Id ?? "unknown";
var commandName = command?.Name ?? "unknown";
var success = false;
try
{

View File

@@ -9,8 +9,8 @@ namespace Microsoft.CmdPal.UI.ViewModels;
public partial class CommandPaletteContentPageViewModel : ContentPageViewModel
{
public CommandPaletteContentPageViewModel(IContentPage model, TaskScheduler scheduler, AppExtensionHost host)
: base(model, scheduler, host)
public CommandPaletteContentPageViewModel(IContentPage model, TaskScheduler scheduler, AppExtensionHost host, ICommandProviderContext providerContext)
: base(model, scheduler, host, providerContext)
{
}

View File

@@ -11,18 +11,20 @@ public class CommandPalettePageViewModelFactory
: IPageViewModelFactoryService
{
private readonly TaskScheduler _scheduler;
private readonly IContextMenuFactory _contextMenuFactory;
public CommandPalettePageViewModelFactory(TaskScheduler scheduler)
public CommandPalettePageViewModelFactory(TaskScheduler scheduler, IContextMenuFactory contextMenuFactory)
{
_scheduler = scheduler;
_contextMenuFactory = contextMenuFactory;
}
public PageViewModel? TryCreatePageViewModel(IPage page, bool nested, AppExtensionHost host)
public PageViewModel? TryCreatePageViewModel(IPage page, bool nested, AppExtensionHost host, ICommandProviderContext providerContext)
{
return page switch
{
IListPage listPage => new ListViewModel(listPage, _scheduler, host) { IsNested = nested },
IContentPage contentPage => new CommandPaletteContentPageViewModel(contentPage, _scheduler, host),
IListPage listPage => new ListViewModel(listPage, _scheduler, host, providerContext, _contextMenuFactory) { IsNested = nested },
IContentPage contentPage => new CommandPaletteContentPageViewModel(contentPage, _scheduler, host, providerContext),
_ => null,
};
}

View File

@@ -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,13 +8,14 @@ using Microsoft.CmdPal.Core.ViewModels;
using Microsoft.CmdPal.Core.ViewModels.Models;
using Microsoft.CmdPal.UI.ViewModels.Services;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Microsoft.Extensions.DependencyInjection;
using Windows.Foundation;
namespace Microsoft.CmdPal.UI.ViewModels;
public sealed class CommandProviderWrapper
public sealed class CommandProviderWrapper : ICommandProviderContext
{
public bool IsExtension => Extension is not null;
@@ -48,6 +49,8 @@ public sealed class CommandProviderWrapper
public string ProviderId => string.IsNullOrEmpty(Extension?.ExtensionUniqueId) ? Id : Extension.ExtensionUniqueId;
public bool SupportsPinning { get; private set; }
public CommandProviderWrapper(ICommandProvider provider, TaskScheduler mainThread)
{
// This ctor is only used for in-proc builtin commands. So the Unsafe!
@@ -158,6 +161,15 @@ public sealed class CommandProviderWrapper
UnsafePreCacheApiAdditions(two);
}
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;
Icon = new(model.Icon);
@@ -175,7 +187,7 @@ public sealed class CommandProviderWrapper
Settings = new(model.Settings, this, _taskScheduler);
// We do need to explicitly initialize commands though
InitializeCommands(commands, fallbacks, serviceProvider, pageContext);
InitializeCommands(commands, fallbacks, pinnedCommands, serviceProvider, pageContext);
Logger.LogDebug($"Loaded commands from {DisplayName} ({ProviderId})");
}
@@ -206,27 +218,40 @@ public sealed class CommandProviderWrapper
}
}
private void InitializeCommands(ICommandItem[] commands, IFallbackCommandItem[] fallbacks, IServiceProvider serviceProvider, WeakReference<IPageContext> pageContext)
private void InitializeCommands(
ICommandItem[] commands,
IFallbackCommandItem[] fallbacks,
ICommandItem[] pinnedCommands,
IServiceProvider serviceProvider,
WeakReference<IPageContext> pageContext)
{
var settings = serviceProvider.GetService<SettingsModel>()!;
var contextMenuFactory = serviceProvider.GetService<IContextMenuFactory>()!;
var providerSettings = GetProviderSettings(settings);
var ourContext = GetProviderContext();
var makeAndAdd = (ICommandItem? i, bool fallback) =>
{
CommandItemViewModel commandItemViewModel = new(new(i), pageContext);
TopLevelViewModel topLevelViewModel = new(commandItemViewModel, fallback, ExtensionHost, ProviderId, settings, providerSettings, serviceProvider, i);
CommandItemViewModel commandItemViewModel = new(new(i), pageContext, contextMenuFactory: contextMenuFactory);
TopLevelViewModel topLevelViewModel = new(commandItemViewModel, fallback, ExtensionHost, ourContext, settings, providerSettings, serviceProvider, i);
topLevelViewModel.InitializeProperties();
return topLevelViewModel;
};
var topLevelList = new List<TopLevelViewModel>();
if (commands is not null)
{
TopLevelItems = commands
.Select(c => makeAndAdd(c, false))
.ToArray();
topLevelList.AddRange(commands.Select(c => makeAndAdd(c, false)));
}
if (pinnedCommands is not null)
{
topLevelList.AddRange(pinnedCommands.Select(c => makeAndAdd(c, false)));
}
TopLevelItems = topLevelList.ToArray();
if (fallbacks is not null)
{
FallbackItems = fallbacks
@@ -235,6 +260,29 @@ public sealed class CommandProviderWrapper
}
}
private ICommandItem[] LoadPinnedCommands(ICommandProvider4 model, ProviderSettings providerSettings)
{
var pinnedItems = new List<ICommandItem>();
foreach (var pinnedId in providerSettings.PinnedCommandIds)
{
try
{
var commandItem = model.GetCommandItem(pinnedId);
if (commandItem is not null)
{
pinnedItems.Add(commandItem);
}
}
catch (Exception e)
{
Logger.LogError($"Failed to load pinned command {pinnedId}: {e.Message}");
}
}
return pinnedItems.ToArray();
}
private void UnsafePreCacheApiAdditions(ICommandProvider2 provider)
{
var apiExtensions = provider.GetApiExtensionStubs();
@@ -248,6 +296,37 @@ public sealed class CommandProviderWrapper
}
}
public void PinCommand(string commandId, IServiceProvider serviceProvider)
{
var settings = serviceProvider.GetService<SettingsModel>()!;
var providerSettings = GetProviderSettings(settings);
if (!providerSettings.PinnedCommandIds.Contains(commandId))
{
providerSettings.PinnedCommandIds.Add(commandId);
SettingsModel.SaveSettings(settings);
// Raise CommandsChanged so the TopLevelCommandManager reloads our commands
this.CommandsChanged?.Invoke(this, new ItemsChangedEventArgs(-1));
}
}
public void UnpinCommand(string commandId, IServiceProvider serviceProvider)
{
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();

View File

@@ -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.
@@ -31,7 +31,7 @@ public partial class CommandSettingsViewModel(ICommandSettings? _unsafeSettings,
if (model.SettingsPage is not null)
{
SettingsPage = new CommandPaletteContentPageViewModel(model.SettingsPage, mainThread, provider.ExtensionHost);
SettingsPage = new CommandPaletteContentPageViewModel(model.SettingsPage, mainThread, provider.ExtensionHost, provider.GetProviderContext());
SettingsPage.InitializeProperties();
}
}

View 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 sealed 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
}

View File

@@ -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 PinCommandItemMessage(string ProviderId, string CommandId)
{
}

View File

@@ -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)
{
}

View File

@@ -18,6 +18,8 @@ public class ProviderSettings
public Dictionary<string, FallbackSettings> FallbackCommands { get; set; } = new();
public List<string> PinnedCommandIds { get; set; } = [];
[JsonIgnore]
public string ProviderDisplayName { get; set; } = string.Empty;

View File

@@ -22,7 +22,8 @@ namespace Microsoft.CmdPal.UI.ViewModels;
public partial class TopLevelCommandManager : ObservableObject,
IRecipient<ReloadCommandsMessage>,
IPageContext,
IRecipient<PinCommandItemMessage>,
IRecipient<UnpinCommandItemMessage>,
IDisposable
{
private readonly IServiceProvider _serviceProvider;
@@ -34,14 +35,14 @@ 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;
_commandProviderCache = commandProviderCache;
_taskScheduler = _serviceProvider.GetService<TaskScheduler>()!;
WeakReferenceMessenger.Default.Register<ReloadCommandsMessage>(this);
WeakReferenceMessenger.Default.Register<PinCommandItemMessage>(this);
WeakReferenceMessenger.Default.Register<UnpinCommandItemMessage>(this);
_reloadCommandsGate = new(ReloadAllCommandsAsyncCore);
}
@@ -102,9 +103,10 @@ 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);
TopLevelItemPageContext pageContext = new(commandProvider, _taskScheduler);
WeakReference<IPageContext> weak = new(pageContext);
await commandProvider.LoadTopLevelCommands(_serviceProvider, weakSelf);
await commandProvider.LoadTopLevelCommands(_serviceProvider, weak);
var commands = await Task.Factory.StartNew(
() =>
@@ -151,8 +153,9 @@ 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);
TopLevelItemPageContext pageContext = new(sender, _taskScheduler);
WeakReference<IPageContext> weak = new(pageContext);
await sender.LoadTopLevelCommands(_serviceProvider, weak);
List<TopLevelViewModel> newItems = [.. sender.TopLevelItems];
foreach (var i in sender.FallbackItems)
@@ -414,10 +417,25 @@ public partial class TopLevelCommandManager : ObservableObject,
public void Receive(ReloadCommandsMessage message) =>
ReloadAllCommandsAsync().ConfigureAwait(false);
void IPageContext.ShowException(Exception ex, string? extensionHint)
public void Receive(PinCommandItemMessage message)
{
var message = DiagnosticsHelper.BuildExceptionMessage(ex, extensionHint ?? "TopLevelCommandManager");
CommandPaletteHost.Instance.Log(message);
var wrapper = LookupProvider(message.ProviderId);
wrapper?.PinCommand(message.CommandId, _serviceProvider);
}
public void Receive(UnpinCommandItemMessage message)
{
var wrapper = LookupProvider(message.ProviderId);
wrapper?.UnpinCommand(message.CommandId, _serviceProvider);
}
public CommandProviderWrapper? LookupProvider(string providerId)
{
lock (_commandProvidersLock)
{
return _builtInCommands.FirstOrDefault(w => w.ProviderId == providerId)
?? _extensionCommandProviders.FirstOrDefault(w => w.ProviderId == providerId);
}
}
internal bool IsProviderActive(string id)

View File

@@ -0,0 +1,37 @@
// 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.Core.Common.Helpers;
using Microsoft.CmdPal.Core.ViewModels;
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);
}
}

View File

@@ -27,7 +27,7 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
private readonly IServiceProvider _serviceProvider;
private readonly CommandItemViewModel _commandItemViewModel;
private readonly string _commandProviderId;
public ICommandProviderContext ProviderContext { get; private set; }
private string IdFromModel => IsFallback && !string.IsNullOrWhiteSpace(_fallbackId) ? _fallbackId : _commandItemViewModel.Command.Id;
@@ -57,7 +57,7 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
public CommandItemViewModel ItemViewModel => _commandItemViewModel;
public string CommandProviderId => _commandProviderId;
public string CommandProviderId => ProviderContext.ProviderId;
////// ICommandItem
public string Title => _commandItemViewModel.Title;
@@ -190,7 +190,7 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
CommandItemViewModel item,
bool isFallback,
CommandPaletteHost extensionHost,
string commandProviderId,
ICommandProviderContext commandProviderContext,
SettingsModel settings,
ProviderSettings providerSettings,
IServiceProvider serviceProvider,
@@ -199,7 +199,7 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
_serviceProvider = serviceProvider;
_settings = settings;
_providerSettings = providerSettings;
_commandProviderId = commandProviderId;
ProviderContext = commandProviderContext;
_commandItemViewModel = item;
IsFallback = isFallback;
@@ -358,8 +358,8 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
{
// Use WyHash64 to generate stable ID hashes.
// manually seeding with 0, so that the hash is stable across launches
var result = WyHash64.ComputeHash64(_commandProviderId + DisplayTitle + Title + Subtitle, seed: 0);
_generatedId = $"{_commandProviderId}{result}";
var result = WyHash64.ComputeHash64(CommandProviderId + DisplayTitle + Title + Subtitle, seed: 0);
_generatedId = $"{CommandProviderId}{result}";
}
private void DoOnUiThread(Action action)

View File

@@ -221,6 +221,7 @@ public partial class App : Application, IDisposable
// ViewModels
services.AddSingleton<ShellViewModel>();
services.AddSingleton<IContextMenuFactory, CommandPaletteContextMenuFactory>();
services.AddSingleton<IPageViewModelFactoryService, CommandPalettePageViewModelFactory>();
}

View File

@@ -0,0 +1,194 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.CmdPal.Core.Common;
using Microsoft.CmdPal.Core.ViewModels;
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;
internal sealed partial class CommandPaletteContextMenuFactory : IContextMenuFactory
{
private readonly SettingsModel _settingsModel;
private readonly TopLevelCommandManager _topLevelCommandManager;
public CommandPaletteContextMenuFactory(SettingsModel settingsModel, TopLevelCommandManager topLevelCommandManager)
{
_settingsModel = settingsModel;
_topLevelCommandManager = topLevelCommandManager;
}
public List<IContextItemViewModel> UnsafeBuildAndInitMoreCommands(
IContextItem[] items,
CommandItemViewModel commandItem)
{
var results = DefaultContextMenuFactory.Instance.UnsafeBuildAndInitMoreCommands(items, commandItem);
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, IDisposable
{
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();
}
public void Dispose()
{
GC.SuppressFinalize(this);
_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()
{
CoreLogger.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);
}
}
}

View File

@@ -26,4 +26,15 @@ internal sealed class PowerToysAppHostService : IAppHostService
return topLevelHost ?? currentHost ?? CommandPaletteHost.Instance;
}
public ICommandProviderContext GetProviderContextForCommand(object? command, ICommandProviderContext? currentContext)
{
ICommandProviderContext? topLevelId = null;
if (command is TopLevelViewModel topLevelViewModel)
{
topLevelId = topLevelViewModel.ProviderContext;
}
return topLevelId ?? currentContext ?? throw new InvalidOperationException("No command provider context could be found for the given command, and no current context was provided.");
}
}

View File

@@ -795,10 +795,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 command</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 command</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>

View File

@@ -77,6 +77,7 @@ functionality.
- [Rendering of ICommandItems in Lists and Menus](#rendering-of-icommanditems-in-lists-and-menus)
- [Addenda I: API additions (ICommandProvider2)](#addenda-i-api-additions-icommandprovider2)
- [Addenda IV: Dock bands](#addenda-iv-dock-bands)
- [Pinning nested commands to the dock (and top level)](#pinning-nested-commands-to-the-dock-and-top-level)
- [Class diagram](#class-diagram)
- [Future considerations](#future-considerations)
- [Arbitrary parameters and arguments](#arbitrary-parameters-and-arguments)
@@ -2128,6 +2129,36 @@ Users may choose to have:
- Dock bands will still display the `Title` & `Subtitle` of each item in the
band as the tooltip on those items, even when the "labels" are hidden.
### Pinning nested commands to the dock (and top level)
We'll use another command provider method to allow the host to ask extensions
for items based on their ID.
```csharp
interface ICommandProvider4 requires ICommandProvider3
{
ICommandItem GetCommandItem(String id);
};
```
This will allow users to pin not just top-level commands, but also nested
commands which have an ID. The host can store that ID away, and then later ask
the extension for the `ICommandItem` with that ID, to get the full details of
the command to pin.
This is needed separate from the `GetCommand` method on `ICommandProvider`,
because that method is was designed for two main purposes:
* Short-circuiting the loading of top-level commands for frozen extensions. In
that case, DevPal would only need to look up the actual `ICommand` to perform
it. It wouldn't need the full `ICommandItem` with all the details.
* Allowing invokable commands to navigate using the GoToPageArgs. In that case,
DevPal would only need the `ICommand` to perform the navigation.
In neither of those scenarios was the full "display" of the item needed. In
pinning scenarios, however, we need everything that the user would see in the UI
for that item, which is all in the `ICommandItem`.
## Class diagram
This is a diagram attempting to show the relationships between the various types we've defined for the SDK. Some elements are omitted for clarity. (Notably, `IconData` and `IPropChanged`, which are used in many places.)

View File

@@ -184,6 +184,20 @@ public partial class AllAppsCommandProvider : CommandProvider
return null;
}
public override ICommandItem? GetCommandItem(string id)
{
var items = _page.GetItems();
foreach (var item in items)
{
if (item.Command.Id == id)
{
return item;
}
}
return null;
}
private void OnPinStateChanged(object? sender, System.EventArgs e)
{
RaiseItemsChanged(0);

View File

@@ -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.
@@ -42,37 +42,61 @@ internal static class Commands
var results = new List<IListItem>();
results.AddRange(new[]
{
new ListItem(new ExecuteCommandConfirmation(Resources.Microsoft_plugin_command_name_shutdown, confirmCommands, Resources.Microsoft_plugin_sys_shutdown_computer_confirmation, () => OpenInShellHelper.OpenInShell("shutdown", "/s /hybrid /t 0", runWithHiddenWindow: true)))
new ListItem(
new ExecuteCommandConfirmation(Resources.Microsoft_plugin_command_name_shutdown, confirmCommands, Resources.Microsoft_plugin_sys_shutdown_computer_confirmation, () => OpenInShellHelper.OpenInShell("shutdown", "/s /hybrid /t 0", runWithHiddenWindow: true))
{
Id = "com.microsoft.cmdpal.builtin.system.shutdown",
})
{
Title = Resources.Microsoft_plugin_sys_shutdown_computer,
Subtitle = Resources.Microsoft_plugin_sys_shutdown_computer_description,
Icon = Icons.ShutdownIcon,
},
new ListItem(new ExecuteCommandConfirmation(Resources.Microsoft_plugin_command_name_restart, confirmCommands, Resources.Microsoft_plugin_sys_restart_computer_confirmation, () => OpenInShellHelper.OpenInShell("shutdown", "/g /t 0", runWithHiddenWindow: true)))
new ListItem(
new ExecuteCommandConfirmation(Resources.Microsoft_plugin_command_name_restart, confirmCommands, Resources.Microsoft_plugin_sys_restart_computer_confirmation, () => OpenInShellHelper.OpenInShell("shutdown", "/g /t 0", runWithHiddenWindow: true))
{
Id = "com.microsoft.cmdpal.builtin.system.restart",
})
{
Title = Resources.Microsoft_plugin_sys_restart_computer,
Subtitle = Resources.Microsoft_plugin_sys_restart_computer_description,
Icon = Icons.RestartIcon,
},
new ListItem(new ExecuteCommandConfirmation(Resources.Microsoft_plugin_command_name_signout, confirmCommands, Resources.Microsoft_plugin_sys_sign_out_confirmation, () => NativeMethods.ExitWindowsEx(EWXLOGOFF, 0)))
new ListItem(
new ExecuteCommandConfirmation(Resources.Microsoft_plugin_command_name_signout, confirmCommands, Resources.Microsoft_plugin_sys_sign_out_confirmation, () => NativeMethods.ExitWindowsEx(EWXLOGOFF, 0))
{
Id = "com.microsoft.cmdpal.builtin.system.signout",
})
{
Title = Resources.Microsoft_plugin_sys_sign_out,
Subtitle = Resources.Microsoft_plugin_sys_sign_out_description,
Icon = Icons.LogoffIcon,
},
new ListItem(new ExecuteCommandConfirmation(Resources.Microsoft_plugin_command_name_lock, confirmCommands, Resources.Microsoft_plugin_sys_lock_confirmation, () => NativeMethods.LockWorkStation()))
new ListItem(
new ExecuteCommandConfirmation(Resources.Microsoft_plugin_command_name_lock, confirmCommands, Resources.Microsoft_plugin_sys_lock_confirmation, () => NativeMethods.LockWorkStation())
{
Id = "com.microsoft.cmdpal.builtin.system.lock",
})
{
Title = Resources.Microsoft_plugin_sys_lock,
Subtitle = Resources.Microsoft_plugin_sys_lock_description,
Icon = Icons.LockIcon,
},
new ListItem(new ExecuteCommandConfirmation(Resources.Microsoft_plugin_command_name_sleep, confirmCommands, Resources.Microsoft_plugin_sys_sleep_confirmation, () => NativeMethods.SetSuspendState(false, true, true)))
new ListItem(
new ExecuteCommandConfirmation(Resources.Microsoft_plugin_command_name_sleep, confirmCommands, Resources.Microsoft_plugin_sys_sleep_confirmation, () => NativeMethods.SetSuspendState(false, true, true))
{
Id = "com.microsoft.cmdpal.builtin.system.sleep",
})
{
Title = Resources.Microsoft_plugin_sys_sleep,
Subtitle = Resources.Microsoft_plugin_sys_sleep_description,
Icon = Icons.SleepIcon,
},
new ListItem(new ExecuteCommandConfirmation(Resources.Microsoft_plugin_command_name_hibernate, confirmCommands, Resources.Microsoft_plugin_sys_hibernate_confirmation, () => NativeMethods.SetSuspendState(true, true, true)))
new ListItem(
new ExecuteCommandConfirmation(Resources.Microsoft_plugin_command_name_hibernate, confirmCommands, Resources.Microsoft_plugin_sys_hibernate_confirmation, () => NativeMethods.SetSuspendState(true, true, true))
{
Id = "com.microsoft.cmdpal.builtin.system.hibernate",
})
{
Title = Resources.Microsoft_plugin_sys_hibernate,
Subtitle = Resources.Microsoft_plugin_sys_hibernate_description,
@@ -85,13 +109,19 @@ internal static class Commands
{
results.AddRange(new[]
{
new ListItem(new OpenInShellCommand(Resources.Microsoft_plugin_command_name_open, "explorer.exe", "shell:RecycleBinFolder"))
new ListItem(new OpenInShellCommand(Resources.Microsoft_plugin_command_name_open, "explorer.exe", "shell:RecycleBinFolder")
{
Id = "com.microsoft.cmdpal.builtin.system.recycle_bin",
})
{
Title = Resources.Microsoft_plugin_sys_RecycleBinOpen,
Subtitle = Resources.Microsoft_plugin_sys_RecycleBin_description,
Icon = Icons.RecycleBinIcon,
},
new ListItem(new EmptyRecycleBinConfirmation(emptyRBSuccessMessage))
new ListItem(new EmptyRecycleBinConfirmation(emptyRBSuccessMessage)
{
Id = "com.microsoft.cmdpal.builtin.system.empty_recycle_bin",
})
{
Title = Resources.Microsoft_plugin_sys_RecycleBinEmptyResult,
Subtitle = Resources.Microsoft_plugin_sys_RecycleBinEmpty_description,
@@ -102,7 +132,10 @@ internal static class Commands
else
{
results.Add(
new ListItem(new OpenInShellCommand(Resources.Microsoft_plugin_command_name_open, "explorer.exe", "shell:RecycleBinFolder"))
new ListItem(new OpenInShellCommand(Resources.Microsoft_plugin_command_name_open, "explorer.exe", "shell:RecycleBinFolder")
{
Id = "com.microsoft.cmdpal.builtin.system.recycle_bin",
})
{
Title = Resources.Microsoft_plugin_sys_RecycleBin,
Subtitle = Resources.Microsoft_plugin_sys_RecycleBin_description,
@@ -110,7 +143,15 @@ internal static class Commands
});
}
results.Add(new ListItem(new ExecuteCommandConfirmation(Resources.Microsoft_plugin_sys_RestartShell_name!, confirmCommands, Resources.Microsoft_plugin_sys_RestartShell_confirmation!, static () => OpenInShellHelper.OpenInShell("cmd", "/C tskill explorer && start explorer", runWithHiddenWindow: true)))
results.Add(new ListItem(
new ExecuteCommandConfirmation(
Resources.Microsoft_plugin_sys_RestartShell_name!,
confirmCommands,
Resources.Microsoft_plugin_sys_RestartShell_confirmation!,
static () => OpenInShellHelper.OpenInShell("cmd", "/C tskill explorer && start explorer", runWithHiddenWindow: true))
{
Id = "com.microsoft.cmdpal.builtin.system.restart_shell",
})
{
Title = Resources.Microsoft_plugin_sys_RestartShell!,
Subtitle = Resources.Microsoft_plugin_sys_RestartShell_description!,
@@ -141,19 +182,19 @@ internal static class Commands
var results = new List<IListItem>();
// We update the cache only if the last query is older than 'updateCacheIntervalSeconds' seconds
DateTime timeOfLastNetworkQueryBefore = timeOfLastNetworkQuery;
var timeOfLastNetworkQueryBefore = timeOfLastNetworkQuery;
timeOfLastNetworkQuery = DateTime.Now; // Set time of last query to this query
if ((timeOfLastNetworkQuery - timeOfLastNetworkQueryBefore).TotalSeconds >= UpdateCacheIntervalSeconds)
{
networkPropertiesCache = NetworkConnectionProperties.GetList();
}
CompositeFormat sysIpv4DescriptionCompositeFormate = CompositeFormat.Parse(Resources.Microsoft_plugin_sys_ip4_description);
CompositeFormat sysIpv6DescriptionCompositeFormate = CompositeFormat.Parse(Resources.Microsoft_plugin_sys_ip6_description);
CompositeFormat sysMacDescriptionCompositeFormate = CompositeFormat.Parse(Resources.Microsoft_plugin_sys_mac_description);
var sysIpv4DescriptionCompositeFormate = CompositeFormat.Parse(Resources.Microsoft_plugin_sys_ip4_description);
var sysIpv6DescriptionCompositeFormate = CompositeFormat.Parse(Resources.Microsoft_plugin_sys_ip6_description);
var sysMacDescriptionCompositeFormate = CompositeFormat.Parse(Resources.Microsoft_plugin_sys_mac_description);
var hideDisconnectedNetworkInfo = manager.HideDisconnectedNetworkInfo();
foreach (NetworkConnectionProperties intInfo in networkPropertiesCache)
foreach (var intInfo in networkPropertiesCache)
{
if (hideDisconnectedNetworkInfo)
{

View File

@@ -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.
@@ -39,4 +39,18 @@ public sealed partial class SystemCommandExtensionProvider : CommandProvider
}
public override IFallbackCommandItem[] FallbackCommands() => [_fallbackSystemItem];
public override ICommandItem? GetCommandItem(string id)
{
var everything = Page.GetItems();
foreach (var item in everything)
{
if (item.Command.Id == id)
{
return item;
}
}
return null;
}
}

View File

@@ -9,7 +9,8 @@ namespace Microsoft.CommandPalette.Extensions.Toolkit;
public abstract partial class CommandProvider :
ICommandProvider,
ICommandProvider2,
ICommandProvider3
ICommandProvider3,
ICommandProvider4
{
public virtual string Id { get; protected set; } = string.Empty;
@@ -25,6 +26,8 @@ public abstract partial class CommandProvider :
public virtual ICommand? GetCommand(string id) => null;
public virtual ICommandItem? GetCommandItem(string id) => null;
public virtual ICommandSettings? Settings { get; protected set; }
public virtual bool Frozen { get; protected set; } = true;

View File

@@ -411,5 +411,11 @@ namespace Microsoft.CommandPalette.Extensions
{
ICommandItem[] GetDockBands();
};
[contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)]
interface ICommandProvider4 requires ICommandProvider3
{
ICommandItem GetCommandItem(String id);
};
}