mirror of
https://github.com/microsoft/PowerToys
synced 2025-08-31 22:45:32 +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 System.Collections.ObjectModel;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.Input;
|
|
||||||
using CommunityToolkit.Mvvm.Messaging;
|
using CommunityToolkit.Mvvm.Messaging;
|
||||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||||
using Microsoft.CommandPalette.Extensions;
|
|
||||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
|
||||||
using Windows.System;
|
using Windows.System;
|
||||||
|
|
||||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||||
|
|
||||||
public partial class CommandBarViewModel : ObservableObject,
|
public partial class CommandBarViewModel : ObservableObject,
|
||||||
IRecipient<UpdateCommandBarMessage>,
|
IRecipient<UpdateCommandBarMessage>
|
||||||
IRecipient<UpdateItemKeybindingsMessage>
|
|
||||||
{
|
{
|
||||||
public ICommandBarContext? SelectedItem
|
public ICommandBarContext? SelectedItem
|
||||||
{
|
{
|
||||||
@@ -53,20 +49,17 @@ public partial class CommandBarViewModel : ObservableObject,
|
|||||||
public partial PageViewModel? CurrentPage { get; set; }
|
public partial PageViewModel? CurrentPage { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[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()
|
public CommandBarViewModel()
|
||||||
{
|
{
|
||||||
WeakReferenceMessenger.Default.Register<UpdateCommandBarMessage>(this);
|
WeakReferenceMessenger.Default.Register<UpdateCommandBarMessage>(this);
|
||||||
WeakReferenceMessenger.Default.Register<UpdateItemKeybindingsMessage>(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Receive(UpdateCommandBarMessage message) => SelectedItem = message.ViewModel;
|
public void Receive(UpdateCommandBarMessage message) => SelectedItem = message.ViewModel;
|
||||||
|
|
||||||
public void Receive(UpdateItemKeybindingsMessage message) => _contextKeybindings = message.Keys;
|
|
||||||
|
|
||||||
private void SetSelectedItem(ICommandBarContext? value)
|
private void SetSelectedItem(ICommandBarContext? value)
|
||||||
{
|
{
|
||||||
if (value != null)
|
if (value != null)
|
||||||
@@ -111,7 +104,10 @@ public partial class CommandBarViewModel : ObservableObject,
|
|||||||
if (SelectedItem.MoreCommands.Count() > 1)
|
if (SelectedItem.MoreCommands.Count() > 1)
|
||||||
{
|
{
|
||||||
ShouldShowContextMenu = true;
|
ShouldShowContextMenu = true;
|
||||||
ContextCommands = [.. SelectedItem.AllCommands.Where(c => c.ShouldBeVisible)];
|
|
||||||
|
ContextMenuStack.Clear();
|
||||||
|
ContextMenuStack.Add(new ContextMenuStackViewModel(SelectedItem));
|
||||||
|
OnPropertyChanged(nameof(ContextMenu));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -125,43 +121,80 @@ public partial class CommandBarViewModel : ObservableObject,
|
|||||||
|
|
||||||
// InvokeItemCommand is what this will be in Xaml due to source generator
|
// InvokeItemCommand is what this will be in Xaml due to source generator
|
||||||
// this comes in when an item in the list is tapped
|
// this comes in when an item in the list is tapped
|
||||||
[RelayCommand]
|
// [RelayCommand]
|
||||||
private void InvokeItem(CommandContextItemViewModel item) =>
|
public ContextKeybindingResult InvokeItem(CommandContextItemViewModel item) =>
|
||||||
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(item.Command.Model, item.Model));
|
PerformCommand(item);
|
||||||
|
|
||||||
// this comes in when the primary button is tapped
|
// this comes in when the primary button is tapped
|
||||||
public void InvokePrimaryCommand()
|
public void InvokePrimaryCommand()
|
||||||
{
|
{
|
||||||
if (PrimaryCommand != null)
|
PerformCommand(SecondaryCommand);
|
||||||
{
|
|
||||||
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(PrimaryCommand.Command.Model, PrimaryCommand.Model));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// this comes in when the secondary button is tapped
|
// this comes in when the secondary button is tapped
|
||||||
public void InvokeSecondaryCommand()
|
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?
|
ContextMenuStack.RemoveAt(ContextMenuStack.Count - 1);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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; } = [];
|
public List<CommandContextItemViewModel> MoreCommands { get; private set; } = [];
|
||||||
|
|
||||||
IEnumerable<CommandContextItemViewModel> ICommandBarContext.MoreCommands => MoreCommands;
|
IEnumerable<CommandContextItemViewModel> IContextMenuContext.MoreCommands => MoreCommands;
|
||||||
|
|
||||||
public bool HasMoreCommands => MoreCommands.Count > 0;
|
public bool HasMoreCommands => MoreCommands.Count > 0;
|
||||||
|
|
||||||
@@ -187,23 +187,26 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
|||||||
// use Initialize straight up
|
// use Initialize straight up
|
||||||
MoreCommands.ForEach(contextItem =>
|
MoreCommands.ForEach(contextItem =>
|
||||||
{
|
{
|
||||||
contextItem.InitializeProperties();
|
contextItem.SlowInitializeProperties();
|
||||||
});
|
});
|
||||||
|
|
||||||
_defaultCommandContextItem = new(new CommandContextItem(model.Command!), PageContext)
|
if (!string.IsNullOrEmpty(model.Command.Name))
|
||||||
{
|
{
|
||||||
_itemTitle = Name,
|
_defaultCommandContextItem = new(new CommandContextItem(model.Command!), PageContext)
|
||||||
Subtitle = Subtitle,
|
{
|
||||||
Command = Command,
|
_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
|
// Only set the icon on the context item for us if our command didn't
|
||||||
// have its own icon
|
// have its own icon
|
||||||
if (!Command.HasIcon)
|
if (!Command.HasIcon)
|
||||||
{
|
{
|
||||||
_defaultCommandContextItem._listItemIcon = _listItemIcon;
|
_defaultCommandContextItem._listItemIcon = _listItemIcon;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Initialized |= InitializedState.SelectionInitialized;
|
Initialized |= InitializedState.SelectionInitialized;
|
||||||
@@ -398,23 +401,6 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
|||||||
base.SafeCleanup();
|
base.SafeCleanup();
|
||||||
Initialized |= InitializedState.CleanedUp;
|
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]
|
[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<UpdateCommandBarMessage>(new(item));
|
||||||
|
|
||||||
WeakReferenceMessenger.Default.Send<UpdateItemKeybindingsMessage>(new(item.Keybindings()));
|
|
||||||
|
|
||||||
if (ShowDetails && item.HasDetails)
|
if (ShowDetails && item.HasDetails)
|
||||||
{
|
{
|
||||||
WeakReferenceMessenger.Default.Send<ShowDetailsMessage>(new(item.Details));
|
WeakReferenceMessenger.Default.Send<ShowDetailsMessage>(new(item.Details));
|
||||||
|
@@ -2,8 +2,11 @@
|
|||||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using Microsoft.CommandPalette.Extensions;
|
using Windows.System;
|
||||||
|
|
||||||
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
|
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.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
|
using Microsoft.CommandPalette.Extensions;
|
||||||
|
|
||||||
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
|
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
|
public interface IContextMenuContext : INotifyPropertyChanged
|
||||||
// buttons at the bottom.
|
|
||||||
//
|
|
||||||
// This is implemented by both ListItemViewModel and ContentPageViewModel,
|
|
||||||
// the two things with sub-commands.
|
|
||||||
public interface ICommandBarContext : INotifyPropertyChanged
|
|
||||||
{
|
{
|
||||||
public IEnumerable<CommandContextItemViewModel> MoreCommands { get; }
|
public IEnumerable<CommandContextItemViewModel> MoreCommands { get; }
|
||||||
|
|
||||||
public bool HasMoreCommands { 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 string SecondaryCommandName { get; }
|
||||||
|
|
||||||
public CommandItemViewModel? PrimaryCommand { get; }
|
public CommandItemViewModel? PrimaryCommand { get; }
|
||||||
|
|
||||||
public CommandItemViewModel? SecondaryCommand { get; }
|
public CommandItemViewModel? SecondaryCommand { get; }
|
||||||
|
|
||||||
public List<CommandContextItemViewModel> AllCommands { get; }
|
|
||||||
}
|
}
|
||||||
|
@@ -225,27 +225,42 @@
|
|||||||
ToolTipService.ToolTip="Ctrl+K"
|
ToolTipService.ToolTip="Ctrl+K"
|
||||||
Visibility="{x:Bind ViewModel.ShouldShowContextMenu, Mode=OneWay}">
|
Visibility="{x:Bind ViewModel.ShouldShowContextMenu, Mode=OneWay}">
|
||||||
<Button.Flyout>
|
<Button.Flyout>
|
||||||
<Flyout Placement="TopEdgeAlignedRight">
|
<Flyout
|
||||||
<ListView
|
Closing="Flyout_Closing"
|
||||||
x:Name="CommandsDropdown"
|
Opened="Flyout_Opened"
|
||||||
MinWidth="248"
|
Placement="TopEdgeAlignedRight">
|
||||||
Margin="-16,-12,-16,-12"
|
<StackPanel>
|
||||||
IsItemClickEnabled="True"
|
|
||||||
ItemClick="CommandsDropdown_ItemClick"
|
<ListView
|
||||||
ItemTemplate="{StaticResource ContextMenuViewModelTemplate}"
|
x:Name="CommandsDropdown"
|
||||||
ItemsSource="{x:Bind ViewModel.ContextCommands, Mode=OneWay}"
|
MinWidth="248"
|
||||||
KeyDown="CommandsDropdown_KeyDown"
|
Margin="-16,-12,-16,-12"
|
||||||
SelectionMode="None">
|
IsItemClickEnabled="True"
|
||||||
<ListView.ItemContainerStyle>
|
ItemClick="CommandsDropdown_ItemClick"
|
||||||
<Style BasedOn="{StaticResource DefaultListViewItemStyle}" TargetType="ListViewItem">
|
ItemTemplate="{StaticResource ContextMenuViewModelTemplate}"
|
||||||
<Setter Property="MinHeight" Value="0" />
|
ItemsSource="{x:Bind ViewModel.ContextMenu.FilteredItems, Mode=OneWay}"
|
||||||
<Setter Property="Padding" Value="12,7,12,7" />
|
KeyDown="CommandsDropdown_KeyDown"
|
||||||
</Style>
|
SelectionMode="Single">
|
||||||
</ListView.ItemContainerStyle>
|
<ListView.ItemContainerStyle>
|
||||||
<ListView.ItemContainerTransitions>
|
<Style BasedOn="{StaticResource DefaultListViewItemStyle}" TargetType="ListViewItem">
|
||||||
<TransitionCollection />
|
<Setter Property="MinHeight" Value="0" />
|
||||||
</ListView.ItemContainerTransitions>
|
<Setter Property="Padding" Value="12,7,12,7" />
|
||||||
</ListView>
|
</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>
|
</Flyout>
|
||||||
</Button.Flyout>
|
</Button.Flyout>
|
||||||
</Button>
|
</Button>
|
||||||
|
@@ -18,9 +18,10 @@ namespace Microsoft.CmdPal.UI.Controls;
|
|||||||
|
|
||||||
public sealed partial class CommandBar : UserControl,
|
public sealed partial class CommandBar : UserControl,
|
||||||
IRecipient<OpenContextMenuMessage>,
|
IRecipient<OpenContextMenuMessage>,
|
||||||
|
IRecipient<TryCommandKeybindingMessage>,
|
||||||
ICurrentPageAware
|
ICurrentPageAware
|
||||||
{
|
{
|
||||||
public CommandBarViewModel ViewModel { get; set; } = new();
|
public CommandBarViewModel ViewModel { get; } = new();
|
||||||
|
|
||||||
public PageViewModel? CurrentPageViewModel
|
public PageViewModel? CurrentPageViewModel
|
||||||
{
|
{
|
||||||
@@ -38,6 +39,9 @@ public sealed partial class CommandBar : UserControl,
|
|||||||
|
|
||||||
// RegisterAll isn't AOT compatible
|
// RegisterAll isn't AOT compatible
|
||||||
WeakReferenceMessenger.Default.Register<OpenContextMenuMessage>(this);
|
WeakReferenceMessenger.Default.Register<OpenContextMenuMessage>(this);
|
||||||
|
WeakReferenceMessenger.Default.Register<TryCommandKeybindingMessage>(this);
|
||||||
|
|
||||||
|
ViewModel.PropertyChanged += ViewModel_PropertyChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Receive(OpenContextMenuMessage message)
|
public void Receive(OpenContextMenuMessage message)
|
||||||
@@ -52,8 +56,41 @@ public sealed partial class CommandBar : UserControl,
|
|||||||
ShowMode = FlyoutShowMode.Standard,
|
ShowMode = FlyoutShowMode.Standard,
|
||||||
};
|
};
|
||||||
MoreCommandsButton.Flyout.ShowAt(MoreCommandsButton, options);
|
MoreCommandsButton.Flyout.ShowAt(MoreCommandsButton, options);
|
||||||
CommandsDropdown.SelectedIndex = 0;
|
UpdateUiForStackChange();
|
||||||
CommandsDropdown.Focus(FocusState.Programmatic);
|
}
|
||||||
|
|
||||||
|
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")]
|
[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)
|
if (e.ClickedItem is CommandContextItemViewModel item)
|
||||||
{
|
{
|
||||||
ViewModel?.InvokeItemCommand.Execute(item);
|
if (ViewModel?.InvokeItem(item) == ContextKeybindingResult.Hide)
|
||||||
MoreCommandsButton.Flyout.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) ||
|
var winPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.LeftWindows).HasFlag(CoreVirtualKeyStates.Down) ||
|
||||||
InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.RightWindows).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;
|
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;
|
||||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||||
using Microsoft.CmdPal.UI.Views;
|
using Microsoft.CmdPal.UI.Views;
|
||||||
using Microsoft.CommandPalette.Extensions;
|
|
||||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
|
||||||
using Microsoft.UI.Dispatching;
|
using Microsoft.UI.Dispatching;
|
||||||
using Microsoft.UI.Input;
|
using Microsoft.UI.Input;
|
||||||
using Microsoft.UI.Xaml;
|
using Microsoft.UI.Xaml;
|
||||||
@@ -23,7 +21,6 @@ namespace Microsoft.CmdPal.UI.Controls;
|
|||||||
public sealed partial class SearchBar : UserControl,
|
public sealed partial class SearchBar : UserControl,
|
||||||
IRecipient<GoHomeMessage>,
|
IRecipient<GoHomeMessage>,
|
||||||
IRecipient<FocusSearchBoxMessage>,
|
IRecipient<FocusSearchBoxMessage>,
|
||||||
IRecipient<UpdateItemKeybindingsMessage>,
|
|
||||||
ICurrentPageAware
|
ICurrentPageAware
|
||||||
{
|
{
|
||||||
private readonly DispatcherQueue _queue = DispatcherQueue.GetForCurrentThread();
|
private readonly DispatcherQueue _queue = DispatcherQueue.GetForCurrentThread();
|
||||||
@@ -34,8 +31,6 @@ public sealed partial class SearchBar : UserControl,
|
|||||||
private readonly DispatcherQueueTimer _debounceTimer = DispatcherQueue.GetForCurrentThread().CreateTimer();
|
private readonly DispatcherQueueTimer _debounceTimer = DispatcherQueue.GetForCurrentThread().CreateTimer();
|
||||||
private bool _isBackspaceHeld;
|
private bool _isBackspaceHeld;
|
||||||
|
|
||||||
private Dictionary<KeyChord, CommandContextItemViewModel>? _keyBindings;
|
|
||||||
|
|
||||||
public PageViewModel? CurrentPageViewModel
|
public PageViewModel? CurrentPageViewModel
|
||||||
{
|
{
|
||||||
get => (PageViewModel?)GetValue(CurrentPageViewModelProperty);
|
get => (PageViewModel?)GetValue(CurrentPageViewModelProperty);
|
||||||
@@ -74,7 +69,6 @@ public sealed partial class SearchBar : UserControl,
|
|||||||
this.InitializeComponent();
|
this.InitializeComponent();
|
||||||
WeakReferenceMessenger.Default.Register<GoHomeMessage>(this);
|
WeakReferenceMessenger.Default.Register<GoHomeMessage>(this);
|
||||||
WeakReferenceMessenger.Default.Register<FocusSearchBoxMessage>(this);
|
WeakReferenceMessenger.Default.Register<FocusSearchBoxMessage>(this);
|
||||||
WeakReferenceMessenger.Default.Register<UpdateItemKeybindingsMessage>(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ClearSearch()
|
public void ClearSearch()
|
||||||
@@ -173,17 +167,14 @@ public sealed partial class SearchBar : UserControl,
|
|||||||
WeakReferenceMessenger.Default.Send<NavigateBackMessage>(new());
|
WeakReferenceMessenger.Default.Send<NavigateBackMessage>(new());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_keyBindings != null)
|
if (!e.Handled)
|
||||||
{
|
{
|
||||||
// Does the pressed key match any of the keybindings?
|
// The CommandBar is responsible for handling all the item keybindings,
|
||||||
var pressedKeyChord = KeyChordHelpers.FromModifiers(ctrlPressed, altPressed, shiftPressed, winPressed, (int)e.Key, 0);
|
// since the bound context item may need to then show another
|
||||||
if (_keyBindings.TryGetValue(pressedKeyChord, out var item))
|
// context menu
|
||||||
{
|
TryCommandKeybindingMessage msg = new(ctrlPressed, altPressed, shiftPressed, winPressed, e.Key);
|
||||||
// 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
|
WeakReferenceMessenger.Default.Send(msg);
|
||||||
// so that the correct item is activated.
|
e.Handled = msg.Handled;
|
||||||
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(item));
|
|
||||||
e.Handled = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -302,10 +293,5 @@ public sealed partial class SearchBar : UserControl,
|
|||||||
|
|
||||||
public void Receive(GoHomeMessage message) => ClearSearch();
|
public void Receive(GoHomeMessage message) => ClearSearch();
|
||||||
|
|
||||||
public void Receive(FocusSearchBoxMessage message) => this.Focus(Microsoft.UI.Xaml.FocusState.Programmatic);
|
public void Receive(FocusSearchBoxMessage message) => FilterBox.Focus(Microsoft.UI.Xaml.FocusState.Programmatic);
|
||||||
|
|
||||||
public void Receive(UpdateItemKeybindingsMessage message)
|
|
||||||
{
|
|
||||||
_keyBindings = message.Keys;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -187,8 +187,6 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
|||||||
|
|
||||||
WeakReferenceMessenger.Default.Send<UpdateCommandBarMessage>(new(null));
|
WeakReferenceMessenger.Default.Send<UpdateCommandBarMessage>(new(null));
|
||||||
|
|
||||||
WeakReferenceMessenger.Default.Send<UpdateItemKeybindingsMessage>(new(null));
|
|
||||||
|
|
||||||
var isMainPage = command is MainListPage;
|
var isMainPage = command is MainListPage;
|
||||||
|
|
||||||
// Construct our ViewModel of the appropriate type and pass it the UI Thread context.
|
// 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">
|
<data name="BehaviorSettingsHeader.Text" xml:space="preserve">
|
||||||
<value>Behavior</value>
|
<value>Behavior</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="ContextFilterBox.PlaceholderText" xml:space="preserve">
|
||||||
|
<value>Search commands...</value>
|
||||||
|
</data>
|
||||||
<data name="Settings_GeneralPage_ShowSystemTrayIcon_SettingsCard.Header" xml:space="preserve">
|
<data name="Settings_GeneralPage_ShowSystemTrayIcon_SettingsCard.Header" xml:space="preserve">
|
||||||
<value>Show system tray icon</value>
|
<value>Show system tray icon</value>
|
||||||
</data>
|
</data>
|
||||||
|
@@ -6,6 +6,7 @@ using System.Threading;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.CommandPalette.Extensions;
|
using Microsoft.CommandPalette.Extensions;
|
||||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||||
|
using Windows.System;
|
||||||
|
|
||||||
namespace SamplePagesExtension;
|
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.",
|
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()
|
public EvilSamplesPage()
|
||||||
|
@@ -69,62 +69,47 @@ internal sealed partial class SampleListPage : ListPage
|
|||||||
},
|
},
|
||||||
|
|
||||||
new ListItem(
|
new ListItem(
|
||||||
new AnonymousCommand(() =>
|
new ToastCommand("Primary command invoked", MessageState.Info) { Name = "Primary command", Icon = new IconInfo("\uF146") }) // dial 1
|
||||||
{
|
|
||||||
var t = new ToastStatusMessage(new StatusMessage()
|
|
||||||
{
|
|
||||||
Message = "Primary command invoked",
|
|
||||||
State = MessageState.Info,
|
|
||||||
});
|
|
||||||
t.Show();
|
|
||||||
})
|
|
||||||
{
|
|
||||||
Result = CommandResult.KeepOpen(),
|
|
||||||
Icon = new IconInfo("\uE712"),
|
|
||||||
})
|
|
||||||
{
|
{
|
||||||
Title = "You can add context menu items too. Press Ctrl+k",
|
Title = "You can add context menu items too. Press Ctrl+k",
|
||||||
Subtitle = "Try pressing Ctrl+1 with me selected",
|
Subtitle = "Try pressing Ctrl+1 with me selected",
|
||||||
Icon = new IconInfo("\uE712"),
|
Icon = new IconInfo("\uE712"), // "More" dots
|
||||||
MoreCommands = [
|
MoreCommands = [
|
||||||
new CommandContextItem(
|
new CommandContextItem(
|
||||||
new AnonymousCommand(() =>
|
new ToastCommand("Secondary command invoked", MessageState.Warning) { Name = "Secondary command", Icon = new IconInfo("\uF147") }) // dial 2
|
||||||
{
|
|
||||||
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(),
|
|
||||||
})
|
|
||||||
{
|
{
|
||||||
Title = "I'm a second command",
|
Title = "I'm a second command",
|
||||||
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.Number1),
|
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.Number1),
|
||||||
},
|
},
|
||||||
new CommandContextItem(
|
new CommandContextItem(
|
||||||
new AnonymousCommand(() =>
|
new ToastCommand("Third command invoked", MessageState.Error) { Name = "Do 3", Icon = new IconInfo("\uF148") }) // dial 3
|
||||||
{
|
|
||||||
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(),
|
|
||||||
})
|
|
||||||
{
|
{
|
||||||
Title = "A third command too",
|
Title = "We can go deeper...",
|
||||||
Icon = new IconInfo("\uF148"),
|
Icon = new IconInfo("\uF148"),
|
||||||
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.Number2),
|
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",
|
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)
|
: base(command)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ListItem()
|
||||||
|
: base()
|
||||||
|
{
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user