Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
3007f04
for felipe and lauren
zadjii-msft Apr 3, 2025
c02034c
do the whole thing
zadjii-msft Apr 4, 2025
68dc40e
Merge branch 'dev/migrie/f/fg-window-sample' into dev/migrie/f/contex…
zadjii-msft Apr 4, 2025
b53cc6f
add samples
zadjii-msft Apr 4, 2025
65f1d99
context menu too
zadjii-msft Apr 4, 2025
8d323f8
spel
zadjii-msft Apr 7, 2025
d708129
Merge remote-tracking branch 'origin/main' into dev/migrie/f/context-…
zadjii-msft Apr 8, 2025
7523801
more spel
zadjii-msft Apr 8, 2025
c42d843
the whole thing
zadjii-msft Apr 8, 2025
ca0b139
attempting to keep the focus on the list items
zadjii-msft Apr 9, 2025
02a1a3b
rudimentary: add a filter box
zadjii-msft Apr 10, 2025
a8e3777
implement the filter box
zadjii-msft Apr 11, 2025
60d027d
most keybindings
zadjii-msft Apr 11, 2025
6b53102
everything is awesome, I'm amazing
zadjii-msft Apr 11, 2025
5eb5f6c
I'm awesome
zadjii-msft Apr 11, 2025
38de9c4
xamlformat
zadjii-msft Apr 11, 2025
d982206
resources
zadjii-msft Apr 11, 2025
314ea6d
Merge remote-tracking branch 'origin/main' into dev/migrie/f/context-…
zadjii-msft Apr 15, 2025
6a963aa
Merge remote-tracking branch 'origin/main' into dev/migrie/f/context-…
zadjii-msft Apr 15, 2025
7e586ca
Merge remote-tracking branch 'origin/main' into dev/migrie/f/context-…
zadjii-msft Apr 17, 2025
d96fd91
Merge branch 'dev/migrie/f/context-shortcuts' into dev/migrie/f/neste…
zadjii-msft Apr 17, 2025
5572830
Ctrl+a will select the text, duh
zadjii-msft Apr 17, 2025
28ed80a
Merge remote-tracking branch 'origin/main' into dev/migrie/f/nested-c…
zadjii-msft Apr 17, 2025
18bcb33
Don't context menu empty primary commands
zadjii-msft Apr 17, 2025
8fe1838
Samples
zadjii-msft Apr 21, 2025
e72741c
Merge remote-tracking branch 'origin/main' into dev/migrie/f/nested-c…
zadjii-msft Apr 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -51,20 +47,17 @@ public ICommandBarContext? SelectedItem
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)
Expand Down Expand Up @@ -109,7 +102,10 @@ private void UpdateContextItems()
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
{
Expand All @@ -119,43 +115,80 @@ private void UpdateContextItems()

// 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)
{
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 CanPopContextStack()
{
return ContextMenuStack.Count > 1;
}

public void PopContextStack()
{
if (ContextMenuStack.Count > 1)
{
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(SecondaryCommand.Command.Model, SecondaryCommand.Model));
ContextMenuStack.RemoveAt(ContextMenuStack.Count - 1);
}

OnPropertyChanging(nameof(ContextMenu));
OnPropertyChanged(nameof(ContextMenu));
}

public bool CheckKeybinding(bool ctrl, bool alt, bool shift, bool win, VirtualKey key)
public void ClearContextStack()
{
if (_contextKeybindings != null)
while (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 enum ContextKeybindingResult
{
Unhandled,
Hide,
KeepOpen,
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -187,23 +187,26 @@ public void SlowInitializeProperties()
// 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,

// 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;
_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
};

// 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;
Expand Down Expand Up @@ -398,23 +401,6 @@ public override void SafeCleanup()
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]
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -344,8 +344,6 @@ private void UpdateSelectedItem(ListItemViewModel item)
{
WeakReferenceMessenger.Default.Send<UpdateCommandBarMessage>(new(item));

WeakReferenceMessenger.Default.Send<UpdateItemKeybindingsMessage>(new(item.Keybindings()));

if (ShowDetails && item.HasDetails)
{
WeakReferenceMessenger.Default.Send<ShowDetailsMessage>(new(item.Details));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -13,22 +14,42 @@ public record UpdateCommandBarMessage(ICommandBarContext? ViewModel)
{
}

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 : INotifyPropertyChanged
public interface ICommandBarContext : IContextMenuContext
{
public IEnumerable<CommandContextItemViewModel> MoreCommands { get; }

public bool HasMoreCommands { get; }

public string SecondaryCommandName { get; }

public CommandItemViewModel? PrimaryCommand { get; }

public CommandItemViewModel? SecondaryCommand { get; }

public List<CommandContextItemViewModel> AllCommands { get; }
}
Loading
Loading