mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-08 20:27:36 +02:00
CmdPal: Handle CommandItem Title changes properly and raise notification every time it changes (#40513)
<!-- Enter a brief description/summary of your PR here. What does it fix/what does it change/how was it tested (even manually, if necessary)? --> ## Summary of the Pull Request <!-- Please review the items on the PR checklist before submitting--> ## PR Checklist - [x] **Closes:** #39167 - [ ] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [ ] **Tests:** Added/updated and all pass - [ ] **Localization:** All end-user-facing strings can be localized - [ ] **Dev docs:** Added/updated - [ ] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx <!-- Provide a more detailed description of the PR, other things fixed, or any additional comments/features here --> ## Detailed Description of the Pull Request / Additional comments <!-- Describe how you validated the behavior. Add automated tests wherever possible, but list manual validation steps taken as well --> ## Validation Steps Performed
This commit is contained in:
@@ -313,6 +313,10 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
|||||||
|
|
||||||
Command = new(model.Command, PageContext);
|
Command = new(model.Command, PageContext);
|
||||||
Command.InitializeProperties();
|
Command.InitializeProperties();
|
||||||
|
|
||||||
|
// Extensions based on Command Palette SDK < 0.3 CommandItem class won't notify when Title changes because Command
|
||||||
|
// or Command.Name change. This is a workaround to ensure that the Title is always up-to-date for extensions with old SDK.
|
||||||
|
_itemTitle = model.Title;
|
||||||
UpdateProperty(nameof(Name));
|
UpdateProperty(nameof(Name));
|
||||||
UpdateProperty(nameof(Title));
|
UpdateProperty(nameof(Title));
|
||||||
UpdateProperty(nameof(Icon));
|
UpdateProperty(nameof(Icon));
|
||||||
@@ -385,6 +389,14 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
|||||||
switch (propertyName)
|
switch (propertyName)
|
||||||
{
|
{
|
||||||
case nameof(Command.Name):
|
case nameof(Command.Name):
|
||||||
|
// Extensions based on Command Palette SDK < 0.3 CommandItem class won't notify when Title changes because Command
|
||||||
|
// or Command.Name change. This is a workaround to ensure that the Title is always up-to-date for extensions with old SDK.
|
||||||
|
var model = _commandItemModel.Unsafe;
|
||||||
|
if (model != null)
|
||||||
|
{
|
||||||
|
_itemTitle = model.Title;
|
||||||
|
}
|
||||||
|
|
||||||
UpdateProperty(nameof(Title));
|
UpdateProperty(nameof(Title));
|
||||||
UpdateProperty(nameof(Name));
|
UpdateProperty(nameof(Name));
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
|||||||
public partial class CommandItem : BaseObservable, ICommandItem
|
public partial class CommandItem : BaseObservable, ICommandItem
|
||||||
{
|
{
|
||||||
private ICommand? _command;
|
private ICommand? _command;
|
||||||
|
private WeakEventListener<CommandItem, object, IPropChangedEventArgs>? _commandListener;
|
||||||
|
private string _title = string.Empty;
|
||||||
|
|
||||||
public virtual IIconInfo? Icon
|
public virtual IIconInfo? Icon
|
||||||
{
|
{
|
||||||
@@ -20,17 +22,15 @@ public partial class CommandItem : BaseObservable, ICommandItem
|
|||||||
|
|
||||||
public virtual string Title
|
public virtual string Title
|
||||||
{
|
{
|
||||||
get => !string.IsNullOrEmpty(field) ? field : _command?.Name ?? string.Empty;
|
get => !string.IsNullOrEmpty(_title) ? _title : _command?.Name ?? string.Empty;
|
||||||
|
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
field = value;
|
_title = value;
|
||||||
OnPropertyChanged(nameof(Title));
|
OnPropertyChanged(nameof(Title));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
= string.Empty;
|
|
||||||
|
|
||||||
public virtual string Subtitle
|
public virtual string Subtitle
|
||||||
{
|
{
|
||||||
get;
|
get;
|
||||||
@@ -48,8 +48,33 @@ public partial class CommandItem : BaseObservable, ICommandItem
|
|||||||
get => _command;
|
get => _command;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
|
if (_commandListener != null)
|
||||||
|
{
|
||||||
|
_commandListener.Detach();
|
||||||
|
_commandListener = null;
|
||||||
|
}
|
||||||
|
|
||||||
_command = value;
|
_command = value;
|
||||||
|
|
||||||
|
if (value != null)
|
||||||
|
{
|
||||||
|
_commandListener = new(this, OnCommandPropertyChanged, listener => value.PropChanged -= listener.OnEvent);
|
||||||
|
value.PropChanged += _commandListener.OnEvent;
|
||||||
|
}
|
||||||
|
|
||||||
OnPropertyChanged(nameof(Command));
|
OnPropertyChanged(nameof(Command));
|
||||||
|
if (string.IsNullOrWhiteSpace(_title))
|
||||||
|
{
|
||||||
|
OnPropertyChanged(nameof(Title));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OnCommandPropertyChanged(CommandItem instance, object source, IPropChangedEventArgs args)
|
||||||
|
{
|
||||||
|
if (args.PropertyName == nameof(ICommand.Name))
|
||||||
|
{
|
||||||
|
instance.OnPropertyChanged(nameof(Title));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
// 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.ComponentModel;
|
||||||
|
|
||||||
|
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Implements a weak event listener that allows the owner to be garbage
|
||||||
|
/// collected if its only remaining link is an event handler.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TInstance">Type of instance listening for the event.</typeparam>
|
||||||
|
/// <typeparam name="TSource">Type of source for the event.</typeparam>
|
||||||
|
/// <typeparam name="TEventArgs">Type of event arguments for the event.</typeparam>
|
||||||
|
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||||
|
internal sealed class WeakEventListener<TInstance, TSource, TEventArgs>
|
||||||
|
where TInstance : class
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// WeakReference to the instance listening for the event.
|
||||||
|
/// </summary>
|
||||||
|
private readonly WeakReference<TInstance> _weakInstance;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="WeakEventListener{TInstance, TSource, TEventArgs}"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="instance">Instance subscribing to the event.</param>
|
||||||
|
/// <param name="onEventAction">Event handler executed when event is raised.</param>
|
||||||
|
/// <param name="onDetachAction">Action to execute when instance was collected.</param>
|
||||||
|
public WeakEventListener(
|
||||||
|
TInstance instance,
|
||||||
|
Action<TInstance, TSource, TEventArgs>? onEventAction = null,
|
||||||
|
Action<WeakEventListener<TInstance, TSource, TEventArgs>>? onDetachAction = null)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(instance);
|
||||||
|
|
||||||
|
_weakInstance = new(instance);
|
||||||
|
OnEventAction = onEventAction;
|
||||||
|
OnDetachAction = onDetachAction;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the method to call when the event fires.
|
||||||
|
/// </summary>
|
||||||
|
public Action<TInstance, TSource, TEventArgs>? OnEventAction { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the method to call when detaching from the event.
|
||||||
|
/// </summary>
|
||||||
|
public Action<WeakEventListener<TInstance, TSource, TEventArgs>>? OnDetachAction { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handler for the subscribed event calls OnEventAction to handle it.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="source">Event source.</param>
|
||||||
|
/// <param name="eventArgs">Event arguments.</param>
|
||||||
|
public void OnEvent(TSource source, TEventArgs eventArgs)
|
||||||
|
{
|
||||||
|
if (_weakInstance.TryGetTarget(out var target))
|
||||||
|
{
|
||||||
|
// Call registered action
|
||||||
|
OnEventAction?.Invoke(target, source, eventArgs);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Detach from event
|
||||||
|
Detach();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Detaches from the subscribed event.
|
||||||
|
/// </summary>
|
||||||
|
public void Detach()
|
||||||
|
{
|
||||||
|
OnDetachAction?.Invoke(this);
|
||||||
|
OnDetachAction = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user