mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-02-23 19:49:43 +01:00
Implement Allowing CommandProviders to ItemsChanged themselves (#289)
This is the implementation of what was spec'd in #282. When we get an `ItemsChanged`, we'll look through the top level commands, slice out all the old ones, then fetch the new ones and stitch them in the same place in the list. I've only implemented it for the Bookmarks provider so far. Closes #277
This commit is contained in:
@@ -27,8 +27,9 @@ public partial class BookmarksCommandProvider : CommandProvider
|
||||
|
||||
private void AddNewCommand_AddedAction(object sender, object? args)
|
||||
{
|
||||
_addNewCommand.AddedAction += AddNewCommand_AddedAction;
|
||||
_commands.Clear();
|
||||
LoadCommands();
|
||||
RaiseItemsChanged(0);
|
||||
}
|
||||
|
||||
private void LoadCommands()
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
using Microsoft.CmdPal.Common.Services;
|
||||
using Microsoft.CmdPal.Extensions;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
@@ -21,6 +22,8 @@ public sealed class CommandProviderWrapper
|
||||
|
||||
public IFallbackCommandItem[] FallbackItems { get; private set; } = [];
|
||||
|
||||
public event TypedEventHandler<CommandProviderWrapper, ItemsChangedEventArgs>? CommandsChanged;
|
||||
|
||||
public string Id { get; private set; } = string.Empty;
|
||||
|
||||
public string DisplayName { get; private set; } = string.Empty;
|
||||
@@ -34,6 +37,8 @@ public sealed class CommandProviderWrapper
|
||||
public CommandProviderWrapper(ICommandProvider provider)
|
||||
{
|
||||
_commandProvider = provider;
|
||||
_commandProvider.ItemsChanged += CommandProvider_ItemsChanged;
|
||||
|
||||
isValid = true;
|
||||
Id = provider.Id;
|
||||
DisplayName = provider.DisplayName;
|
||||
@@ -56,6 +61,15 @@ public sealed class CommandProviderWrapper
|
||||
}
|
||||
|
||||
_commandProvider = provider;
|
||||
|
||||
try
|
||||
{
|
||||
_commandProvider.ItemsChanged += CommandProvider_ItemsChanged;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
isValid = true;
|
||||
}
|
||||
|
||||
@@ -106,4 +120,15 @@ public sealed class CommandProviderWrapper
|
||||
public override bool Equals(object? obj) => obj is CommandProviderWrapper wrapper && isValid == wrapper.isValid;
|
||||
|
||||
public override int GetHashCode() => _commandProvider.GetHashCode();
|
||||
|
||||
private void CommandProvider_ItemsChanged(object sender, ItemsChangedEventArgs args)
|
||||
{
|
||||
// We don't want to handle this ourselves - we want the
|
||||
// TopLevelCommandManager to know about this, so they can remove
|
||||
// our old commands from their own list.
|
||||
//
|
||||
// In handling this, a call will be made to `LoadTopLevelCommands` to
|
||||
// retrieve the new items.
|
||||
this.CommandsChanged?.Invoke(this, args);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Microsoft.CmdPal.Common.Services;
|
||||
using Microsoft.CmdPal.Extensions;
|
||||
using Microsoft.CmdPal.Extensions.Helpers;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
@@ -53,6 +54,88 @@ public partial class TopLevelCommandManager(IServiceProvider _serviceProvider) :
|
||||
{
|
||||
TopLevelCommands.Add(new(new(i), true));
|
||||
}
|
||||
|
||||
commandProvider.CommandsChanged += CommandProvider_CommandsChanged;
|
||||
}
|
||||
|
||||
private void CommandProvider_CommandsChanged(CommandProviderWrapper sender, ItemsChangedEventArgs args)
|
||||
{
|
||||
// By all accounts, we're already on a background thread (the COM call
|
||||
// to handle the event shouldn't be on the main thread.). But just to
|
||||
// be sure we don't block the caller, hop off this thread
|
||||
_ = Task.Run(async () => await UpdateCommandsForProvider(sender, args));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when a command provider raises its ItemsChanged event. We'll
|
||||
/// remove the old commands from the top-level list and try to put the new
|
||||
/// ones in the same place in the list.
|
||||
/// </summary>
|
||||
/// <param name="sender">The provider who's commands changed</param>
|
||||
/// <param name="args">the ItemsChangedEvent the provider raised</param>
|
||||
/// <returns>an awaitable task</returns>
|
||||
private async Task UpdateCommandsForProvider(CommandProviderWrapper sender, ItemsChangedEventArgs args)
|
||||
{
|
||||
// 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<TopLevelCommandWrapper> clone = [.. TopLevelCommands];
|
||||
List<TopLevelCommandWrapper> newItems = [];
|
||||
var startIndex = -1;
|
||||
var firstCommand = sender.TopLevelItems[0];
|
||||
var commandsToRemove = sender.TopLevelItems.Length + sender.FallbackItems.Length;
|
||||
|
||||
// Tricky: all Commands from a single provider get added to the
|
||||
// top-level list all together, in a row. So if we find just the first
|
||||
// one, we can slice it out and insert the new ones there.
|
||||
for (var i = 0; i < clone.Count; i++)
|
||||
{
|
||||
var wrapper = clone[i];
|
||||
try
|
||||
{
|
||||
var thisCommand = wrapper.Model.Unsafe;
|
||||
if (thisCommand != null)
|
||||
{
|
||||
var isTheSame = thisCommand == firstCommand;
|
||||
if (isTheSame)
|
||||
{
|
||||
startIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch the new items
|
||||
await sender.LoadTopLevelCommands();
|
||||
foreach (var i in sender.TopLevelItems)
|
||||
{
|
||||
newItems.Add(new(new(i), false));
|
||||
}
|
||||
|
||||
foreach (var i in sender.FallbackItems)
|
||||
{
|
||||
newItems.Add(new(new(i), true));
|
||||
}
|
||||
|
||||
// Slice out the old commands
|
||||
if (startIndex != -1)
|
||||
{
|
||||
clone.RemoveRange(startIndex, commandsToRemove);
|
||||
}
|
||||
else
|
||||
{
|
||||
// ... or, just stick them at the end (this is unexpected)
|
||||
startIndex = clone.Count;
|
||||
}
|
||||
|
||||
// add the new commands into the list at the place we found the old ones
|
||||
clone.InsertRange(startIndex, newItems);
|
||||
|
||||
// now update the actual observable list with the new contents
|
||||
ListHelpers.InPlaceUpdateList(TopLevelCommands, clone);
|
||||
}
|
||||
|
||||
// Load commands from our extensions.
|
||||
|
||||
Reference in New Issue
Block a user