mirror of
https://github.com/microsoft/PowerToys
synced 2025-08-31 06:25:20 +00:00
Add support for filterable, nested context menus (#38776)
_targets #38573_ At first I just wanted to add support for nested context menus. But then I also had to add a search box, so the focus wouldn't get weird. End result:  This gets rid of the need to have the search box and the command bar both track item keybindings - now it's just in the command bar. Closes #38299 Closes #38442
This commit is contained in:
@@ -4,18 +4,14 @@
|
||||
|
||||
using System.Collections.ObjectModel;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Windows.System;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public partial class CommandBarViewModel : ObservableObject,
|
||||
IRecipient<UpdateCommandBarMessage>,
|
||||
IRecipient<UpdateItemKeybindingsMessage>
|
||||
IRecipient<UpdateCommandBarMessage>
|
||||
{
|
||||
public ICommandBarContext? SelectedItem
|
||||
{
|
||||
@@ -53,20 +49,17 @@ public partial class CommandBarViewModel : ObservableObject,
|
||||
public partial PageViewModel? CurrentPage { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ObservableCollection<CommandContextItemViewModel> ContextCommands { get; set; } = [];
|
||||
public partial ObservableCollection<ContextMenuStackViewModel> ContextMenuStack { get; set; } = [];
|
||||
|
||||
private Dictionary<KeyChord, CommandContextItemViewModel>? _contextKeybindings;
|
||||
public ContextMenuStackViewModel? ContextMenu => ContextMenuStack.LastOrDefault();
|
||||
|
||||
public CommandBarViewModel()
|
||||
{
|
||||
WeakReferenceMessenger.Default.Register<UpdateCommandBarMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<UpdateItemKeybindingsMessage>(this);
|
||||
}
|
||||
|
||||
public void Receive(UpdateCommandBarMessage message) => SelectedItem = message.ViewModel;
|
||||
|
||||
public void Receive(UpdateItemKeybindingsMessage message) => _contextKeybindings = message.Keys;
|
||||
|
||||
private void SetSelectedItem(ICommandBarContext? value)
|
||||
{
|
||||
if (value != null)
|
||||
@@ -111,7 +104,10 @@ public partial class CommandBarViewModel : ObservableObject,
|
||||
if (SelectedItem.MoreCommands.Count() > 1)
|
||||
{
|
||||
ShouldShowContextMenu = true;
|
||||
ContextCommands = [.. SelectedItem.AllCommands.Where(c => c.ShouldBeVisible)];
|
||||
|
||||
ContextMenuStack.Clear();
|
||||
ContextMenuStack.Add(new ContextMenuStackViewModel(SelectedItem));
|
||||
OnPropertyChanged(nameof(ContextMenu));
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -125,43 +121,80 @@ public partial class CommandBarViewModel : ObservableObject,
|
||||
|
||||
// InvokeItemCommand is what this will be in Xaml due to source generator
|
||||
// this comes in when an item in the list is tapped
|
||||
[RelayCommand]
|
||||
private void InvokeItem(CommandContextItemViewModel item) =>
|
||||
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(item.Command.Model, item.Model));
|
||||
// [RelayCommand]
|
||||
public ContextKeybindingResult InvokeItem(CommandContextItemViewModel item) =>
|
||||
PerformCommand(item);
|
||||
|
||||
// this comes in when the primary button is tapped
|
||||
public void InvokePrimaryCommand()
|
||||
{
|
||||
if (PrimaryCommand != null)
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(PrimaryCommand.Command.Model, PrimaryCommand.Model));
|
||||
}
|
||||
PerformCommand(SecondaryCommand);
|
||||
}
|
||||
|
||||
// this comes in when the secondary button is tapped
|
||||
public void InvokeSecondaryCommand()
|
||||
{
|
||||
if (SecondaryCommand != null)
|
||||
PerformCommand(SecondaryCommand);
|
||||
}
|
||||
|
||||
public ContextKeybindingResult CheckKeybinding(bool ctrl, bool alt, bool shift, bool win, VirtualKey key)
|
||||
{
|
||||
var matchedItem = ContextMenu?.CheckKeybinding(ctrl, alt, shift, win, key);
|
||||
return matchedItem != null ? PerformCommand(matchedItem) : ContextKeybindingResult.Unhandled;
|
||||
}
|
||||
|
||||
private ContextKeybindingResult PerformCommand(CommandItemViewModel? command)
|
||||
{
|
||||
if (command == null)
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(SecondaryCommand.Command.Model, SecondaryCommand.Model));
|
||||
return ContextKeybindingResult.Unhandled;
|
||||
}
|
||||
|
||||
if (command.HasMoreCommands)
|
||||
{
|
||||
ContextMenuStack.Add(new ContextMenuStackViewModel(command));
|
||||
OnPropertyChanging(nameof(ContextMenu));
|
||||
OnPropertyChanged(nameof(ContextMenu));
|
||||
return ContextKeybindingResult.KeepOpen;
|
||||
}
|
||||
else
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(command.Command.Model, command.Model));
|
||||
return ContextKeybindingResult.Hide;
|
||||
}
|
||||
}
|
||||
|
||||
public bool CheckKeybinding(bool ctrl, bool alt, bool shift, bool win, VirtualKey key)
|
||||
public bool CanPopContextStack()
|
||||
{
|
||||
if (_contextKeybindings != null)
|
||||
return ContextMenuStack.Count > 1;
|
||||
}
|
||||
|
||||
public void PopContextStack()
|
||||
{
|
||||
if (ContextMenuStack.Count > 1)
|
||||
{
|
||||
// Does the pressed key match any of the keybindings?
|
||||
var pressedKeyChord = KeyChordHelpers.FromModifiers(ctrl, alt, shift, win, key, 0);
|
||||
if (_contextKeybindings.TryGetValue(pressedKeyChord, out var item))
|
||||
{
|
||||
// TODO GH #245: This is a bit of a hack, but we need to make sure that the keybindings are updated before we send the message
|
||||
// so that the correct item is activated.
|
||||
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(item));
|
||||
return true;
|
||||
}
|
||||
ContextMenuStack.RemoveAt(ContextMenuStack.Count - 1);
|
||||
}
|
||||
|
||||
return false;
|
||||
OnPropertyChanging(nameof(ContextMenu));
|
||||
OnPropertyChanged(nameof(ContextMenu));
|
||||
}
|
||||
|
||||
public void ClearContextStack()
|
||||
{
|
||||
while (ContextMenuStack.Count > 1)
|
||||
{
|
||||
ContextMenuStack.RemoveAt(ContextMenuStack.Count - 1);
|
||||
}
|
||||
|
||||
OnPropertyChanging(nameof(ContextMenu));
|
||||
OnPropertyChanged(nameof(ContextMenu));
|
||||
}
|
||||
}
|
||||
|
||||
public enum ContextKeybindingResult
|
||||
{
|
||||
Unhandled,
|
||||
Hide,
|
||||
KeepOpen,
|
||||
}
|
||||
|
@@ -48,7 +48,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
|
||||
public List<CommandContextItemViewModel> MoreCommands { get; private set; } = [];
|
||||
|
||||
IEnumerable<CommandContextItemViewModel> ICommandBarContext.MoreCommands => MoreCommands;
|
||||
IEnumerable<CommandContextItemViewModel> IContextMenuContext.MoreCommands => MoreCommands;
|
||||
|
||||
public bool HasMoreCommands => MoreCommands.Count > 0;
|
||||
|
||||
@@ -187,23 +187,26 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
// use Initialize straight up
|
||||
MoreCommands.ForEach(contextItem =>
|
||||
{
|
||||
contextItem.InitializeProperties();
|
||||
contextItem.SlowInitializeProperties();
|
||||
});
|
||||
|
||||
_defaultCommandContextItem = new(new CommandContextItem(model.Command!), PageContext)
|
||||
if (!string.IsNullOrEmpty(model.Command.Name))
|
||||
{
|
||||
_itemTitle = Name,
|
||||
Subtitle = Subtitle,
|
||||
Command = Command,
|
||||
_defaultCommandContextItem = new(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
|
||||
};
|
||||
// TODO this probably should just be a CommandContextItemViewModel(CommandItemViewModel) ctor, or a copy ctor or whatever
|
||||
};
|
||||
|
||||
// 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;
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
Initialized |= InitializedState.SelectionInitialized;
|
||||
@@ -398,23 +401,6 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
base.SafeCleanup();
|
||||
Initialized |= InitializedState.CleanedUp;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a mapping of key -> command item for this particular item's
|
||||
/// MoreCommands. (This won't include the primary Command, but it will
|
||||
/// include the secondary one). This map can be used to quickly check if a
|
||||
/// shortcut key was pressed
|
||||
/// </summary>
|
||||
/// <returns>a dictionary of KeyChord -> Context commands, for all commands
|
||||
/// that have a shortcut key set.</returns>
|
||||
internal Dictionary<KeyChord, CommandContextItemViewModel> Keybindings()
|
||||
{
|
||||
return MoreCommands
|
||||
.Where(c => c.HasRequestedShortcut)
|
||||
.ToDictionary(
|
||||
c => c.RequestedShortcut ?? new KeyChord(0, 0, 0),
|
||||
c => c);
|
||||
}
|
||||
}
|
||||
|
||||
[Flags]
|
||||
|
@@ -0,0 +1,82 @@
|
||||
// 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.Collections.ObjectModel;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Windows.System;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public partial class ContextMenuStackViewModel : ObservableObject
|
||||
{
|
||||
[ObservableProperty]
|
||||
public partial ObservableCollection<CommandContextItemViewModel> FilteredItems { get; set; }
|
||||
|
||||
private readonly IContextMenuContext _context;
|
||||
private string _lastSearchText = string.Empty;
|
||||
|
||||
// private Dictionary<KeyChord, CommandContextItemViewModel>? _contextKeybindings;
|
||||
public ContextMenuStackViewModel(IContextMenuContext context)
|
||||
{
|
||||
_context = context;
|
||||
FilteredItems = [.. context.AllCommands];
|
||||
}
|
||||
|
||||
public void SetSearchText(string searchText)
|
||||
{
|
||||
if (searchText == _lastSearchText)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_lastSearchText = searchText;
|
||||
|
||||
var commands = _context.AllCommands.Where(c => c.ShouldBeVisible);
|
||||
if (string.IsNullOrEmpty(searchText))
|
||||
{
|
||||
ListHelpers.InPlaceUpdateList(FilteredItems, commands);
|
||||
return;
|
||||
}
|
||||
|
||||
var newResults = ListHelpers.FilterList<CommandContextItemViewModel>(commands, searchText, ScoreContextCommand);
|
||||
ListHelpers.InPlaceUpdateList(FilteredItems, newResults);
|
||||
}
|
||||
|
||||
private static int ScoreContextCommand(string query, CommandContextItemViewModel item)
|
||||
{
|
||||
if (string.IsNullOrEmpty(query) || string.IsNullOrWhiteSpace(query))
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(item.Title))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var nameMatch = StringMatcher.FuzzySearch(query, item.Title);
|
||||
|
||||
var descriptionMatch = StringMatcher.FuzzySearch(query, item.Subtitle);
|
||||
|
||||
return new[] { nameMatch.Score, (descriptionMatch.Score - 4) / 2, 0 }.Max();
|
||||
}
|
||||
|
||||
public CommandContextItemViewModel? CheckKeybinding(bool ctrl, bool alt, bool shift, bool win, VirtualKey key)
|
||||
{
|
||||
var keybindings = _context.Keybindings();
|
||||
if (keybindings != null)
|
||||
{
|
||||
// Does the pressed key match any of the keybindings?
|
||||
var pressedKeyChord = KeyChordHelpers.FromModifiers(ctrl, alt, shift, win, key, 0);
|
||||
if (keybindings.TryGetValue(pressedKeyChord, out var item))
|
||||
{
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
@@ -344,8 +344,6 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<UpdateCommandBarMessage>(new(item));
|
||||
|
||||
WeakReferenceMessenger.Default.Send<UpdateItemKeybindingsMessage>(new(item.Keybindings()));
|
||||
|
||||
if (ShowDetails && item.HasDetails)
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<ShowDetailsMessage>(new(item.Details));
|
||||
|
@@ -2,8 +2,11 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Windows.System;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
|
||||
public record UpdateItemKeybindingsMessage(Dictionary<KeyChord, CommandContextItemViewModel>? Keys);
|
||||
public record TryCommandKeybindingMessage(bool Ctrl, bool Alt, bool Shift, bool Win, VirtualKey Key)
|
||||
{
|
||||
public bool Handled { get; set; }
|
||||
}
|
@@ -3,6 +3,7 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.ComponentModel;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
|
||||
@@ -13,22 +14,42 @@ public record UpdateCommandBarMessage(ICommandBarContext? ViewModel)
|
||||
{
|
||||
}
|
||||
|
||||
// Represents everything the command bar needs to know about to show command
|
||||
// buttons at the bottom.
|
||||
//
|
||||
// This is implemented by both ListItemViewModel and ContentPageViewModel,
|
||||
// the two things with sub-commands.
|
||||
public interface ICommandBarContext : INotifyPropertyChanged
|
||||
public interface IContextMenuContext : INotifyPropertyChanged
|
||||
{
|
||||
public IEnumerable<CommandContextItemViewModel> MoreCommands { get; }
|
||||
|
||||
public bool HasMoreCommands { get; }
|
||||
|
||||
public List<CommandContextItemViewModel> AllCommands { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Generates a mapping of key -> command item for this particular item's
|
||||
/// MoreCommands. (This won't include the primary Command, but it will
|
||||
/// include the secondary one). This map can be used to quickly check if a
|
||||
/// shortcut key was pressed
|
||||
/// </summary>
|
||||
/// <returns>a dictionary of KeyChord -> Context commands, for all commands
|
||||
/// that have a shortcut key set.</returns>
|
||||
public Dictionary<KeyChord, CommandContextItemViewModel> Keybindings()
|
||||
{
|
||||
return MoreCommands
|
||||
.Where(c => c.HasRequestedShortcut)
|
||||
.ToDictionary(
|
||||
c => c.RequestedShortcut ?? new KeyChord(0, 0, 0),
|
||||
c => c);
|
||||
}
|
||||
}
|
||||
|
||||
// Represents everything the command bar needs to know about to show command
|
||||
// buttons at the bottom.
|
||||
//
|
||||
// This is implemented by both ListItemViewModel and ContentPageViewModel,
|
||||
// the two things with sub-commands.
|
||||
public interface ICommandBarContext : IContextMenuContext
|
||||
{
|
||||
public string SecondaryCommandName { get; }
|
||||
|
||||
public CommandItemViewModel? PrimaryCommand { get; }
|
||||
|
||||
public CommandItemViewModel? SecondaryCommand { get; }
|
||||
|
||||
public List<CommandContextItemViewModel> AllCommands { get; }
|
||||
}
|
||||
|
@@ -225,27 +225,42 @@
|
||||
ToolTipService.ToolTip="Ctrl+K"
|
||||
Visibility="{x:Bind ViewModel.ShouldShowContextMenu, Mode=OneWay}">
|
||||
<Button.Flyout>
|
||||
<Flyout Placement="TopEdgeAlignedRight">
|
||||
<ListView
|
||||
x:Name="CommandsDropdown"
|
||||
MinWidth="248"
|
||||
Margin="-16,-12,-16,-12"
|
||||
IsItemClickEnabled="True"
|
||||
ItemClick="CommandsDropdown_ItemClick"
|
||||
ItemTemplate="{StaticResource ContextMenuViewModelTemplate}"
|
||||
ItemsSource="{x:Bind ViewModel.ContextCommands, Mode=OneWay}"
|
||||
KeyDown="CommandsDropdown_KeyDown"
|
||||
SelectionMode="None">
|
||||
<ListView.ItemContainerStyle>
|
||||
<Style BasedOn="{StaticResource DefaultListViewItemStyle}" TargetType="ListViewItem">
|
||||
<Setter Property="MinHeight" Value="0" />
|
||||
<Setter Property="Padding" Value="12,7,12,7" />
|
||||
</Style>
|
||||
</ListView.ItemContainerStyle>
|
||||
<ListView.ItemContainerTransitions>
|
||||
<TransitionCollection />
|
||||
</ListView.ItemContainerTransitions>
|
||||
</ListView>
|
||||
<Flyout
|
||||
Closing="Flyout_Closing"
|
||||
Opened="Flyout_Opened"
|
||||
Placement="TopEdgeAlignedRight">
|
||||
<StackPanel>
|
||||
|
||||
<ListView
|
||||
x:Name="CommandsDropdown"
|
||||
MinWidth="248"
|
||||
Margin="-16,-12,-16,-12"
|
||||
IsItemClickEnabled="True"
|
||||
ItemClick="CommandsDropdown_ItemClick"
|
||||
ItemTemplate="{StaticResource ContextMenuViewModelTemplate}"
|
||||
ItemsSource="{x:Bind ViewModel.ContextMenu.FilteredItems, Mode=OneWay}"
|
||||
KeyDown="CommandsDropdown_KeyDown"
|
||||
SelectionMode="Single">
|
||||
<ListView.ItemContainerStyle>
|
||||
<Style BasedOn="{StaticResource DefaultListViewItemStyle}" TargetType="ListViewItem">
|
||||
<Setter Property="MinHeight" Value="0" />
|
||||
<Setter Property="Padding" Value="12,7,12,7" />
|
||||
</Style>
|
||||
</ListView.ItemContainerStyle>
|
||||
<ListView.ItemContainerTransitions>
|
||||
<TransitionCollection />
|
||||
</ListView.ItemContainerTransitions>
|
||||
</ListView>
|
||||
|
||||
<TextBox
|
||||
x:Name="ContextFilterBox"
|
||||
x:Uid="ContextFilterBox"
|
||||
Margin="-12,12,-12,-12"
|
||||
KeyDown="ContextFilterBox_KeyDown"
|
||||
PreviewKeyDown="ContextFilterBox_PreviewKeyDown"
|
||||
TextChanged="ContextFilterBox_TextChanged" />
|
||||
|
||||
</StackPanel>
|
||||
</Flyout>
|
||||
</Button.Flyout>
|
||||
</Button>
|
||||
|
@@ -18,9 +18,10 @@ namespace Microsoft.CmdPal.UI.Controls;
|
||||
|
||||
public sealed partial class CommandBar : UserControl,
|
||||
IRecipient<OpenContextMenuMessage>,
|
||||
IRecipient<TryCommandKeybindingMessage>,
|
||||
ICurrentPageAware
|
||||
{
|
||||
public CommandBarViewModel ViewModel { get; set; } = new();
|
||||
public CommandBarViewModel ViewModel { get; } = new();
|
||||
|
||||
public PageViewModel? CurrentPageViewModel
|
||||
{
|
||||
@@ -38,6 +39,9 @@ public sealed partial class CommandBar : UserControl,
|
||||
|
||||
// RegisterAll isn't AOT compatible
|
||||
WeakReferenceMessenger.Default.Register<OpenContextMenuMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<TryCommandKeybindingMessage>(this);
|
||||
|
||||
ViewModel.PropertyChanged += ViewModel_PropertyChanged;
|
||||
}
|
||||
|
||||
public void Receive(OpenContextMenuMessage message)
|
||||
@@ -52,8 +56,41 @@ public sealed partial class CommandBar : UserControl,
|
||||
ShowMode = FlyoutShowMode.Standard,
|
||||
};
|
||||
MoreCommandsButton.Flyout.ShowAt(MoreCommandsButton, options);
|
||||
CommandsDropdown.SelectedIndex = 0;
|
||||
CommandsDropdown.Focus(FocusState.Programmatic);
|
||||
UpdateUiForStackChange();
|
||||
}
|
||||
|
||||
public void Receive(TryCommandKeybindingMessage msg)
|
||||
{
|
||||
if (!ViewModel.ShouldShowContextMenu)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var result = ViewModel?.CheckKeybinding(msg.Ctrl, msg.Alt, msg.Shift, msg.Win, msg.Key);
|
||||
|
||||
if (result == ContextKeybindingResult.Hide)
|
||||
{
|
||||
msg.Handled = true;
|
||||
}
|
||||
else if (result == ContextKeybindingResult.KeepOpen)
|
||||
{
|
||||
if (!MoreCommandsButton.Flyout.IsOpen)
|
||||
{
|
||||
var options = new FlyoutShowOptions
|
||||
{
|
||||
ShowMode = FlyoutShowMode.Standard,
|
||||
};
|
||||
MoreCommandsButton.Flyout.ShowAt(MoreCommandsButton, options);
|
||||
}
|
||||
|
||||
UpdateUiForStackChange();
|
||||
|
||||
msg.Handled = true;
|
||||
}
|
||||
else if (result == ContextKeybindingResult.Unhandled)
|
||||
{
|
||||
msg.Handled = false;
|
||||
}
|
||||
}
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "VS has a tendency to delete XAML bound methods over-aggressively")]
|
||||
@@ -88,8 +125,14 @@ public sealed partial class CommandBar : UserControl,
|
||||
{
|
||||
if (e.ClickedItem is CommandContextItemViewModel item)
|
||||
{
|
||||
ViewModel?.InvokeItemCommand.Execute(item);
|
||||
MoreCommandsButton.Flyout.Hide();
|
||||
if (ViewModel?.InvokeItem(item) == ContextKeybindingResult.Hide)
|
||||
{
|
||||
MoreCommandsButton.Flyout.Hide();
|
||||
}
|
||||
else
|
||||
{
|
||||
UpdateUiForStackChange();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,9 +149,136 @@ public sealed partial class CommandBar : UserControl,
|
||||
var winPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.LeftWindows).HasFlag(CoreVirtualKeyStates.Down) ||
|
||||
InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.RightWindows).HasFlag(CoreVirtualKeyStates.Down);
|
||||
|
||||
if (ViewModel?.CheckKeybinding(ctrlPressed, altPressed, shiftPressed, winPressed, e.Key) ?? false)
|
||||
var result = ViewModel?.CheckKeybinding(ctrlPressed, altPressed, shiftPressed, winPressed, e.Key);
|
||||
|
||||
if (result == ContextKeybindingResult.Hide)
|
||||
{
|
||||
e.Handled = true;
|
||||
MoreCommandsButton.Flyout.Hide();
|
||||
WeakReferenceMessenger.Default.Send<FocusSearchBoxMessage>();
|
||||
}
|
||||
else if (result == ContextKeybindingResult.KeepOpen)
|
||||
{
|
||||
e.Handled = true;
|
||||
}
|
||||
else if (result == ContextKeybindingResult.Unhandled)
|
||||
{
|
||||
e.Handled = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void Flyout_Opened(object sender, object e)
|
||||
{
|
||||
UpdateUiForStackChange();
|
||||
}
|
||||
|
||||
private void Flyout_Closing(FlyoutBase sender, FlyoutBaseClosingEventArgs args)
|
||||
{
|
||||
ViewModel?.ClearContextStack();
|
||||
WeakReferenceMessenger.Default.Send<FocusSearchBoxMessage>();
|
||||
}
|
||||
|
||||
private void ViewModel_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
{
|
||||
var prop = e.PropertyName;
|
||||
if (prop == nameof(ViewModel.ContextMenu))
|
||||
{
|
||||
UpdateUiForStackChange();
|
||||
}
|
||||
}
|
||||
|
||||
private void ContextFilterBox_TextChanged(object sender, TextChangedEventArgs e)
|
||||
{
|
||||
ViewModel.ContextMenu?.SetSearchText(ContextFilterBox.Text);
|
||||
|
||||
if (CommandsDropdown.SelectedIndex == -1)
|
||||
{
|
||||
CommandsDropdown.SelectedIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private void ContextFilterBox_KeyDown(object sender, KeyRoutedEventArgs e)
|
||||
{
|
||||
var ctrlPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Control).HasFlag(CoreVirtualKeyStates.Down);
|
||||
var altPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Menu).HasFlag(CoreVirtualKeyStates.Down);
|
||||
var shiftPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Shift).HasFlag(CoreVirtualKeyStates.Down);
|
||||
var winPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.LeftWindows).HasFlag(CoreVirtualKeyStates.Down) ||
|
||||
InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.RightWindows).HasFlag(CoreVirtualKeyStates.Down);
|
||||
|
||||
if (e.Key == VirtualKey.Enter)
|
||||
{
|
||||
if (CommandsDropdown.SelectedItem is CommandContextItemViewModel item)
|
||||
{
|
||||
if (ViewModel?.InvokeItem(item) == ContextKeybindingResult.Hide)
|
||||
{
|
||||
MoreCommandsButton.Flyout.Hide();
|
||||
WeakReferenceMessenger.Default.Send<FocusSearchBoxMessage>();
|
||||
}
|
||||
else
|
||||
{
|
||||
UpdateUiForStackChange();
|
||||
}
|
||||
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
else if (e.Key == VirtualKey.Escape ||
|
||||
(e.Key == VirtualKey.Left && altPressed))
|
||||
{
|
||||
if (ViewModel.CanPopContextStack())
|
||||
{
|
||||
ViewModel.PopContextStack();
|
||||
UpdateUiForStackChange();
|
||||
}
|
||||
else
|
||||
{
|
||||
MoreCommandsButton.Flyout.Hide();
|
||||
WeakReferenceMessenger.Default.Send<FocusSearchBoxMessage>();
|
||||
}
|
||||
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
CommandsDropdown_KeyDown(sender, e);
|
||||
}
|
||||
|
||||
private void ContextFilterBox_PreviewKeyDown(object sender, KeyRoutedEventArgs e)
|
||||
{
|
||||
if (e.Key == VirtualKey.Up)
|
||||
{
|
||||
// navigate previous
|
||||
if (CommandsDropdown.SelectedIndex > 0)
|
||||
{
|
||||
CommandsDropdown.SelectedIndex--;
|
||||
}
|
||||
else
|
||||
{
|
||||
CommandsDropdown.SelectedIndex = CommandsDropdown.Items.Count - 1;
|
||||
}
|
||||
|
||||
e.Handled = true;
|
||||
}
|
||||
else if (e.Key == VirtualKey.Down)
|
||||
{
|
||||
// navigate next
|
||||
if (CommandsDropdown.SelectedIndex < CommandsDropdown.Items.Count - 1)
|
||||
{
|
||||
CommandsDropdown.SelectedIndex++;
|
||||
}
|
||||
else
|
||||
{
|
||||
CommandsDropdown.SelectedIndex = 0;
|
||||
}
|
||||
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateUiForStackChange()
|
||||
{
|
||||
ContextFilterBox.Text = string.Empty;
|
||||
ViewModel.ContextMenu?.SetSearchText(string.Empty);
|
||||
CommandsDropdown.SelectedIndex = 0;
|
||||
ContextFilterBox.Focus(FocusState.Programmatic);
|
||||
}
|
||||
}
|
||||
|
@@ -8,8 +8,6 @@ using CommunityToolkit.WinUI;
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.Views;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Input;
|
||||
using Microsoft.UI.Xaml;
|
||||
@@ -23,7 +21,6 @@ namespace Microsoft.CmdPal.UI.Controls;
|
||||
public sealed partial class SearchBar : UserControl,
|
||||
IRecipient<GoHomeMessage>,
|
||||
IRecipient<FocusSearchBoxMessage>,
|
||||
IRecipient<UpdateItemKeybindingsMessage>,
|
||||
ICurrentPageAware
|
||||
{
|
||||
private readonly DispatcherQueue _queue = DispatcherQueue.GetForCurrentThread();
|
||||
@@ -34,8 +31,6 @@ public sealed partial class SearchBar : UserControl,
|
||||
private readonly DispatcherQueueTimer _debounceTimer = DispatcherQueue.GetForCurrentThread().CreateTimer();
|
||||
private bool _isBackspaceHeld;
|
||||
|
||||
private Dictionary<KeyChord, CommandContextItemViewModel>? _keyBindings;
|
||||
|
||||
public PageViewModel? CurrentPageViewModel
|
||||
{
|
||||
get => (PageViewModel?)GetValue(CurrentPageViewModelProperty);
|
||||
@@ -74,7 +69,6 @@ public sealed partial class SearchBar : UserControl,
|
||||
this.InitializeComponent();
|
||||
WeakReferenceMessenger.Default.Register<GoHomeMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<FocusSearchBoxMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<UpdateItemKeybindingsMessage>(this);
|
||||
}
|
||||
|
||||
public void ClearSearch()
|
||||
@@ -173,17 +167,14 @@ public sealed partial class SearchBar : UserControl,
|
||||
WeakReferenceMessenger.Default.Send<NavigateBackMessage>(new());
|
||||
}
|
||||
|
||||
if (_keyBindings != null)
|
||||
if (!e.Handled)
|
||||
{
|
||||
// Does the pressed key match any of the keybindings?
|
||||
var pressedKeyChord = KeyChordHelpers.FromModifiers(ctrlPressed, altPressed, shiftPressed, winPressed, (int)e.Key, 0);
|
||||
if (_keyBindings.TryGetValue(pressedKeyChord, out var item))
|
||||
{
|
||||
// TODO GH #245: This is a bit of a hack, but we need to make sure that the keybindings are updated before we send the message
|
||||
// so that the correct item is activated.
|
||||
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(item));
|
||||
e.Handled = true;
|
||||
}
|
||||
// The CommandBar is responsible for handling all the item keybindings,
|
||||
// since the bound context item may need to then show another
|
||||
// context menu
|
||||
TryCommandKeybindingMessage msg = new(ctrlPressed, altPressed, shiftPressed, winPressed, e.Key);
|
||||
WeakReferenceMessenger.Default.Send(msg);
|
||||
e.Handled = msg.Handled;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -302,10 +293,5 @@ public sealed partial class SearchBar : UserControl,
|
||||
|
||||
public void Receive(GoHomeMessage message) => ClearSearch();
|
||||
|
||||
public void Receive(FocusSearchBoxMessage message) => this.Focus(Microsoft.UI.Xaml.FocusState.Programmatic);
|
||||
|
||||
public void Receive(UpdateItemKeybindingsMessage message)
|
||||
{
|
||||
_keyBindings = message.Keys;
|
||||
}
|
||||
public void Receive(FocusSearchBoxMessage message) => FilterBox.Focus(Microsoft.UI.Xaml.FocusState.Programmatic);
|
||||
}
|
||||
|
@@ -187,8 +187,6 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
|
||||
WeakReferenceMessenger.Default.Send<UpdateCommandBarMessage>(new(null));
|
||||
|
||||
WeakReferenceMessenger.Default.Send<UpdateItemKeybindingsMessage>(new(null));
|
||||
|
||||
var isMainPage = command is MainListPage;
|
||||
|
||||
// Construct our ViewModel of the appropriate type and pass it the UI Thread context.
|
||||
|
@@ -394,6 +394,9 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
||||
<data name="BehaviorSettingsHeader.Text" xml:space="preserve">
|
||||
<value>Behavior</value>
|
||||
</data>
|
||||
<data name="ContextFilterBox.PlaceholderText" xml:space="preserve">
|
||||
<value>Search commands...</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_ShowSystemTrayIcon_SettingsCard.Header" xml:space="preserve">
|
||||
<value>Show system tray icon</value>
|
||||
</data>
|
||||
|
@@ -6,6 +6,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Windows.System;
|
||||
|
||||
namespace SamplePagesExtension;
|
||||
|
||||
@@ -76,7 +77,136 @@ public partial class EvilSamplesPage : ListPage
|
||||
{
|
||||
Body = "This is a test for GH#512. If it doesn't appear immediately, it's likely InvokeCommand is happening on the UI thread.",
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
// More edge cases than truly evil
|
||||
new ListItem(
|
||||
new ToastCommand("Primary command invoked", MessageState.Info) { Name = "Primary command", Icon = new IconInfo("\uF146") }) // dial 1
|
||||
{
|
||||
Title = "anonymous command test",
|
||||
Subtitle = "Try pressing Ctrl+1 with me selected",
|
||||
Icon = new IconInfo("\uE712"), // "More" dots
|
||||
MoreCommands = [
|
||||
new CommandContextItem(
|
||||
new ToastCommand("Secondary command invoked", MessageState.Warning) { Name = "Secondary command", Icon = new IconInfo("\uF147") }) // dial 2
|
||||
{
|
||||
Title = "I'm a second command",
|
||||
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.Number1),
|
||||
},
|
||||
new CommandContextItem("nested...")
|
||||
{
|
||||
Title = "We can go deeper...",
|
||||
Icon = new IconInfo("\uF148"),
|
||||
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.Number2),
|
||||
MoreCommands = [
|
||||
new CommandContextItem(
|
||||
new ToastCommand("Nested A invoked") { Name = "Do it", Icon = new IconInfo("A") })
|
||||
{
|
||||
Title = "Nested A",
|
||||
RequestedShortcut = KeyChordHelpers.FromModifiers(alt: true, vkey: VirtualKey.A),
|
||||
},
|
||||
|
||||
new CommandContextItem(
|
||||
new ToastCommand("Nested B invoked") { Name = "Do it", Icon = new IconInfo("B") })
|
||||
{
|
||||
Title = "Nested B...",
|
||||
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.B),
|
||||
MoreCommands = [
|
||||
new CommandContextItem(
|
||||
new ToastCommand("Nested C invoked") { Name = "Do it" })
|
||||
{
|
||||
Title = "You get it",
|
||||
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.B),
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
},
|
||||
new ListItem(
|
||||
new ToastCommand("Primary command invoked", MessageState.Info) { Name = "Primary command", Icon = new IconInfo("\uF146") }) // dial 1
|
||||
{
|
||||
Title = "noop command test",
|
||||
Subtitle = "Try pressing Ctrl+1 with me selected",
|
||||
Icon = new IconInfo("\uE712"), // "More" dots
|
||||
MoreCommands = [
|
||||
new CommandContextItem(
|
||||
new ToastCommand("Secondary command invoked", MessageState.Warning) { Name = "Secondary command", Icon = new IconInfo("\uF147") }) // dial 2
|
||||
{
|
||||
Title = "I'm a second command",
|
||||
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.Number1),
|
||||
},
|
||||
new CommandContextItem(new NoOpCommand())
|
||||
{
|
||||
Title = "We can go deeper...",
|
||||
Icon = new IconInfo("\uF148"),
|
||||
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.Number2),
|
||||
MoreCommands = [
|
||||
new CommandContextItem(
|
||||
new ToastCommand("Nested A invoked") { Name = "Do it", Icon = new IconInfo("A") })
|
||||
{
|
||||
Title = "Nested A",
|
||||
RequestedShortcut = KeyChordHelpers.FromModifiers(alt: true, vkey: VirtualKey.A),
|
||||
},
|
||||
|
||||
new CommandContextItem(
|
||||
new ToastCommand("Nested B invoked") { Name = "Do it", Icon = new IconInfo("B") })
|
||||
{
|
||||
Title = "Nested B...",
|
||||
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.B),
|
||||
MoreCommands = [
|
||||
new CommandContextItem(
|
||||
new ToastCommand("Nested C invoked") { Name = "Do it" })
|
||||
{
|
||||
Title = "You get it",
|
||||
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.B),
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
},
|
||||
new ListItem(
|
||||
new ToastCommand("Primary command invoked", MessageState.Info) { Name = "Primary command", Icon = new IconInfo("\uF146") }) // dial 1
|
||||
{
|
||||
Title = "noop secondary command test",
|
||||
Subtitle = "Try pressing Ctrl+1 with me selected",
|
||||
Icon = new IconInfo("\uE712"), // "More" dots
|
||||
MoreCommands = [
|
||||
new CommandContextItem(new NoOpCommand())
|
||||
{
|
||||
Title = "We can go deeper...",
|
||||
Icon = new IconInfo("\uF148"),
|
||||
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.Number2),
|
||||
MoreCommands = [
|
||||
new CommandContextItem(
|
||||
new ToastCommand("Nested A invoked") { Name = "Do it", Icon = new IconInfo("A") })
|
||||
{
|
||||
Title = "Nested A",
|
||||
RequestedShortcut = KeyChordHelpers.FromModifiers(alt: true, vkey: VirtualKey.A),
|
||||
},
|
||||
|
||||
new CommandContextItem(
|
||||
new ToastCommand("Nested B invoked") { Name = "Do it", Icon = new IconInfo("B") })
|
||||
{
|
||||
Title = "Nested B...",
|
||||
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.B),
|
||||
MoreCommands = [
|
||||
new CommandContextItem(
|
||||
new ToastCommand("Nested C invoked") { Name = "Do it" })
|
||||
{
|
||||
Title = "You get it",
|
||||
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.B),
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
},
|
||||
|
||||
];
|
||||
|
||||
public EvilSamplesPage()
|
||||
|
@@ -69,62 +69,47 @@ internal sealed partial class SampleListPage : ListPage
|
||||
},
|
||||
|
||||
new ListItem(
|
||||
new AnonymousCommand(() =>
|
||||
{
|
||||
var t = new ToastStatusMessage(new StatusMessage()
|
||||
{
|
||||
Message = "Primary command invoked",
|
||||
State = MessageState.Info,
|
||||
});
|
||||
t.Show();
|
||||
})
|
||||
{
|
||||
Result = CommandResult.KeepOpen(),
|
||||
Icon = new IconInfo("\uE712"),
|
||||
})
|
||||
new ToastCommand("Primary command invoked", MessageState.Info) { Name = "Primary command", Icon = new IconInfo("\uF146") }) // dial 1
|
||||
{
|
||||
Title = "You can add context menu items too. Press Ctrl+k",
|
||||
Subtitle = "Try pressing Ctrl+1 with me selected",
|
||||
Icon = new IconInfo("\uE712"),
|
||||
Icon = new IconInfo("\uE712"), // "More" dots
|
||||
MoreCommands = [
|
||||
new CommandContextItem(
|
||||
new AnonymousCommand(() =>
|
||||
{
|
||||
var t = new ToastStatusMessage(new StatusMessage()
|
||||
{
|
||||
Message = "Secondary command invoked",
|
||||
State = MessageState.Warning,
|
||||
});
|
||||
t.Show();
|
||||
})
|
||||
{
|
||||
Name = "Secondary command",
|
||||
Icon = new IconInfo("\uF147"), // Dial 2
|
||||
Result = CommandResult.KeepOpen(),
|
||||
})
|
||||
new ToastCommand("Secondary command invoked", MessageState.Warning) { Name = "Secondary command", Icon = new IconInfo("\uF147") }) // dial 2
|
||||
{
|
||||
Title = "I'm a second command",
|
||||
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.Number1),
|
||||
},
|
||||
new CommandContextItem(
|
||||
new AnonymousCommand(() =>
|
||||
{
|
||||
var t = new ToastStatusMessage(new StatusMessage()
|
||||
{
|
||||
Message = "Third command invoked",
|
||||
State = MessageState.Error,
|
||||
});
|
||||
t.Show();
|
||||
})
|
||||
{
|
||||
Name = "Do it",
|
||||
Icon = new IconInfo("\uF148"), // dial 3
|
||||
Result = CommandResult.KeepOpen(),
|
||||
})
|
||||
new ToastCommand("Third command invoked", MessageState.Error) { Name = "Do 3", Icon = new IconInfo("\uF148") }) // dial 3
|
||||
{
|
||||
Title = "A third command too",
|
||||
Title = "We can go deeper...",
|
||||
Icon = new IconInfo("\uF148"),
|
||||
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.Number2),
|
||||
MoreCommands = [
|
||||
new CommandContextItem(
|
||||
new ToastCommand("Nested A invoked") { Name = "Do it", Icon = new IconInfo("A") })
|
||||
{
|
||||
Title = "Nested A",
|
||||
RequestedShortcut = KeyChordHelpers.FromModifiers(alt: true, vkey: VirtualKey.A),
|
||||
},
|
||||
|
||||
new CommandContextItem(
|
||||
new ToastCommand("Nested B invoked") { Name = "Do it", Icon = new IconInfo("B") })
|
||||
{
|
||||
Title = "Nested B...",
|
||||
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.B),
|
||||
MoreCommands = [
|
||||
new CommandContextItem(
|
||||
new ToastCommand("Nested C invoked") { Name = "Do it" })
|
||||
{
|
||||
Title = "You get it",
|
||||
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.B),
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
},
|
||||
@@ -183,7 +168,6 @@ internal sealed partial class SampleListPage : ListPage
|
||||
{
|
||||
Title = "Get the name of the Foreground window",
|
||||
},
|
||||
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,23 @@
|
||||
// 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 Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace SamplePagesExtension;
|
||||
|
||||
internal sealed partial class ToastCommand(string message, MessageState state = MessageState.Info) : InvokableCommand
|
||||
{
|
||||
public override ICommandResult Invoke()
|
||||
{
|
||||
var t = new ToastStatusMessage(new StatusMessage()
|
||||
{
|
||||
Message = message,
|
||||
State = state,
|
||||
});
|
||||
t.Show();
|
||||
|
||||
return CommandResult.KeepOpen();
|
||||
}
|
||||
}
|
@@ -61,4 +61,9 @@ public partial class ListItem : CommandItem, IListItem
|
||||
: base(command)
|
||||
{
|
||||
}
|
||||
|
||||
public ListItem()
|
||||
: base()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user