CmdPal: Add Context Menu command "Show Details" when list item has details, but list view's ShowDetails == false (#40870)

Closes #38270

When a list item's `Details` property is not null, but it's parent
ListViews `ShowDetails` property is false, this PR adds a context menu
item at the bottom of the commands for the list item to show details.
Clicking that command will show the details for the selected item, but
the details pane will hide when a different item is selected.

## Preview


https://github.com/user-attachments/assets/7b5cd3d4-b4ae-433a-ad25-f620590cd261

---------

Signed-off-by: Shawn Yuan <shuaiyuan@microsoft.com>
Co-authored-by: Niels Laute <niels.laute@live.nl>
Co-authored-by: Shawn Yuan <shuaiyuan@microsoft.com>
This commit is contained in:
Michael Jolley
2025-09-26 12:46:09 -05:00
committed by GitHub
parent 744415f20a
commit e1681ec08f
9 changed files with 285 additions and 27 deletions

View File

@@ -3,8 +3,10 @@
// See the LICENSE file in the project root for more information.
using System.Diagnostics.CodeAnalysis;
using Microsoft.CmdPal.Core.ViewModels.Commands;
using Microsoft.CmdPal.Core.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Core.ViewModels;
@@ -73,6 +75,8 @@ public partial class ListItemViewModel(IListItem model, WeakReference<IPageConte
UpdateProperty(nameof(HasDetails));
}
AddShowDetailsCommands();
TextToSuggest = model.TextToSuggest;
UpdateProperty(nameof(TextToSuggest));
}
@@ -104,6 +108,10 @@ public partial class ListItemViewModel(IListItem model, WeakReference<IPageConte
Details?.InitializeProperties();
UpdateProperty(nameof(Details));
UpdateProperty(nameof(HasDetails));
UpdateShowDetailsCommand();
break;
case nameof(MoreCommands):
AddShowDetailsCommands();
break;
case nameof(Title):
case nameof(Subtitle):
@@ -122,6 +130,65 @@ public partial class ListItemViewModel(IListItem model, WeakReference<IPageConte
public override int GetHashCode() => Model.GetHashCode();
private void AddShowDetailsCommands()
{
// If the parent page has ShowDetails = false and we have details,
// then we should add a show details action in the context menu.
if (HasDetails &&
PageContext.TryGetTarget(out var pageContext) &&
pageContext is ListViewModel listViewModel &&
!listViewModel.ShowDetails)
{
// Check if "Show Details" action already exists to prevent duplicates
if (!MoreCommands.Any(cmd => cmd is CommandContextItemViewModel contextItemViewModel &&
contextItemViewModel.Command.Id == ShowDetailsCommand.ShowDetailsCommandId))
{
// Create the view model for the show details command
var showDetailsCommand = new ShowDetailsCommand(Details);
var showDetailsContextItem = new CommandContextItem(showDetailsCommand);
var showDetailsContextItemViewModel = new CommandContextItemViewModel(showDetailsContextItem, PageContext);
showDetailsContextItemViewModel.SlowInitializeProperties();
MoreCommands.Add(showDetailsContextItemViewModel);
}
UpdateProperty(nameof(MoreCommands));
UpdateProperty(nameof(AllCommands));
}
}
// This method is called when the details change to make sure we
// have the latest details in the show details command.
private void UpdateShowDetailsCommand()
{
// If the parent page has ShowDetails = false and we have details,
// then we should add a show details action in the context menu.
if (HasDetails &&
PageContext.TryGetTarget(out var pageContext) &&
pageContext is ListViewModel listViewModel &&
!listViewModel.ShowDetails)
{
var existingCommand = MoreCommands.FirstOrDefault(cmd =>
cmd is CommandContextItemViewModel contextItemViewModel &&
contextItemViewModel.Command.Id == ShowDetailsCommand.ShowDetailsCommandId);
// If the command already exists, remove it to update with the new details
if (existingCommand is not null)
{
MoreCommands.Remove(existingCommand);
}
// Create the view model for the show details command
var showDetailsCommand = new ShowDetailsCommand(Details);
var showDetailsContextItem = new CommandContextItem(showDetailsCommand);
var showDetailsContextItemViewModel = new CommandContextItemViewModel(showDetailsContextItem, PageContext);
showDetailsContextItemViewModel.SlowInitializeProperties();
MoreCommands.Add(showDetailsContextItemViewModel);
UpdateProperty(nameof(MoreCommands));
UpdateProperty(nameof(AllCommands));
}
}
private void UpdateTags(ITag[]? newTagsFromModel)
{
var newTags = newTagsFromModel?.Select(t =>

View File

@@ -2,7 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.Core.ViewModels.Messages;
namespace Microsoft.CmdPal.Core.ViewModels.Commands;
/// <summary>
/// Used to navigate to the next command in the page when pressing the Down key in the SearchBox.

View File

@@ -0,0 +1,72 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace Microsoft.CmdPal.Core.ViewModels.Properties {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
public class Resources {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
public static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.CmdPal.Core.ViewModels.Properties.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
public static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to Show details.
/// </summary>
public static string ShowDetailsCommand {
get {
return ResourceManager.GetString("ShowDetailsCommand", resourceCulture);
}
}
}
}

View File

@@ -0,0 +1,65 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="ShowDetailsCommand" xml:space="preserve">
<value>Show details</value>
<comment>Name for the command that shows details of an item</comment>
</data>
</root>

View File

@@ -0,0 +1,33 @@
// 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 CommunityToolkit.Mvvm.Messaging;
using Microsoft.CmdPal.Core.ViewModels.Messages;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Core.ViewModels.Commands;
public sealed partial class ShowDetailsCommand : InvokableCommand
{
public static string ShowDetailsCommandId { get; } = "com.microsoft.cmdpal.showDetails";
private static IconInfo IconInfo { get; } = new IconInfo("\uF000"); // KnowledgeArticle Icon
private DetailsViewModel Details { get; set; }
public ShowDetailsCommand(DetailsViewModel details)
{
Id = ShowDetailsCommandId;
Name = Properties.Resources.ShowDetailsCommand;
Icon = IconInfo;
Details = details;
}
public override CommandResult Invoke()
{
// Send the ShowDetailsMessage when the action is invoked
WeakReferenceMessenger.Default.Send<ShowDetailsMessage>(new(Details));
return CommandResult.KeepOpen();
}
}

View File

@@ -36,4 +36,11 @@
<ProjectReference Include="$(MSBuildThisFileDirectory)\extensionsdk\Microsoft.CommandPalette.Extensions.Toolkit\Microsoft.CommandPalette.Extensions.Toolkit.csproj" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Properties\Resources.resx">
<Generator>PublicResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
</Project>

View File

@@ -5,6 +5,7 @@
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.WinUI;
using Microsoft.CmdPal.Core.ViewModels;
using Microsoft.CmdPal.Core.ViewModels.Commands;
using Microsoft.CmdPal.Core.ViewModels.Messages;
using Microsoft.CmdPal.UI.Messages;
using Microsoft.CmdPal.UI.Views;

View File

@@ -6,6 +6,7 @@ using System.Diagnostics;
using CommunityToolkit.Mvvm.Messaging;
using ManagedCommon;
using Microsoft.CmdPal.Core.ViewModels;
using Microsoft.CmdPal.Core.ViewModels.Commands;
using Microsoft.CmdPal.Core.ViewModels.Messages;
using Microsoft.CmdPal.UI.Helpers;
using Microsoft.CmdPal.UI.Messages;
@@ -386,8 +387,6 @@ public sealed partial class ListPage : Page,
ItemView.SelectedItem = item;
}
ViewModel?.UpdateSelectedItemCommand.Execute(item);
if (!e.TryGetPosition(element, out var pos))
{
pos = new(0, element.ActualHeight);

View File

@@ -246,6 +246,14 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
}
public void Receive(ShowDetailsMessage message)
{
if (ViewModel is not null &&
ViewModel.CurrentPage is not null)
{
if (ViewModel.CurrentPage.PageContext.TryGetTarget(out var pageContext))
{
Task.Factory.StartNew(
() =>
{
// TERRIBLE HACK TODO GH #245
// There's weird wacky bugs with debounce currently.
@@ -273,6 +281,12 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
interval: TimeSpan.FromMilliseconds(50),
immediate: ViewModel.IsDetailsVisible == false);
ViewModel.IsDetailsVisible = true;
},
CancellationToken.None,
TaskCreationOptions.None,
pageContext.Scheduler);
}
}
}
public void Receive(HideDetailsMessage message) => HideDetails();