mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-03 17:56:44 +02:00
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
This commit is contained in:
@@ -17,7 +17,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
public ExtensionObject<ICommandItem> Model => _commandItemModel;
|
||||
|
||||
private readonly ExtensionObject<ICommandItem> _commandItemModel = new(null);
|
||||
private CommandContextItemViewModel? _defaultCommandContextItem;
|
||||
private CommandContextItemViewModel? _defaultCommandContextItemViewModel;
|
||||
|
||||
internal InitializedState Initialized { get; private set; } = InitializedState.Uninitialized;
|
||||
|
||||
@@ -43,9 +43,9 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
|
||||
public string Subtitle { get; private set; } = string.Empty;
|
||||
|
||||
private IconInfoViewModel _listItemIcon = new(null);
|
||||
private IconInfoViewModel _icon = new(null);
|
||||
|
||||
public IconInfoViewModel Icon => _listItemIcon.IsSet ? _listItemIcon : Command.Icon;
|
||||
public IconInfoViewModel Icon => _icon.IsSet ? _icon : Command.Icon;
|
||||
|
||||
public CommandViewModel Command { get; private set; }
|
||||
|
||||
@@ -69,9 +69,9 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
{
|
||||
get
|
||||
{
|
||||
List<IContextItemViewModel> l = _defaultCommandContextItem is null ?
|
||||
List<IContextItemViewModel> l = _defaultCommandContextItemViewModel is null ?
|
||||
new() :
|
||||
[_defaultCommandContextItem];
|
||||
[_defaultCommandContextItemViewModel];
|
||||
|
||||
l.AddRange(MoreCommands);
|
||||
return l;
|
||||
@@ -136,11 +136,11 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
|
||||
Command.InitializeProperties();
|
||||
|
||||
var listIcon = model.Icon;
|
||||
if (listIcon is not null)
|
||||
var icon = model.Icon;
|
||||
if (icon is not null)
|
||||
{
|
||||
_listItemIcon = new(listIcon);
|
||||
_listItemIcon.InitializeProperties();
|
||||
_icon = new(icon);
|
||||
_icon.InitializeProperties();
|
||||
}
|
||||
|
||||
// TODO: Do these need to go into FastInit?
|
||||
@@ -201,21 +201,19 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
|
||||
if (!string.IsNullOrEmpty(model.Command?.Name))
|
||||
{
|
||||
_defaultCommandContextItem = new(new CommandContextItem(model.Command!), PageContext)
|
||||
_defaultCommandContextItemViewModel = new CommandContextItemViewModel(new CommandContextItem(model.Command!), PageContext)
|
||||
{
|
||||
_itemTitle = Name,
|
||||
Subtitle = Subtitle,
|
||||
Command = Command,
|
||||
|
||||
// TODO this probably should just be a CommandContextItemViewModel(CommandItemViewModel) ctor, or a copy ctor or whatever
|
||||
// Anything we set manually here must stay in sync with the corresponding properties on CommandItemViewModel.
|
||||
};
|
||||
|
||||
// Only set the icon on the context item for us if our command didn't
|
||||
// have its own icon
|
||||
if (!Command.HasIcon)
|
||||
{
|
||||
_defaultCommandContextItem._listItemIcon = _listItemIcon;
|
||||
}
|
||||
UpdateDefaultContextItemIcon();
|
||||
}
|
||||
|
||||
Initialized |= InitializedState.SelectionInitialized;
|
||||
@@ -238,7 +236,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
_itemTitle = "Error";
|
||||
Subtitle = "Item failed to load";
|
||||
MoreCommands = [];
|
||||
_listItemIcon = _errorIcon;
|
||||
_icon = _errorIcon;
|
||||
Initialized |= InitializedState.Error;
|
||||
}
|
||||
|
||||
@@ -275,7 +273,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
_itemTitle = "Error";
|
||||
Subtitle = "Item failed to load";
|
||||
MoreCommands = [];
|
||||
_listItemIcon = _errorIcon;
|
||||
_icon = _errorIcon;
|
||||
Initialized |= InitializedState.Error;
|
||||
}
|
||||
|
||||
@@ -305,17 +303,18 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
switch (propertyName)
|
||||
{
|
||||
case nameof(Command):
|
||||
if (Command is not null)
|
||||
{
|
||||
Command.PropertyChanged -= Command_PropertyChanged;
|
||||
}
|
||||
|
||||
Command.PropertyChanged -= Command_PropertyChanged;
|
||||
Command = new(model.Command, PageContext);
|
||||
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;
|
||||
|
||||
_defaultCommandContextItemViewModel?.Command = Command;
|
||||
_defaultCommandContextItemViewModel?.UpdateTitle(_itemTitle);
|
||||
UpdateDefaultContextItemIcon();
|
||||
|
||||
UpdateProperty(nameof(Name));
|
||||
UpdateProperty(nameof(Title));
|
||||
UpdateProperty(nameof(Icon));
|
||||
@@ -326,12 +325,22 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
break;
|
||||
|
||||
case nameof(Subtitle):
|
||||
this.Subtitle = model.Subtitle;
|
||||
var modelSubtitle = model.Subtitle;
|
||||
this.Subtitle = modelSubtitle;
|
||||
_defaultCommandContextItemViewModel?.Subtitle = modelSubtitle;
|
||||
break;
|
||||
|
||||
case nameof(Icon):
|
||||
_listItemIcon = new(model.Icon);
|
||||
_listItemIcon.InitializeProperties();
|
||||
var oldIcon = _icon;
|
||||
_icon = new(model.Icon);
|
||||
_icon.InitializeProperties();
|
||||
if (oldIcon.IsSet || _icon.IsSet)
|
||||
{
|
||||
UpdateProperty(nameof(Icon));
|
||||
}
|
||||
|
||||
UpdateDefaultContextItemIcon();
|
||||
|
||||
break;
|
||||
|
||||
case nameof(model.MoreCommands):
|
||||
@@ -378,26 +387,49 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
private void Command_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
{
|
||||
var propertyName = e.PropertyName;
|
||||
var model = _commandItemModel.Unsafe;
|
||||
if (model is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
switch (propertyName)
|
||||
{
|
||||
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 is not null)
|
||||
{
|
||||
_itemTitle = model.Title;
|
||||
}
|
||||
_itemTitle = model.Title;
|
||||
UpdateProperty(nameof(Title), nameof(Name));
|
||||
|
||||
UpdateProperty(nameof(Title));
|
||||
UpdateProperty(nameof(Name));
|
||||
_defaultCommandContextItemViewModel?.UpdateTitle(model.Command.Name);
|
||||
break;
|
||||
|
||||
case nameof(Command.Icon):
|
||||
UpdateDefaultContextItemIcon();
|
||||
UpdateProperty(nameof(Icon));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateDefaultContextItemIcon()
|
||||
{
|
||||
// Command icon takes precedence over our icon on the primary command
|
||||
_defaultCommandContextItemViewModel?.UpdateIcon(Command.Icon.IsSet ? Command.Icon : _icon);
|
||||
}
|
||||
|
||||
private void UpdateTitle(string? title)
|
||||
{
|
||||
_itemTitle = title ?? string.Empty;
|
||||
UpdateProperty(nameof(Title));
|
||||
}
|
||||
|
||||
private void UpdateIcon(IIconInfo? iconInfo)
|
||||
{
|
||||
_icon = new(iconInfo);
|
||||
_icon.InitializeProperties();
|
||||
UpdateProperty(nameof(Icon));
|
||||
}
|
||||
|
||||
protected override void UnsafeCleanup()
|
||||
{
|
||||
base.UnsafeCleanup();
|
||||
@@ -411,10 +443,10 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
}
|
||||
|
||||
// _listItemIcon.SafeCleanup();
|
||||
_listItemIcon = new(null); // necessary?
|
||||
_icon = new(null); // necessary?
|
||||
|
||||
_defaultCommandContextItem?.SafeCleanup();
|
||||
_defaultCommandContextItem = null;
|
||||
_defaultCommandContextItemViewModel?.SafeCleanup();
|
||||
_defaultCommandContextItemViewModel = null;
|
||||
|
||||
Command.PropertyChanged -= Command_PropertyChanged;
|
||||
Command.SafeCleanup();
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
|
||||
<!-- Template for context items in the context item menu -->
|
||||
<DataTemplate x:Key="DefaultContextMenuViewModelTemplate" x:DataType="coreViewModels:CommandContextItemViewModel">
|
||||
<Grid AutomationProperties.Name="{x:Bind Title}">
|
||||
<Grid AutomationProperties.Name="{x:Bind Title, Mode=OneWay}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="32" />
|
||||
<ColumnDefinition Width="*" />
|
||||
@@ -42,7 +42,7 @@
|
||||
Height="16"
|
||||
Margin="4,0,0,0"
|
||||
HorizontalAlignment="Left"
|
||||
SourceKey="{x:Bind Icon}"
|
||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
|
||||
<TextBlock
|
||||
x:Name="TitleTextBlock"
|
||||
@@ -51,11 +51,11 @@
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
MaxLines="1"
|
||||
Text="{x:Bind Title}"
|
||||
Text="{x:Bind Title, Mode=OneWay}"
|
||||
TextTrimming="WordEllipsis"
|
||||
TextWrapping="NoWrap">
|
||||
<ToolTipService.ToolTip>
|
||||
<ToolTip Content="{x:Bind Title}" Visibility="{Binding IsTextTrimmed, ElementName=TitleTextBlock, Converter={StaticResource BoolToVisibilityConverter}}" />
|
||||
<ToolTip Content="{x:Bind Title, Mode=OneWay}" Visibility="{Binding IsTextTrimmed, ElementName=TitleTextBlock, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
|
||||
</ToolTipService.ToolTip>
|
||||
</TextBlock>
|
||||
<TextBlock
|
||||
@@ -65,13 +65,13 @@
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{ThemeResource MenuFlyoutItemKeyboardAcceleratorTextForeground}"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind RequestedShortcut, Converter={StaticResource KeyChordToStringConverter}}" />
|
||||
Text="{x:Bind RequestedShortcut, Mode=OneWay, Converter={StaticResource KeyChordToStringConverter}}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
<!-- Template for context items flagged as critical -->
|
||||
<DataTemplate x:Key="CriticalContextMenuViewModelTemplate" x:DataType="coreViewModels:CommandContextItemViewModel">
|
||||
<Grid AutomationProperties.Name="{x:Bind Title}">
|
||||
<Grid AutomationProperties.Name="{x:Bind Title, Mode=OneWay}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="32" />
|
||||
<ColumnDefinition Width="*" />
|
||||
@@ -83,7 +83,7 @@
|
||||
Margin="4,0,0,0"
|
||||
HorizontalAlignment="Left"
|
||||
Foreground="{ThemeResource SystemFillColorCriticalBrush}"
|
||||
SourceKey="{x:Bind Icon}"
|
||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
|
||||
<TextBlock
|
||||
x:Name="TitleTextBlock"
|
||||
@@ -93,11 +93,11 @@
|
||||
VerticalAlignment="Center"
|
||||
MaxLines="1"
|
||||
Style="{StaticResource ContextItemTitleTextBlockCriticalStyle}"
|
||||
Text="{x:Bind Title}"
|
||||
Text="{x:Bind Title, Mode=OneWay}"
|
||||
TextTrimming="WordEllipsis"
|
||||
TextWrapping="NoWrap">
|
||||
<ToolTipService.ToolTip>
|
||||
<ToolTip Content="{x:Bind Title}" Visibility="{Binding IsTextTrimmed, ElementName=TitleTextBlock, Converter={StaticResource BoolToVisibilityConverter}}" />
|
||||
<ToolTip Content="{x:Bind Title, Mode=OneWay}" Visibility="{Binding IsTextTrimmed, ElementName=TitleTextBlock, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
|
||||
</ToolTipService.ToolTip>
|
||||
</TextBlock>
|
||||
<TextBlock
|
||||
@@ -106,7 +106,7 @@
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource ContextItemCaptionTextBlockCriticalStyle}"
|
||||
Text="{x:Bind RequestedShortcut, Converter={StaticResource KeyChordToStringConverter}}" />
|
||||
Text="{x:Bind RequestedShortcut, Mode=OneWay, Converter={StaticResource KeyChordToStringConverter}}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
|
||||
@@ -16,24 +16,19 @@ using WyHash;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Apps;
|
||||
|
||||
public sealed partial class AppCommand : InvokableCommand
|
||||
internal sealed partial class AppCommand : InvokableCommand
|
||||
{
|
||||
private readonly AppItem _app;
|
||||
|
||||
public AppCommand(AppItem app)
|
||||
{
|
||||
_app = app;
|
||||
|
||||
Name = Resources.run_command_action;
|
||||
Name = Resources.run_command_action!;
|
||||
Id = GenerateId();
|
||||
|
||||
if (!string.IsNullOrEmpty(app.IcoPath))
|
||||
{
|
||||
Icon = new(app.IcoPath);
|
||||
}
|
||||
Icon = Icons.GenericAppIcon;
|
||||
}
|
||||
|
||||
internal static async Task StartApp(string aumid)
|
||||
private static async Task StartApp(string aumid)
|
||||
{
|
||||
await Task.Run(() =>
|
||||
{
|
||||
@@ -58,7 +53,7 @@ public sealed partial class AppCommand : InvokableCommand
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
internal static async Task StartExe(string path)
|
||||
private static async Task StartExe(string path)
|
||||
{
|
||||
await Task.Run(() =>
|
||||
{
|
||||
@@ -73,7 +68,7 @@ public sealed partial class AppCommand : InvokableCommand
|
||||
});
|
||||
}
|
||||
|
||||
internal async Task Launch()
|
||||
private async Task Launch()
|
||||
{
|
||||
if (_app.IsPackaged)
|
||||
{
|
||||
|
||||
@@ -5,34 +5,51 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.Core.Common.Helpers;
|
||||
using Microsoft.CmdPal.Ext.Apps.Commands;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Windows.Storage.Streams;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Apps.Programs;
|
||||
|
||||
internal sealed partial class AppListItem : ListItem
|
||||
{
|
||||
private readonly AppItem _app;
|
||||
private static readonly Tag _appTag = new("App");
|
||||
|
||||
private readonly AppCommand _appCommand;
|
||||
private readonly AppItem _app;
|
||||
private readonly Lazy<Details> _details;
|
||||
private readonly Lazy<IconInfo> _icon;
|
||||
private readonly Lazy<Task<IconInfo?>> _iconLoadTask;
|
||||
|
||||
private InterlockedBoolean _isLoadingIcon;
|
||||
|
||||
public override IDetails? Details { get => _details.Value; set => base.Details = value; }
|
||||
|
||||
public override IIconInfo? Icon { get => _icon.Value; set => base.Icon = value; }
|
||||
public override IIconInfo? Icon
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_isLoadingIcon.Set())
|
||||
{
|
||||
_ = LoadIconAsync();
|
||||
}
|
||||
|
||||
return base.Icon;
|
||||
}
|
||||
set => base.Icon = value;
|
||||
}
|
||||
|
||||
public string AppIdentifier => _app.AppIdentifier;
|
||||
|
||||
public AppListItem(AppItem app, bool useThumbnails, bool isPinned)
|
||||
: base(new AppCommand(app))
|
||||
{
|
||||
Command = _appCommand = new AppCommand(app);
|
||||
_app = app;
|
||||
Title = app.Name;
|
||||
Subtitle = app.Subtitle;
|
||||
Tags = [_appTag];
|
||||
Icon = Icons.GenericAppIcon;
|
||||
|
||||
MoreCommands = AddPinCommands(_app.Commands!, isPinned);
|
||||
|
||||
@@ -43,12 +60,19 @@ internal sealed partial class AppListItem : ListItem
|
||||
return t.Result;
|
||||
});
|
||||
|
||||
_icon = new Lazy<IconInfo>(() =>
|
||||
_iconLoadTask = new Lazy<Task<IconInfo?>>(async () => await FetchIcon(useThumbnails));
|
||||
}
|
||||
|
||||
private async Task LoadIconAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var t = FetchIcon(useThumbnails);
|
||||
t.Wait();
|
||||
return t.Result;
|
||||
});
|
||||
Icon = _appCommand.Icon = await _iconLoadTask.Value ?? Icons.GenericAppIcon;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning($"Failed to load icon for {AppIdentifier}\n{ex}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<Details> BuildDetails()
|
||||
@@ -87,12 +111,12 @@ internal sealed partial class AppListItem : ListItem
|
||||
return new Details()
|
||||
{
|
||||
Title = this.Title,
|
||||
HeroImage = heroImage ?? this.Icon ?? new IconInfo(string.Empty),
|
||||
HeroImage = heroImage ?? this.Icon ?? Icons.GenericAppIcon,
|
||||
Metadata = metadata.ToArray(),
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<IconInfo> FetchIcon(bool useThumbnails)
|
||||
private async Task<IconInfo> FetchIcon(bool useThumbnails)
|
||||
{
|
||||
IconInfo? icon = null;
|
||||
if (_app.IsPackaged)
|
||||
@@ -108,12 +132,12 @@ internal sealed partial class AppListItem : ListItem
|
||||
var stream = await ThumbnailHelper.GetThumbnail(_app.ExePath);
|
||||
if (stream is not null)
|
||||
{
|
||||
var data = new IconData(RandomAccessStreamReference.CreateFromStream(stream));
|
||||
icon = new IconInfo(data, data);
|
||||
icon = IconInfo.FromStream(stream);
|
||||
}
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogDebug($"Failed to load icon for {AppIdentifier}:\n{ex}");
|
||||
}
|
||||
|
||||
icon = icon ?? new IconInfo(_app.IcoPath);
|
||||
|
||||
@@ -6,21 +6,23 @@ using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Apps;
|
||||
|
||||
internal sealed class Icons
|
||||
internal static class Icons
|
||||
{
|
||||
internal static IconInfo AllAppsIcon => IconHelpers.FromRelativePath("Assets\\AllApps.svg");
|
||||
internal static IconInfo AllAppsIcon { get; } = IconHelpers.FromRelativePath("Assets\\AllApps.svg");
|
||||
|
||||
internal static IconInfo RunAsUserIcon => new("\uE7EE"); // OtherUser icon
|
||||
internal static IconInfo RunAsUserIcon { get; } = new("\uE7EE"); // OtherUser icon
|
||||
|
||||
internal static IconInfo RunAsAdminIcon => new("\uE7EF"); // Admin icon
|
||||
internal static IconInfo RunAsAdminIcon { get; } = new("\uE7EF"); // Admin icon
|
||||
|
||||
internal static IconInfo OpenPathIcon => new("\ue838"); // Folder Open icon
|
||||
internal static IconInfo OpenPathIcon { get; } = new("\ue838"); // Folder Open icon
|
||||
|
||||
internal static IconInfo CopyIcon => new("\ue8c8"); // Copy icon
|
||||
internal static IconInfo CopyIcon { get; } = new("\ue8c8"); // Copy icon
|
||||
|
||||
public static IconInfo UnpinIcon { get; } = new("\uE77A"); // Unpin icon
|
||||
|
||||
public static IconInfo PinIcon { get; } = new("\uE840"); // Pin icon
|
||||
|
||||
public static IconInfo UninstallApplicationIcon { get; } = new("\uE74D"); // Uninstall icon
|
||||
|
||||
public static IconInfo GenericAppIcon { get; } = new("\uE737"); // Favicon
|
||||
}
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
// 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;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Threading;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Windows.Foundation;
|
||||
@@ -181,6 +182,15 @@ internal sealed partial class SampleListPage : ListPage
|
||||
{
|
||||
Title = "I also have properties",
|
||||
},
|
||||
new ListItem(new EverChangingCommand("Cat", "🐈⬛", "🐈"))
|
||||
{
|
||||
Title = "And I have a commands with changing name and icon",
|
||||
MoreCommands = [
|
||||
new CommandContextItem(new EverChangingCommand("Water", "🐬", "🐳", "🐟", "🦈")),
|
||||
new CommandContextItem(new EverChangingCommand("Faces", "😁", "🥺", "😍")),
|
||||
new CommandContextItem(new EverChangingCommand("Hearts", "♥️", "💚", "💜", "🧡", "💛", "💙")),
|
||||
],
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
@@ -229,4 +239,47 @@ internal sealed partial class SampleListPage : ListPage
|
||||
{ "hmm?", null },
|
||||
};
|
||||
}
|
||||
|
||||
internal sealed partial class EverChangingCommand : InvokableCommand, IDisposable
|
||||
{
|
||||
private readonly string[] _icons;
|
||||
private readonly Timer _timer;
|
||||
private readonly string _name;
|
||||
private int _currentIndex;
|
||||
|
||||
public EverChangingCommand(string name, params string[] icons)
|
||||
{
|
||||
_icons = icons ?? throw new ArgumentNullException(nameof(icons));
|
||||
if (_icons.Length == 0)
|
||||
{
|
||||
throw new ArgumentException("Icons array cannot be empty", nameof(icons));
|
||||
}
|
||||
|
||||
_name = name;
|
||||
Name = $"{_name} {DateTimeOffset.UtcNow:hh:mm:ss}";
|
||||
Icon = new IconInfo(_icons[_currentIndex]);
|
||||
|
||||
// Start timer to change icon and name every 5 seconds
|
||||
_timer = new Timer(OnTimerElapsed, null, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
private void OnTimerElapsed(object state)
|
||||
{
|
||||
var nextIndex = (_currentIndex + 1) % _icons.Length;
|
||||
if (nextIndex == _currentIndex && _icons.Length > 1)
|
||||
{
|
||||
nextIndex = (_currentIndex + 1) % _icons.Length;
|
||||
}
|
||||
|
||||
_currentIndex = nextIndex;
|
||||
|
||||
Name = $"{_name} {DateTimeOffset.UtcNow:hh:mm:ss}";
|
||||
Icon = new IconInfo(_icons[_currentIndex]);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_timer?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,16 +63,17 @@ public partial class CommandItem : BaseObservable, ICommandItem
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(Command));
|
||||
if (string.IsNullOrWhiteSpace(_title))
|
||||
if (string.IsNullOrEmpty(_title))
|
||||
{
|
||||
OnPropertyChanged(nameof(Title));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnCommandPropertyChanged(CommandItem instance, object source, IPropChangedEventArgs args)
|
||||
private void OnCommandPropertyChanged(CommandItem instance, object source, IPropChangedEventArgs args)
|
||||
{
|
||||
if (args.PropertyName == nameof(ICommand.Name))
|
||||
// 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));
|
||||
}
|
||||
@@ -98,13 +99,11 @@ public partial class CommandItem : BaseObservable, ICommandItem
|
||||
public CommandItem(ICommand command)
|
||||
{
|
||||
Command = command;
|
||||
Title = command.Name;
|
||||
}
|
||||
|
||||
public CommandItem(ICommandItem other)
|
||||
{
|
||||
Command = other.Command;
|
||||
Title = other.Title;
|
||||
Subtitle = other.Subtitle;
|
||||
Icon = (IconInfo?)other.Icon;
|
||||
MoreCommands = other.MoreCommands;
|
||||
|
||||
Reference in New Issue
Block a user