mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-16 11:48:06 +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:
@@ -2,95 +2,267 @@
|
||||
// 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.ObjectModel;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Windows.Foundation;
|
||||
using WyHash;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public sealed partial class TopLevelViewModel : ObservableObject
|
||||
public sealed partial class TopLevelViewModel : ObservableObject, IListItem
|
||||
{
|
||||
private readonly SettingsModel _settings;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly CommandItemViewModel _commandItemViewModel;
|
||||
|
||||
// TopLevelCommandItemWrapper is a ListItem, but it's in-memory for the app already.
|
||||
// We construct it either from data that we pulled from the cache, or from the
|
||||
// extension, but the data in it is all in our process now.
|
||||
private readonly TopLevelCommandItemWrapper _item;
|
||||
private readonly string _commandProviderId;
|
||||
|
||||
public IconInfoViewModel Icon { get; private set; }
|
||||
private string IdFromModel => _commandItemViewModel.Command.Id;
|
||||
|
||||
public string Title => _item.Title;
|
||||
private string _generatedId = string.Empty;
|
||||
|
||||
public string Subtitle => _item.Subtitle;
|
||||
private HotkeySettings? _hotkey;
|
||||
|
||||
private CommandAlias? Alias { get; set; }
|
||||
|
||||
public bool IsFallback { get; private set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ObservableCollection<Tag> Tags { get; set; } = [];
|
||||
|
||||
public string Id => string.IsNullOrEmpty(IdFromModel) ? _generatedId : IdFromModel;
|
||||
|
||||
public CommandPaletteHost ExtensionHost { get; private set; }
|
||||
|
||||
public CommandViewModel CommandViewModel => _commandItemViewModel.Command;
|
||||
|
||||
public CommandItemViewModel ItemViewModel => _commandItemViewModel;
|
||||
|
||||
////// ICommandItem
|
||||
public string Title => _commandItemViewModel.Title;
|
||||
|
||||
public string Subtitle => _commandItemViewModel.Subtitle;
|
||||
|
||||
public IIconInfo Icon => _commandItemViewModel.Icon;
|
||||
|
||||
ICommand? ICommandItem.Command => _commandItemViewModel.Command.Model.Unsafe;
|
||||
|
||||
IContextItem?[] ICommandItem.MoreCommands => _commandItemViewModel.MoreCommands.Select(i => i.Model.Unsafe).ToArray();
|
||||
|
||||
////// IListItem
|
||||
ITag[] IListItem.Tags => Tags.ToArray();
|
||||
|
||||
IDetails? IListItem.Details => null;
|
||||
|
||||
string IListItem.Section => string.Empty;
|
||||
|
||||
string IListItem.TextToSuggest => string.Empty;
|
||||
|
||||
////// INotifyPropChanged
|
||||
public event TypedEventHandler<object, IPropChangedEventArgs>? PropChanged;
|
||||
|
||||
public HotkeySettings? Hotkey
|
||||
{
|
||||
get => _item.Hotkey;
|
||||
get => _hotkey;
|
||||
set
|
||||
{
|
||||
_serviceProvider.GetService<HotkeyManager>()!.UpdateHotkey(_item.Id, value);
|
||||
_item.Hotkey = value;
|
||||
_serviceProvider.GetService<HotkeyManager>()!.UpdateHotkey(Id, value);
|
||||
UpdateHotkey();
|
||||
UpdateTags();
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
private string _aliasText;
|
||||
public bool HasAlias => !string.IsNullOrEmpty(AliasText);
|
||||
|
||||
public string AliasText
|
||||
{
|
||||
get => _aliasText;
|
||||
get => Alias?.Alias ?? string.Empty;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _aliasText, value))
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
UpdateAlias();
|
||||
Alias = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Alias is CommandAlias a)
|
||||
{
|
||||
a.Alias = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
Alias = new CommandAlias(value, Id);
|
||||
}
|
||||
}
|
||||
|
||||
HandleChangeAlias();
|
||||
}
|
||||
}
|
||||
|
||||
private bool _isDirectAlias;
|
||||
|
||||
public bool IsDirectAlias
|
||||
{
|
||||
get => _isDirectAlias;
|
||||
get => Alias?.IsDirect ?? false;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _isDirectAlias, value))
|
||||
if (Alias is CommandAlias a)
|
||||
{
|
||||
UpdateAlias();
|
||||
a.IsDirect = value;
|
||||
}
|
||||
|
||||
HandleChangeAlias();
|
||||
}
|
||||
}
|
||||
|
||||
public TopLevelViewModel(TopLevelCommandItemWrapper item, SettingsModel settings, IServiceProvider serviceProvider)
|
||||
public TopLevelViewModel(
|
||||
CommandItemViewModel item,
|
||||
bool isFallback,
|
||||
CommandPaletteHost extensionHost,
|
||||
string commandProviderId,
|
||||
SettingsModel settings,
|
||||
IServiceProvider serviceProvider)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
_settings = settings;
|
||||
_commandProviderId = commandProviderId;
|
||||
_commandItemViewModel = item;
|
||||
|
||||
_item = item;
|
||||
Icon = new(item.Icon ?? item.Command?.Icon);
|
||||
Icon.InitializeProperties();
|
||||
IsFallback = isFallback;
|
||||
ExtensionHost = extensionHost;
|
||||
|
||||
var aliases = _serviceProvider.GetService<AliasManager>()!;
|
||||
_isDirectAlias = _item.Alias?.IsDirect ?? false;
|
||||
_aliasText = _item.Alias?.Alias ?? string.Empty;
|
||||
item.PropertyChanged += Item_PropertyChanged;
|
||||
|
||||
// UpdateAlias();
|
||||
// UpdateHotkey();
|
||||
// UpdateTags();
|
||||
}
|
||||
|
||||
private void Item_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(e.PropertyName))
|
||||
{
|
||||
PropChanged?.Invoke(this, new PropChangedEventArgs(e.PropertyName));
|
||||
|
||||
if (e.PropertyName == "IsInitialized")
|
||||
{
|
||||
GenerateId();
|
||||
|
||||
UpdateAlias();
|
||||
UpdateHotkey();
|
||||
UpdateTags();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Save() => SettingsModel.SaveSettings(_settings);
|
||||
|
||||
private void UpdateAlias()
|
||||
private void HandleChangeAlias()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_aliasText))
|
||||
{
|
||||
_item.UpdateAlias(null);
|
||||
}
|
||||
else
|
||||
{
|
||||
var newAlias = new CommandAlias(_aliasText, _item.Id, _isDirectAlias);
|
||||
_item.UpdateAlias(newAlias);
|
||||
}
|
||||
|
||||
SetAlias(Alias);
|
||||
Save();
|
||||
}
|
||||
|
||||
public void SetAlias(CommandAlias? newAlias)
|
||||
{
|
||||
_serviceProvider.GetService<AliasManager>()!.UpdateAlias(Id, newAlias);
|
||||
UpdateAlias();
|
||||
UpdateTags();
|
||||
}
|
||||
|
||||
private void UpdateAlias()
|
||||
{
|
||||
// Add tags for the alias, if we have one.
|
||||
var aliases = _serviceProvider.GetService<AliasManager>();
|
||||
if (aliases != null)
|
||||
{
|
||||
Alias = aliases.AliasFromId(Id);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateHotkey()
|
||||
{
|
||||
var hotkey = _settings.CommandHotkeys.Where(hk => hk.CommandId == Id).FirstOrDefault();
|
||||
if (hotkey != null)
|
||||
{
|
||||
_hotkey = hotkey.Hotkey;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateTags()
|
||||
{
|
||||
List<Tag> tags = new();
|
||||
|
||||
if (Hotkey != null)
|
||||
{
|
||||
tags.Add(new Tag() { Text = Hotkey.ToString() });
|
||||
}
|
||||
|
||||
if (Alias != null)
|
||||
{
|
||||
tags.Add(new Tag() { Text = Alias.SearchPrefix });
|
||||
}
|
||||
|
||||
PropChanged?.Invoke(this, new PropChangedEventArgs(nameof(Tags)));
|
||||
|
||||
DoOnUiThread(
|
||||
() =>
|
||||
{
|
||||
ListHelpers.InPlaceUpdateList(Tags, tags);
|
||||
});
|
||||
}
|
||||
|
||||
private void GenerateId()
|
||||
{
|
||||
// Use WyHash64 to generate stable ID hashes.
|
||||
// manually seeding with 0, so that the hash is stable across launches
|
||||
var result = WyHash64.ComputeHash64(_commandProviderId + Title + Subtitle, seed: 0);
|
||||
_generatedId = $"{_commandProviderId}{result}";
|
||||
}
|
||||
|
||||
private void DoOnUiThread(Action action)
|
||||
{
|
||||
if (_commandItemViewModel.PageContext.TryGetTarget(out var pageContext))
|
||||
{
|
||||
Task.Factory.StartNew(
|
||||
action,
|
||||
CancellationToken.None,
|
||||
TaskCreationOptions.None,
|
||||
pageContext.Scheduler);
|
||||
}
|
||||
}
|
||||
|
||||
public void TryUpdateFallbackText(string newQuery)
|
||||
{
|
||||
if (!IsFallback)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var model = _commandItemViewModel.Model.Unsafe;
|
||||
if (model is IFallbackCommandItem fallback)
|
||||
{
|
||||
var wasEmpty = string.IsNullOrEmpty(Title);
|
||||
fallback.FallbackHandler.UpdateQuery(newQuery);
|
||||
var isEmpty = string.IsNullOrEmpty(Title);
|
||||
if (wasEmpty != isEmpty)
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<UpdateFallbackItemsMessage>();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user