diff --git a/src/modules/cmdpal/Exts/SamplePagesExtension/Pages/SampleContentPage.cs b/src/modules/cmdpal/Exts/SamplePagesExtension/Pages/SampleContentPage.cs index e935a4bac0..a602f74a00 100644 --- a/src/modules/cmdpal/Exts/SamplePagesExtension/Pages/SampleContentPage.cs +++ b/src/modules/cmdpal/Exts/SamplePagesExtension/Pages/SampleContentPage.cs @@ -18,8 +18,24 @@ internal sealed partial class SampleContentPage : ContentPage public SampleContentPage() { - Name = "Sample Content"; + Name = "Open"; + Title = "Sample Content"; Icon = new IconInfo("\uECA5"); // Tiles + + Commands = [ + new CommandContextItem( + title: "Do thing", + name: "Do thing", + subtitle: "Pops a toast", + result: CommandResult.ShowToast(new ToastArgs() { Message = "what's up doc", Result = CommandResult.KeepOpen() }), + action: () => { Title = Title + "+1"; }), + new CommandContextItem( + title: "Something else", + name: "Something else", + subtitle: "Something else", + result: CommandResult.ShowToast(new ToastArgs() { Message = "turn down for what?", Result = CommandResult.KeepOpen() }), + action: () => { Title = Title + "-1"; }), + ]; } } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandBarViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandBarViewModel.cs index c2760a7cc6..11c92baab1 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandBarViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandBarViewModel.cs @@ -13,7 +13,7 @@ namespace Microsoft.CmdPal.UI.ViewModels; public partial class CommandBarViewModel : ObservableObject, IRecipient { - public ListItemViewModel? SelectedItem + public ICommandBarContext? SelectedItem { get => field; set @@ -51,11 +51,11 @@ public partial class CommandBarViewModel : ObservableObject, public void Receive(UpdateCommandBarMessage message) => SelectedItem = message.ViewModel; - private void SetSelectedItem(ListItemViewModel? value) + private void SetSelectedItem(ICommandBarContext? value) { if (value != null) { - PrimaryCommand = value; + PrimaryCommand = value.PrimaryCommand; value.PropertyChanged += SelectedItemPropertyChanged; } else @@ -92,7 +92,7 @@ public partial class CommandBarViewModel : ObservableObject, SecondaryCommand = SelectedItem.SecondaryCommand; - if (SelectedItem.MoreCommands.Count > 1) + if (SelectedItem.MoreCommands.Count() > 1) { ShouldShowContextMenu = true; ContextCommands = [.. SelectedItem.AllCommands]; @@ -104,7 +104,26 @@ 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(new(item.Command.Model, item.Model)); + WeakReferenceMessenger.Default.Send(new(item.Command.Model, item.Model)); + + // this comes in when the primary button is tapped + public void InvokePrimaryCommand() + { + if (PrimaryCommand != null) + { + WeakReferenceMessenger.Default.Send(new(PrimaryCommand.Command.Model, PrimaryCommand.Model)); + } + } + + // this comes in when the secondary button is tapped + public void InvokeSecondaryCommand() + { + if (SecondaryCommand != null) + { + WeakReferenceMessenger.Default.Send(new(SecondaryCommand.Command.Model, SecondaryCommand.Model)); + } + } } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandContextItemViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandContextItemViewModel.cs index 82ebc49ae9..eea629ba05 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandContextItemViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandContextItemViewModel.cs @@ -9,7 +9,7 @@ namespace Microsoft.CmdPal.UI.ViewModels; public partial class CommandContextItemViewModel(ICommandContextItem contextItem, IPageContext context) : CommandItemViewModel(new(contextItem), context) { - public ExtensionObject Model { get; } = new(contextItem); + public new ExtensionObject Model { get; } = new(contextItem); public bool IsCritical { get; private set; } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandItemViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandItemViewModel.cs index 0e7672f582..a2adf570ec 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandItemViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandItemViewModel.cs @@ -2,14 +2,17 @@ // 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.CmdPal.UI.ViewModels.Messages; using Microsoft.CmdPal.UI.ViewModels.Models; using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions.Toolkit; namespace Microsoft.CmdPal.UI.ViewModels; -public partial class CommandItemViewModel : ExtensionObjectViewModel +public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBarContext { + public ExtensionObject Model => _commandItemModel; + private readonly ExtensionObject _commandItemModel = new(null); private CommandContextItemViewModel? _defaultCommandContextItem; @@ -37,10 +40,14 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel public List MoreCommands { get; private set; } = []; + IEnumerable ICommandBarContext.MoreCommands => MoreCommands; + public bool HasMoreCommands => MoreCommands.Count > 0; public string SecondaryCommandName => SecondaryCommand?.Name ?? string.Empty; + public CommandItemViewModel? PrimaryCommand => this; + public CommandItemViewModel? SecondaryCommand => HasMoreCommands ? MoreCommands[0] : null; public bool ShouldBeVisible => !string.IsNullOrEmpty(Name); diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContentPageViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContentPageViewModel.cs index 1e48d324f7..a9515601a3 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContentPageViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContentPageViewModel.cs @@ -5,6 +5,7 @@ using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Messaging; using Microsoft.CmdPal.UI.ViewModels.Messages; using Microsoft.CmdPal.UI.ViewModels.Models; @@ -13,7 +14,7 @@ using Microsoft.CommandPalette.Extensions.Toolkit; namespace Microsoft.CmdPal.UI.ViewModels; -public partial class ContentPageViewModel : PageViewModel +public partial class ContentPageViewModel : PageViewModel, ICommandBarContext { private readonly ExtensionObject _model; @@ -29,6 +30,20 @@ public partial class ContentPageViewModel : PageViewModel [MemberNotNullWhen(true, nameof(Details))] public bool HasDetails => Details != null; + /////// ICommandBarContext /////// + public IEnumerable MoreCommands => Commands.Skip(1); + + public bool HasMoreCommands => Commands.Count > 1; + + public string SecondaryCommandName => SecondaryCommand?.Name ?? string.Empty; + + public CommandItemViewModel? PrimaryCommand => HasCommands ? Commands[0] : null; + + public CommandItemViewModel? SecondaryCommand => HasMoreCommands ? Commands[1] : null; + + public List AllCommands => Commands; + /////// /ICommandBarContext /////// + // Remember - "observable" properties from the model (via PropChanged) // cannot be marked [ObservableProperty] public ContentPageViewModel(IContentPage model, TaskScheduler scheduler, CommandPaletteHost host) @@ -102,6 +117,10 @@ public partial class ContentPageViewModel : PageViewModel .Select(contextItem => (contextItem as ICommandContextItem)!) .Select(contextItem => new CommandContextItemViewModel(contextItem, PageContext)) .ToList(); + Commands.ForEach(contextItem => + { + contextItem.InitializeProperties(); + }); var extensionDetails = model.Details; if (extensionDetails != null) @@ -114,6 +133,15 @@ public partial class ContentPageViewModel : PageViewModel FetchContent(); model.ItemsChanged += Model_ItemsChanged; + + Task.Factory.StartNew( + () => + { + WeakReferenceMessenger.Default.Send(new(this)); + }, + CancellationToken.None, + TaskCreationOptions.None, + PageContext.Scheduler); } protected override void FetchProperty(string propertyName) @@ -128,10 +156,47 @@ public partial class ContentPageViewModel : PageViewModel switch (propertyName) { - // case nameof(Commands): - // TODO GH #360 - make MoreCommands observable - // this.ShowDetails = model.ShowDetails; - // break; + case nameof(Commands): + + var more = model.Commands; + if (more != null) + { + var newContextMenu = more + .Where(contextItem => contextItem is ICommandContextItem) + .Select(contextItem => (contextItem as ICommandContextItem)!) + .Select(contextItem => new CommandContextItemViewModel(contextItem, PageContext)) + .ToList(); + lock (Commands) + { + ListHelpers.InPlaceUpdateList(Commands, newContextMenu); + } + + Commands.ForEach(contextItem => + { + contextItem.InitializeProperties(); + }); + } + else + { + Commands.Clear(); + } + + UpdateProperty(nameof(PrimaryCommand)); + UpdateProperty(nameof(SecondaryCommand)); + UpdateProperty(nameof(SecondaryCommandName)); + UpdateProperty(nameof(HasCommands)); + UpdateProperty(nameof(HasMoreCommands)); + UpdateProperty(nameof(AllCommands)); + Task.Factory.StartNew( + () => + { + WeakReferenceMessenger.Default.Send(new(this)); + }, + CancellationToken.None, + TaskCreationOptions.None, + PageContext.Scheduler); + + break; case nameof(Details): var extensionDetails = model.Details; Details = extensionDetails != null ? new(extensionDetails, PageContext) : null; @@ -163,4 +228,25 @@ public partial class ContentPageViewModel : PageViewModel TaskCreationOptions.None, PageContext.Scheduler); } + + // InvokeItemCommand is what this will be in Xaml due to source generator + // this comes in on Enter keypresses in the SearchBox + [RelayCommand] + private void InvokePrimaryCommand(ContentPageViewModel page) + { + if (PrimaryCommand != null) + { + WeakReferenceMessenger.Default.Send(new(PrimaryCommand.Command.Model, PrimaryCommand.Model)); + } + } + + // this comes in on Ctrl+Enter keypresses in the SearchBox + [RelayCommand] + private void InvokeSecondaryCommand(ContentPageViewModel page) + { + if (SecondaryCommand != null) + { + WeakReferenceMessenger.Default.Send(new(SecondaryCommand.Command.Model, SecondaryCommand.Model)); + } + } } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ListItemViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ListItemViewModel.cs index a8376b98e0..d21a91b248 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ListItemViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ListItemViewModel.cs @@ -14,7 +14,7 @@ namespace Microsoft.CmdPal.UI.ViewModels; public partial class ListItemViewModel(IListItem model, IPageContext context) : CommandItemViewModel(new(model), context) { - public ExtensionObject Model { get; } = new(model); + public new ExtensionObject Model { get; } = new(model); [ObservableProperty] public partial ObservableCollection Tags { get; set; } = []; diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/PerformCommandMessage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/PerformCommandMessage.cs index 9fe20b3d0e..acd5df65e5 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/PerformCommandMessage.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/PerformCommandMessage.cs @@ -30,6 +30,12 @@ public record PerformCommandMessage Context = context.Unsafe; } + public PerformCommandMessage(ExtensionObject command, ExtensionObject context) + { + Command = command; + Context = context.Unsafe; + } + public PerformCommandMessage(ExtensionObject command, ExtensionObject context) { Command = command; diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/UpdateCommandBarMessage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/UpdateCommandBarMessage.cs index 2e409ff39b..0a540c7408 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/UpdateCommandBarMessage.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/UpdateCommandBarMessage.cs @@ -2,11 +2,33 @@ // 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.ComponentModel; + namespace Microsoft.CmdPal.UI.ViewModels.Messages; /// /// Used to update the command bar at the bottom to reflect the commands for a list item /// -public record UpdateCommandBarMessage(ListItemViewModel? ViewModel) +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 IEnumerable MoreCommands { get; } + + public bool HasMoreCommands { get; } + + public string SecondaryCommandName { get; } + + public CommandItemViewModel? PrimaryCommand { get; } + + public CommandItemViewModel? SecondaryCommand { get; } + + public List AllCommands { get; } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml.cs index d1679bad62..382e7ba7ea 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml.cs @@ -10,7 +10,6 @@ using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls.Primitives; using Microsoft.UI.Xaml.Input; -using Windows.System; namespace Microsoft.CmdPal.UI.Controls; @@ -55,12 +54,16 @@ public sealed partial class CommandBar : UserControl, } [System.Diagnostics.CodeAnalysis.SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "VS has a tendency to delete XAML bound methods over-aggressively")] - private void PrimaryButton_Tapped(object sender, TappedRoutedEventArgs e) => - WeakReferenceMessenger.Default.Send(); + private void PrimaryButton_Tapped(object sender, TappedRoutedEventArgs e) + { + ViewModel.InvokePrimaryCommand(); + } [System.Diagnostics.CodeAnalysis.SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "VS has a tendency to delete XAML bound methods over-aggressively")] - private void SecondaryButton_Tapped(object sender, TappedRoutedEventArgs e) => - WeakReferenceMessenger.Default.Send(); + private void SecondaryButton_Tapped(object sender, TappedRoutedEventArgs e) + { + ViewModel.InvokeSecondaryCommand(); + } private void PageIcon_Tapped(object sender, TappedRoutedEventArgs e) { diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ContentPage.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ContentPage.xaml.cs index 6e746bbd4e..45dde01148 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ContentPage.xaml.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ContentPage.xaml.cs @@ -2,7 +2,9 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using CommunityToolkit.Mvvm.Messaging; using Microsoft.CmdPal.UI.ViewModels; +using Microsoft.CmdPal.UI.ViewModels.Messages; using Microsoft.UI.Dispatching; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; @@ -13,7 +15,9 @@ namespace Microsoft.CmdPal.UI; /// /// An empty page that can be used on its own or navigated to within a Frame. /// -public sealed partial class ContentPage : Page +public sealed partial class ContentPage : Page, + IRecipient, + IRecipient { private readonly DispatcherQueue _queue = DispatcherQueue.GetForCurrentThread(); @@ -30,6 +34,8 @@ public sealed partial class ContentPage : Page public ContentPage() { this.InitializeComponent(); + WeakReferenceMessenger.Default.Register(this); + WeakReferenceMessenger.Default.Register(this); } protected override void OnNavigatedTo(NavigationEventArgs e) @@ -42,5 +48,25 @@ public sealed partial class ContentPage : Page base.OnNavigatedTo(e); } - protected override void OnNavigatingFrom(NavigatingCancelEventArgs e) => base.OnNavigatingFrom(e); + protected override void OnNavigatingFrom(NavigatingCancelEventArgs e) + { + base.OnNavigatingFrom(e); + WeakReferenceMessenger.Default.Unregister(this); + WeakReferenceMessenger.Default.Unregister(this); + + // Clean-up event listeners + ViewModel = null; + } + + // this comes in on Enter keypresses in the SearchBox + public void Receive(ActivateSelectedListItemMessage message) + { + ViewModel?.InvokePrimaryCommandCommand?.Execute(ViewModel); + } + + // this comes in on Ctrl+Enter keypresses in the SearchBox + public void Receive(ActivateSecondaryCommandMessage message) + { + ViewModel?.InvokeSecondaryCommandCommand?.Execute(ViewModel); + } } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml.cs index 8037ceb775..66a149dbc6 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml.cs @@ -18,8 +18,8 @@ namespace Microsoft.CmdPal.UI; public sealed partial class ListPage : Page, IRecipient, IRecipient, - IRecipient, - IRecipient + IRecipient, + IRecipient { public ListViewModel? ViewModel { @@ -56,9 +56,9 @@ public sealed partial class ListPage : Page, // RegisterAll isn't AOT compatible WeakReferenceMessenger.Default.Register(this); WeakReferenceMessenger.Default.Register(this); + WeakReferenceMessenger.Default.Register(this); WeakReferenceMessenger.Default.Register(this); - base.OnNavigatedTo(e); }