Files
PowerToys/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/CommandItem.cs
Jiří Polášek 466a94eb40 CmdPal: Fix updating primary command and context menu and app icons (#42155)
## Summary of the Pull Request

This PR fixes three issues in one go:
- Restores missing icons in app context menus.
- Fixes propagation of changes from a command item to the context menu
item for the primary action.
- Ensures the context menus stay in sync when underlying command items
change.

Details:
- Correctly propagates updates of name, icon, and subtitle from a
command item to its primary command
(`CommandItemViewModel._defaultCommandContextItemViewModel`).
- Correctly propagate updates of command's name to title
(`CommandItem.ctor`).
- Fixes icon loading for application items: `AppCommand` no longer loads
an app icon by default but instead relies on the caller to provide one
(since `AppListItem` also handles icon loading).
- Adds a generic fallback icon for apps when an icon cannot be loaded.
- Updates bindings on context menu items to `OneWay`, ensuring the UI
properly reflects item changes.
- Adds a sample that showcases dynamically updated commands (with cats
and dolphins!) to _Samples → List Page Sample Command_.

⚠️ Toolkit changes:
- `CommandItem` won't capture assigned Command's name as its `Title`.
This will allow it to propagate future changes to `Command.Name`.

Pictures? Moving ones!


https://github.com/user-attachments/assets/1a482394-d222-4f7c-9922-bb67d47dc566

<img width="864" height="538" alt="image"
src="https://github.com/user-attachments/assets/12f07b3e-f41c-4c40-a4e5-315f40676c52"
/>


<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] Closes: #40946
- [x] Related: #40991 
- [ ] **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
2025-10-06 09:45:10 -05:00

136 lines
3.1 KiB
C#

// 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.CommandPalette.Extensions.Toolkit;
public partial class CommandItem : BaseObservable, ICommandItem
{
private ICommand? _command;
private WeakEventListener<CommandItem, object, IPropChangedEventArgs>? _commandListener;
private string _title = string.Empty;
public virtual IIconInfo? Icon
{
get => field;
set
{
field = value;
OnPropertyChanged(nameof(Icon));
}
}
public virtual string Title
{
get => !string.IsNullOrEmpty(_title) ? _title : _command?.Name ?? string.Empty;
set
{
_title = value;
OnPropertyChanged(nameof(Title));
}
}
public virtual string Subtitle
{
get;
set
{
field = value;
OnPropertyChanged(nameof(Subtitle));
}
}
= string.Empty;
public virtual ICommand? Command
{
get => _command;
set
{
if (_commandListener is not null)
{
_commandListener.Detach();
_commandListener = null;
}
_command = value;
if (value is not null)
{
_commandListener = new(this, OnCommandPropertyChanged, listener => value.PropChanged -= listener.OnEvent);
value.PropChanged += _commandListener.OnEvent;
}
OnPropertyChanged(nameof(Command));
if (string.IsNullOrEmpty(_title))
{
OnPropertyChanged(nameof(Title));
}
}
}
private void OnCommandPropertyChanged(CommandItem instance, object source, IPropChangedEventArgs args)
{
// command's name affects Title only if Title wasn't explicitly set
if (args.PropertyName == nameof(ICommand.Name) && string.IsNullOrEmpty(_title))
{
instance.OnPropertyChanged(nameof(Title));
}
}
public virtual IContextItem[] MoreCommands
{
get;
set
{
field = value;
OnPropertyChanged(nameof(MoreCommands));
}
}
= [];
public CommandItem()
: this(new NoOpCommand())
{
}
public CommandItem(ICommand command)
{
Command = command;
}
public CommandItem(ICommandItem other)
{
Command = other.Command;
Subtitle = other.Subtitle;
Icon = (IconInfo?)other.Icon;
MoreCommands = other.MoreCommands;
}
public CommandItem(
string title,
string subtitle = "",
string name = "",
Action? action = null,
ICommandResult? result = null)
{
var c = new AnonymousCommand(action);
if (!string.IsNullOrEmpty(name))
{
c.Name = name;
}
if (result is not null)
{
c.Result = result;
}
Command = c;
Title = title;
Subtitle = subtitle;
}
}