mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-02-23 19:49:43 +01:00
CmdPal: Fix SUI crash ; Allow extensions to be disabled (#38040)
Both `TopLevelCommandItemWrapper` and `TopLevelViewModel` were really the same thing. The latter was from an earlier prototype, and the former is a more correct, safer abstraction. We really should have only ever used the former, but alas, we only used it for the SUI, and it piggy-backed off the latter, and that meant the latter's bugs became the former's. tldr: I made the icon access safe in the SUI. And while I was doing this, because we now have a cleaner VM abstraction here in the host, we can actually cleanly disable extensions, because the `CommandProviderWrapper` knows which `ViewModel`s it made. Closes https://github.com/zadjii-msft/PowerToys/issues/426 Closes https://github.com/zadjii-msft/PowerToys/issues/478 Closes https://github.com/zadjii-msft/PowerToys/issues/577
This commit is contained in:
@@ -16,7 +16,8 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public partial class TopLevelCommandManager : ObservableObject,
|
||||
IRecipient<ReloadCommandsMessage>
|
||||
IRecipient<ReloadCommandsMessage>,
|
||||
IPageContext
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly TaskScheduler _taskScheduler;
|
||||
@@ -24,6 +25,8 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
private readonly List<CommandProviderWrapper> _builtInCommands = [];
|
||||
private readonly List<CommandProviderWrapper> _extensionCommandProviders = [];
|
||||
|
||||
TaskScheduler IPageContext.Scheduler => _taskScheduler;
|
||||
|
||||
public TopLevelCommandManager(IServiceProvider serviceProvider)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
@@ -31,7 +34,7 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
WeakReferenceMessenger.Default.Register<ReloadCommandsMessage>(this);
|
||||
}
|
||||
|
||||
public ObservableCollection<TopLevelCommandItemWrapper> TopLevelCommands { get; set; } = [];
|
||||
public ObservableCollection<TopLevelViewModel> TopLevelCommands { get; set; } = [];
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsLoading { get; private set; } = true;
|
||||
@@ -58,35 +61,43 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
// May be called from a background thread
|
||||
private async Task LoadTopLevelCommandsFromProvider(CommandProviderWrapper commandProvider)
|
||||
{
|
||||
await commandProvider.LoadTopLevelCommands();
|
||||
WeakReference<IPageContext> weakSelf = new(this);
|
||||
|
||||
await commandProvider.LoadTopLevelCommands(_serviceProvider, weakSelf);
|
||||
|
||||
var settings = _serviceProvider.GetService<SettingsModel>()!;
|
||||
var makeAndAdd = (ICommandItem? i, bool fallback) =>
|
||||
{
|
||||
TopLevelCommandItemWrapper wrapper = new(
|
||||
new(i), fallback, commandProvider.ExtensionHost, commandProvider.ProviderId, _serviceProvider);
|
||||
var commandItemViewModel = new CommandItemViewModel(new(i), weakSelf);
|
||||
var topLevelViewModel = new TopLevelViewModel(commandItemViewModel, fallback, commandProvider.ExtensionHost, commandProvider.ProviderId, settings, _serviceProvider);
|
||||
|
||||
lock (TopLevelCommands)
|
||||
{
|
||||
TopLevelCommands.Add(wrapper);
|
||||
TopLevelCommands.Add(topLevelViewModel);
|
||||
}
|
||||
};
|
||||
|
||||
await Task.Factory.StartNew(
|
||||
() =>
|
||||
{
|
||||
foreach (var i in commandProvider.TopLevelItems)
|
||||
lock (TopLevelCommands)
|
||||
{
|
||||
makeAndAdd(i, false);
|
||||
}
|
||||
foreach (var item in commandProvider.TopLevelItems)
|
||||
{
|
||||
TopLevelCommands.Add(item);
|
||||
}
|
||||
|
||||
foreach (var i in commandProvider.FallbackItems)
|
||||
{
|
||||
makeAndAdd(i, true);
|
||||
foreach (var item in commandProvider.FallbackItems)
|
||||
{
|
||||
TopLevelCommands.Add(item);
|
||||
}
|
||||
}
|
||||
},
|
||||
CancellationToken.None,
|
||||
TaskCreationOptions.None,
|
||||
_taskScheduler);
|
||||
|
||||
commandProvider.CommandsChanged -= CommandProvider_CommandsChanged;
|
||||
commandProvider.CommandsChanged += CommandProvider_CommandsChanged;
|
||||
}
|
||||
|
||||
@@ -108,8 +119,8 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
{
|
||||
// Work on a clone of the list, so that we can just do one atomic
|
||||
// update to the actual observable list at the end
|
||||
List<TopLevelCommandItemWrapper> clone = [.. TopLevelCommands];
|
||||
List<TopLevelCommandItemWrapper> newItems = [];
|
||||
List<TopLevelViewModel> clone = [.. TopLevelCommands];
|
||||
List<TopLevelViewModel> newItems = [];
|
||||
var startIndex = -1;
|
||||
var firstCommand = sender.TopLevelItems[0];
|
||||
var commandsToRemove = sender.TopLevelItems.Length + sender.FallbackItems.Length;
|
||||
@@ -122,7 +133,8 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
var wrapper = clone[i];
|
||||
try
|
||||
{
|
||||
var thisCommand = wrapper.Model.Unsafe;
|
||||
// TODO! this can be safer, we're not directly exposing ICommandItem's out of CPW anymore
|
||||
var thisCommand = wrapper.ItemViewModel.Model.Unsafe;
|
||||
if (thisCommand != null)
|
||||
{
|
||||
var isTheSame = thisCommand == firstCommand;
|
||||
@@ -138,16 +150,21 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
}
|
||||
}
|
||||
|
||||
WeakReference<IPageContext> weakSelf = new(this);
|
||||
|
||||
// Fetch the new items
|
||||
await sender.LoadTopLevelCommands();
|
||||
await sender.LoadTopLevelCommands(_serviceProvider, weakSelf);
|
||||
|
||||
var settings = _serviceProvider.GetService<SettingsModel>()!;
|
||||
|
||||
foreach (var i in sender.TopLevelItems)
|
||||
{
|
||||
newItems.Add(new(new(i), false, sender.ExtensionHost, sender.ProviderId, _serviceProvider));
|
||||
newItems.Add(i);
|
||||
}
|
||||
|
||||
foreach (var i in sender.FallbackItems)
|
||||
{
|
||||
newItems.Add(new(new(i), true, sender.ExtensionHost, sender.ProviderId, _serviceProvider));
|
||||
newItems.Add(i);
|
||||
}
|
||||
|
||||
// Slice out the old commands
|
||||
@@ -253,7 +270,7 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
async () =>
|
||||
{
|
||||
// Then find all the top-level commands that belonged to that extension
|
||||
List<TopLevelCommandItemWrapper> commandsToRemove = [];
|
||||
List<TopLevelViewModel> commandsToRemove = [];
|
||||
lock (TopLevelCommands)
|
||||
{
|
||||
foreach (var extension in extensions)
|
||||
@@ -292,7 +309,7 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
});
|
||||
}
|
||||
|
||||
public TopLevelCommandItemWrapper? LookupCommand(string id)
|
||||
public TopLevelViewModel? LookupCommand(string id)
|
||||
{
|
||||
lock (TopLevelCommands)
|
||||
{
|
||||
@@ -310,4 +327,10 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
|
||||
public void Receive(ReloadCommandsMessage message) =>
|
||||
ReloadAllCommandsAsync().ConfigureAwait(false);
|
||||
|
||||
void IPageContext.ShowException(Exception ex, string? extensionHint)
|
||||
{
|
||||
var errorMessage = $"A bug occurred in {$"the \"{extensionHint}\"" ?? "an unknown's"} extension's code:\n{ex.Message}\n{ex.Source}\n{ex.StackTrace}\n\n";
|
||||
CommandPaletteHost.Instance.Log(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user