mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-02-23 19:49:43 +01:00
Add support for commands on content pages (#495)
Basically just abstracts what we already had for list items, and uses that abstraction for contentpageviewmodel too Closes #476
This commit is contained in:
@@ -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"; }),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
public partial class CommandBarViewModel : ObservableObject,
|
||||
IRecipient<UpdateCommandBarMessage>
|
||||
{
|
||||
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<PerformCommandMessage>(new(item.Command.Model, item.Model));
|
||||
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(item.Command.Model, item.Model));
|
||||
|
||||
// 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));
|
||||
}
|
||||
}
|
||||
|
||||
// this comes in when the secondary button is tapped
|
||||
public void InvokeSecondaryCommand()
|
||||
{
|
||||
if (SecondaryCommand != null)
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(SecondaryCommand.Command.Model, SecondaryCommand.Model));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public partial class CommandContextItemViewModel(ICommandContextItem contextItem, IPageContext context) : CommandItemViewModel(new(contextItem), context)
|
||||
{
|
||||
public ExtensionObject<ICommandContextItem> Model { get; } = new(contextItem);
|
||||
public new ExtensionObject<ICommandContextItem> Model { get; } = new(contextItem);
|
||||
|
||||
public bool IsCritical { get; private set; }
|
||||
|
||||
|
||||
@@ -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<ICommandItem> Model => _commandItemModel;
|
||||
|
||||
private readonly ExtensionObject<ICommandItem> _commandItemModel = new(null);
|
||||
private CommandContextItemViewModel? _defaultCommandContextItem;
|
||||
|
||||
@@ -37,10 +40,14 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel
|
||||
|
||||
public List<CommandContextItemViewModel> MoreCommands { get; private set; } = [];
|
||||
|
||||
IEnumerable<CommandContextItemViewModel> 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);
|
||||
|
||||
@@ -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<IContentPage> _model;
|
||||
|
||||
@@ -29,6 +30,20 @@ public partial class ContentPageViewModel : PageViewModel
|
||||
[MemberNotNullWhen(true, nameof(Details))]
|
||||
public bool HasDetails => Details != null;
|
||||
|
||||
/////// ICommandBarContext ///////
|
||||
public IEnumerable<CommandContextItemViewModel> 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<CommandContextItemViewModel> 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<UpdateCommandBarMessage>(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<UpdateCommandBarMessage>(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<PerformCommandMessage>(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<PerformCommandMessage>(new(SecondaryCommand.Command.Model, SecondaryCommand.Model));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
public partial class ListItemViewModel(IListItem model, IPageContext context)
|
||||
: CommandItemViewModel(new(model), context)
|
||||
{
|
||||
public ExtensionObject<IListItem> Model { get; } = new(model);
|
||||
public new ExtensionObject<IListItem> Model { get; } = new(model);
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ObservableCollection<TagViewModel> Tags { get; set; } = [];
|
||||
|
||||
@@ -30,6 +30,12 @@ public record PerformCommandMessage
|
||||
Context = context.Unsafe;
|
||||
}
|
||||
|
||||
public PerformCommandMessage(ExtensionObject<ICommand> command, ExtensionObject<ICommandItem> context)
|
||||
{
|
||||
Command = command;
|
||||
Context = context.Unsafe;
|
||||
}
|
||||
|
||||
public PerformCommandMessage(ExtensionObject<ICommand> command, ExtensionObject<ICommandContextItem> context)
|
||||
{
|
||||
Command = command;
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Used to update the command bar at the bottom to reflect the commands for a list item
|
||||
/// </summary>
|
||||
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<CommandContextItemViewModel> MoreCommands { get; }
|
||||
|
||||
public bool HasMoreCommands { get; }
|
||||
|
||||
public string SecondaryCommandName { get; }
|
||||
|
||||
public CommandItemViewModel? PrimaryCommand { get; }
|
||||
|
||||
public CommandItemViewModel? SecondaryCommand { get; }
|
||||
|
||||
public List<CommandContextItemViewModel> AllCommands { get; }
|
||||
}
|
||||
|
||||
@@ -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<ActivateSelectedListItemMessage>();
|
||||
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<ActivateSecondaryCommandMessage>();
|
||||
private void SecondaryButton_Tapped(object sender, TappedRoutedEventArgs e)
|
||||
{
|
||||
ViewModel.InvokeSecondaryCommand();
|
||||
}
|
||||
|
||||
private void PageIcon_Tapped(object sender, TappedRoutedEventArgs e)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
/// <summary>
|
||||
/// An empty page that can be used on its own or navigated to within a Frame.
|
||||
/// </summary>
|
||||
public sealed partial class ContentPage : Page
|
||||
public sealed partial class ContentPage : Page,
|
||||
IRecipient<ActivateSelectedListItemMessage>,
|
||||
IRecipient<ActivateSecondaryCommandMessage>
|
||||
{
|
||||
private readonly DispatcherQueue _queue = DispatcherQueue.GetForCurrentThread();
|
||||
|
||||
@@ -30,6 +34,8 @@ public sealed partial class ContentPage : Page
|
||||
public ContentPage()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
WeakReferenceMessenger.Default.Register<ActivateSelectedListItemMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<ActivateSecondaryCommandMessage>(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<ActivateSelectedListItemMessage>(this);
|
||||
WeakReferenceMessenger.Default.Unregister<ActivateSecondaryCommandMessage>(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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,8 +18,8 @@ namespace Microsoft.CmdPal.UI;
|
||||
public sealed partial class ListPage : Page,
|
||||
IRecipient<NavigateNextCommand>,
|
||||
IRecipient<NavigatePreviousCommand>,
|
||||
IRecipient<ActivateSelectedListItemMessage>,
|
||||
IRecipient<ActivateSecondaryCommandMessage>
|
||||
IRecipient<ActivateSelectedListItemMessage>,
|
||||
IRecipient<ActivateSecondaryCommandMessage>
|
||||
{
|
||||
public ListViewModel? ViewModel
|
||||
{
|
||||
@@ -56,9 +56,9 @@ public sealed partial class ListPage : Page,
|
||||
// RegisterAll isn't AOT compatible
|
||||
WeakReferenceMessenger.Default.Register<NavigateNextCommand>(this);
|
||||
WeakReferenceMessenger.Default.Register<NavigatePreviousCommand>(this);
|
||||
|
||||
WeakReferenceMessenger.Default.Register<ActivateSelectedListItemMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<ActivateSecondaryCommandMessage>(this);
|
||||
|
||||
base.OnNavigatedTo(e);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user