From 94ace730c86e321efce786910a6966450c2093e3 Mon Sep 17 00:00:00 2001 From: Guilherme <57814418+DevLGuilherme@users.noreply.github.com> Date: Wed, 10 Dec 2025 23:51:42 -0300 Subject: [PATCH] [CmdPal] Add Sections and Separators for List Pages and Grid Pages (#43952) ## Summary of the Pull Request This pull request adds sections and separators to ListPages and GridPages ## PR Checklist - [x] Closes: #38267 - [ ] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [ ] **Tests:** Added/updated and all pass - [ ] **Localization:** All end-user-facing strings can be localized - [ ] **Dev docs:** Added/updated - [ ] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx ## Detailed Description of the Pull Request / Additional comments Since `CollectionViewSource` was causing performance issues and @zadjii-msft asked for a new approach, I came up with this idea, heavily inspired by how separators work on the `ContextMenu`, `FiltersDropDown` and `Details`. The way this is currently working is: Any ListItem where `Section` is not null and `Command` is null, is considered a Separator. On my tests, this seems to be working fine. Tried to make this work without changes to the API, but I think this needs to be discussed. ### Some of the possible enhancements to existing extensions ### Search apps Screenshot 2025-11-27 173618 ### Window Walker Screenshot 2025-11-27 173728 ### Winget Screenshot 2025-11-27 174006 ### Search files image ### Grid Pages Screenshot 2025-11-27 174055 ## Validation Steps Performed --- .github/actions/spell-check/expect.txt | 2 + .../ListItemViewModel.cs | 33 +- .../SeparatorViewModel.cs | 3 +- .../Controls/WrapPanelCustom/UvBounds.cs | 37 ++ .../Controls/WrapPanelCustom/UvMeasure.cs | 96 ++++ .../Controls/WrapPanelCustom/WrapPanel.cs | 416 ++++++++++++++++++ .../Converters/GridItemTemplateSelector.cs | 15 + .../Converters/ListItemTemplateSelector.cs | 47 ++ .../ExtViews/ListPage.xaml | 64 ++- .../ExtViews/ListPage.xaml.cs | 259 +++++++++-- .../SampleListPageWithSections.cs | 114 +++++ .../Pages/SectionsPages/SectionsIndexPage.cs | 40 ++ .../SamplePagesExtension/SamplesListPage.cs | 5 + .../Section.cs | 39 ++ .../Separator.cs | 36 +- 15 files changed, 1146 insertions(+), 60 deletions(-) create mode 100644 src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/WrapPanelCustom/UvBounds.cs create mode 100644 src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/WrapPanelCustom/UvMeasure.cs create mode 100644 src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/WrapPanelCustom/WrapPanel.cs create mode 100644 src/modules/cmdpal/Microsoft.CmdPal.UI/Converters/ListItemTemplateSelector.cs create mode 100644 src/modules/cmdpal/ext/SamplePagesExtension/Pages/SectionsPages/SampleListPageWithSections.cs create mode 100644 src/modules/cmdpal/ext/SamplePagesExtension/Pages/SectionsPages/SectionsIndexPage.cs create mode 100644 src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/Section.cs diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index f839c5976c..f649476f60 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -1854,6 +1854,8 @@ uitests UITo ULONGLONG ums +UMax +UMin uncompilable UNCPRIORITY UNDNAME diff --git a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/ListItemViewModel.cs b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/ListItemViewModel.cs index a400374e3c..fb4afc49b8 100644 --- a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/ListItemViewModel.cs +++ b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/ListItemViewModel.cs @@ -24,6 +24,8 @@ public partial class ListItemViewModel : CommandItemViewModel public string Section { get; private set; } = string.Empty; + public bool IsSectionOrSeparator { get; private set; } + public DetailsViewModel? Details { get; private set; } [MemberNotNullWhen(true, nameof(Details))] @@ -82,14 +84,18 @@ public partial class ListItemViewModel : CommandItemViewModel } UpdateTags(li.Tags); - Section = li.Section ?? string.Empty; - - UpdateProperty(nameof(Section)); + IsSectionOrSeparator = IsSeparator(li); + UpdateProperty(nameof(Section), nameof(IsSectionOrSeparator)); UpdateAccessibleName(); } + private bool IsSeparator(IListItem item) + { + return item.Command is null; + } + public override void SlowInitializeProperties() { base.SlowInitializeProperties(); @@ -104,8 +110,7 @@ public partial class ListItemViewModel : CommandItemViewModel { Details = new(extensionDetails, PageContext); Details.InitializeProperties(); - UpdateProperty(nameof(Details)); - UpdateProperty(nameof(HasDetails)); + UpdateProperty(nameof(Details), nameof(HasDetails)); } AddShowDetailsCommands(); @@ -135,14 +140,18 @@ public partial class ListItemViewModel : CommandItemViewModel break; case nameof(model.Section): Section = model.Section ?? string.Empty; - UpdateProperty(nameof(Section)); + IsSectionOrSeparator = IsSeparator(model); + UpdateProperty(nameof(Section), nameof(IsSectionOrSeparator)); break; - case nameof(model.Details): + case nameof(model.Command): + IsSectionOrSeparator = IsSeparator(model); + UpdateProperty(nameof(IsSectionOrSeparator)); + break; + case nameof(Details): var extensionDetails = model.Details; Details = extensionDetails is not null ? new(extensionDetails, PageContext) : null; Details?.InitializeProperties(); - UpdateProperty(nameof(Details)); - UpdateProperty(nameof(HasDetails)); + UpdateProperty(nameof(Details), nameof(HasDetails)); UpdateShowDetailsCommand(); break; case nameof(model.MoreCommands): @@ -194,8 +203,7 @@ public partial class ListItemViewModel : CommandItemViewModel MoreCommands.Add(showDetailsContextItemViewModel); } - UpdateProperty(nameof(MoreCommands)); - UpdateProperty(nameof(AllCommands)); + UpdateProperty(nameof(MoreCommands), nameof(AllCommands)); } } @@ -227,8 +235,7 @@ public partial class ListItemViewModel : CommandItemViewModel showDetailsContextItemViewModel.SlowInitializeProperties(); MoreCommands.Add(showDetailsContextItemViewModel); - UpdateProperty(nameof(MoreCommands)); - UpdateProperty(nameof(AllCommands)); + UpdateProperty(nameof(MoreCommands), nameof(AllCommands)); } } diff --git a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/SeparatorViewModel.cs b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/SeparatorViewModel.cs index a1c4696b35..93fae9beff 100644 --- a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/SeparatorViewModel.cs +++ b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/SeparatorViewModel.cs @@ -3,13 +3,14 @@ // See the LICENSE file in the project root for more information. using System.Diagnostics.CodeAnalysis; -using Microsoft.CmdPal.Core.ViewModels; using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; namespace Microsoft.CmdPal.Core.ViewModels; [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] public partial class SeparatorViewModel() : + CommandItem, IContextItemViewModel, IFilterItemViewModel, ISeparatorContextItem, diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/WrapPanelCustom/UvBounds.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/WrapPanelCustom/UvBounds.cs new file mode 100644 index 0000000000..2d7567c346 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/WrapPanelCustom/UvBounds.cs @@ -0,0 +1,37 @@ +// 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.UI.Xaml.Controls; +using Windows.Foundation; + +namespace Microsoft.CmdPal.UI.Controls; + +internal sealed class UVBounds +{ + public double UMin { get; } + + public double UMax { get; } + + public double VMin { get; } + + public double VMax { get; } + + public UVBounds(Orientation orientation, Rect rect) + { + if (orientation == Orientation.Horizontal) + { + UMin = rect.Left; + UMax = rect.Right; + VMin = rect.Top; + VMax = rect.Bottom; + } + else + { + UMin = rect.Top; + UMax = rect.Bottom; + VMin = rect.Left; + VMax = rect.Right; + } + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/WrapPanelCustom/UvMeasure.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/WrapPanelCustom/UvMeasure.cs new file mode 100644 index 0000000000..1b75c31564 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/WrapPanelCustom/UvMeasure.cs @@ -0,0 +1,96 @@ +// 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.Diagnostics; +using Microsoft.UI.Xaml.Controls; +using Windows.Foundation; + +namespace Microsoft.CmdPal.UI.Controls; + +[DebuggerDisplay("U = {U} V = {V}")] +internal struct UvMeasure +{ + internal double U { get; set; } + + internal double V { get; set; } + + internal static UvMeasure Zero => default(UvMeasure); + + public UvMeasure(Orientation orientation, Size size) + : this(orientation, size.Width, size.Height) + { + } + + public UvMeasure(Orientation orientation, double width, double height) + { + if (orientation == Orientation.Horizontal) + { + U = width; + V = height; + } + else + { + U = height; + V = width; + } + } + + public UvMeasure Add(double u, double v) + { + UvMeasure result = default(UvMeasure); + result.U = U + u; + result.V = V + v; + return result; + } + + public UvMeasure Add(UvMeasure measure) + { + return Add(measure.U, measure.V); + } + + public Size ToSize(Orientation orientation) + { + if (orientation != Orientation.Horizontal) + { + return new Size(V, U); + } + + return new Size(U, V); + } + + public Point GetPoint(Orientation orientation) + { + return orientation is Orientation.Horizontal ? new Point(U, V) : new Point(V, U); + } + + public Size GetSize(Orientation orientation) + { + return orientation is Orientation.Horizontal ? new Size(U, V) : new Size(V, U); + } + + public static bool operator ==(UvMeasure measure1, UvMeasure measure2) + { + return measure1.U == measure2.U && measure1.V == measure2.V; + } + + public static bool operator !=(UvMeasure measure1, UvMeasure measure2) + { + return !(measure1 == measure2); + } + + public override bool Equals(object? obj) + { + return obj is UvMeasure measure && this == measure; + } + + public bool Equals(UvMeasure value) + { + return this == value; + } + + public override int GetHashCode() + { + return base.GetHashCode(); + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/WrapPanelCustom/WrapPanel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/WrapPanelCustom/WrapPanel.cs new file mode 100644 index 0000000000..ea0101bfa3 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/WrapPanelCustom/WrapPanel.cs @@ -0,0 +1,416 @@ +// 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.WinUI.Controls; +using Microsoft.CmdPal.Core.ViewModels; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Windows.Foundation; + +namespace Microsoft.CmdPal.UI.Controls; + +/// +/// Arranges elements by wrapping them to fit the available space. +/// When is set to Orientation.Horizontal, element are arranged in rows until the available width is reached and then to a new row. +/// When is set to Orientation.Vertical, element are arranged in columns until the available height is reached. +/// +public sealed partial class WrapPanel : Panel +{ + private struct UvRect + { + public UvMeasure Position { get; set; } + + public UvMeasure Size { get; set; } + + public Rect ToRect(Orientation orientation) + { + return orientation switch + { + Orientation.Vertical => new Rect(Position.V, Position.U, Size.V, Size.U), + Orientation.Horizontal => new Rect(Position.U, Position.V, Size.U, Size.V), + _ => ThrowArgumentException(), + }; + } + + private static Rect ThrowArgumentException() + { + throw new ArgumentException("The input orientation is not valid."); + } + } + + private struct Row + { + public List ChildrenRects { get; } + + public UvMeasure Size { get; set; } + + public UvRect Rect + { + get + { + UvRect result; + if (ChildrenRects.Count <= 0) + { + result = default(UvRect); + result.Position = UvMeasure.Zero; + result.Size = Size; + return result; + } + + result = default(UvRect); + result.Position = ChildrenRects.First().Position; + result.Size = Size; + return result; + } + } + + public Row(List childrenRects, UvMeasure size) + { + ChildrenRects = childrenRects; + Size = size; + } + + public void Add(UvMeasure position, UvMeasure size) + { + ChildrenRects.Add(new UvRect + { + Position = position, + Size = size, + }); + + Size = new UvMeasure + { + U = position.U + size.U, + V = Math.Max(Size.V, size.V), + }; + } + } + + /// + /// Gets or sets a uniform Horizontal distance (in pixels) between items when is set to Horizontal, + /// or between columns of items when is set to Vertical. + /// + public double HorizontalSpacing + { + get { return (double)GetValue(HorizontalSpacingProperty); } + set { SetValue(HorizontalSpacingProperty, value); } + } + + private bool IsSectionItem(UIElement element) => element is FrameworkElement fe && fe.DataContext is ListItemViewModel item && item.IsSectionOrSeparator; + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty HorizontalSpacingProperty = + DependencyProperty.Register( + nameof(HorizontalSpacing), + typeof(double), + typeof(WrapPanel), + new PropertyMetadata(0d, LayoutPropertyChanged)); + + /// + /// Gets or sets a uniform Vertical distance (in pixels) between items when is set to Vertical, + /// or between rows of items when is set to Horizontal. + /// + public double VerticalSpacing + { + get { return (double)GetValue(VerticalSpacingProperty); } + set { SetValue(VerticalSpacingProperty, value); } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty VerticalSpacingProperty = + DependencyProperty.Register( + nameof(VerticalSpacing), + typeof(double), + typeof(WrapPanel), + new PropertyMetadata(0d, LayoutPropertyChanged)); + + /// + /// Gets or sets the orientation of the WrapPanel. + /// Horizontal means that child controls will be added horizontally until the width of the panel is reached, then a new row is added to add new child controls. + /// Vertical means that children will be added vertically until the height of the panel is reached, then a new column is added. + /// + public Orientation Orientation + { + get { return (Orientation)GetValue(OrientationProperty); } + set { SetValue(OrientationProperty, value); } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty OrientationProperty = + DependencyProperty.Register( + nameof(Orientation), + typeof(Orientation), + typeof(WrapPanel), + new PropertyMetadata(Orientation.Horizontal, LayoutPropertyChanged)); + + /// + /// Gets or sets the distance between the border and its child object. + /// + /// + /// The dimensions of the space between the border and its child as a Thickness value. + /// Thickness is a structure that stores dimension values using pixel measures. + /// + public Thickness Padding + { + get { return (Thickness)GetValue(PaddingProperty); } + set { SetValue(PaddingProperty, value); } + } + + /// + /// Identifies the Padding dependency property. + /// + /// The identifier for the dependency property. + public static readonly DependencyProperty PaddingProperty = + DependencyProperty.Register( + nameof(Padding), + typeof(Thickness), + typeof(WrapPanel), + new PropertyMetadata(default(Thickness), LayoutPropertyChanged)); + + /// + /// Gets or sets a value indicating how to arrange child items + /// + public StretchChild StretchChild + { + get { return (StretchChild)GetValue(StretchChildProperty); } + set { SetValue(StretchChildProperty, value); } + } + + /// + /// Identifies the dependency property. + /// + /// The identifier for the dependency property. + public static readonly DependencyProperty StretchChildProperty = + DependencyProperty.Register( + nameof(StretchChild), + typeof(StretchChild), + typeof(WrapPanel), + new PropertyMetadata(StretchChild.None, LayoutPropertyChanged)); + + /// + /// Identifies the IsFullLine attached dependency property. + /// If true, the child element will occupy the entire width of the panel and force a line break before and after itself. + /// + public static readonly DependencyProperty IsFullLineProperty = + DependencyProperty.RegisterAttached( + "IsFullLine", + typeof(bool), + typeof(WrapPanel), + new PropertyMetadata(false, OnIsFullLineChanged)); + + public static bool GetIsFullLine(DependencyObject obj) + { + return (bool)obj.GetValue(IsFullLineProperty); + } + + public static void SetIsFullLine(DependencyObject obj, bool value) + { + obj.SetValue(IsFullLineProperty, value); + } + + private static void OnIsFullLineChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (FindVisualParentWrapPanel(d) is WrapPanel wp) + { + wp.InvalidateMeasure(); + } + } + + private static WrapPanel? FindVisualParentWrapPanel(DependencyObject child) + { + var parent = Microsoft.UI.Xaml.Media.VisualTreeHelper.GetParent(child); + + while (parent != null) + { + if (parent is WrapPanel wrapPanel) + { + return wrapPanel; + } + + parent = Microsoft.UI.Xaml.Media.VisualTreeHelper.GetParent(parent); + } + + return null; + } + + private static void LayoutPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is WrapPanel wp) + { + wp.InvalidateMeasure(); + wp.InvalidateArrange(); + } + } + + private readonly List _rows = new List(); + + /// + protected override Size MeasureOverride(Size availableSize) + { + var childAvailableSize = new Size( + availableSize.Width - Padding.Left - Padding.Right, + availableSize.Height - Padding.Top - Padding.Bottom); + foreach (var child in Children) + { + child.Measure(childAvailableSize); + } + + var requiredSize = UpdateRows(availableSize); + return requiredSize; + } + + /// + protected override Size ArrangeOverride(Size finalSize) + { + if ((Orientation == Orientation.Horizontal && finalSize.Width < DesiredSize.Width) || + (Orientation == Orientation.Vertical && finalSize.Height < DesiredSize.Height)) + { + // We haven't received our desired size. We need to refresh the rows. + UpdateRows(finalSize); + } + + if (_rows.Count > 0) + { + // Now that we have all the data, we do the actual arrange pass + var childIndex = 0; + foreach (var row in _rows) + { + foreach (var rect in row.ChildrenRects) + { + var child = Children[childIndex++]; + while (child.Visibility == Visibility.Collapsed) + { + // Collapsed children are not added into the rows, + // we skip them. + child = Children[childIndex++]; + } + + var arrangeRect = new UvRect + { + Position = rect.Position, + Size = new UvMeasure { U = rect.Size.U, V = row.Size.V }, + }; + + var finalRect = arrangeRect.ToRect(Orientation); + child.Arrange(finalRect); + } + } + } + + return finalSize; + } + + private Size UpdateRows(Size availableSize) + { + _rows.Clear(); + + var paddingStart = new UvMeasure(Orientation, Padding.Left, Padding.Top); + var paddingEnd = new UvMeasure(Orientation, Padding.Right, Padding.Bottom); + + if (Children.Count == 0) + { + return paddingStart.Add(paddingEnd).ToSize(Orientation); + } + + var parentMeasure = new UvMeasure(Orientation, availableSize.Width, availableSize.Height); + var spacingMeasure = new UvMeasure(Orientation, HorizontalSpacing, VerticalSpacing); + var position = new UvMeasure(Orientation, Padding.Left, Padding.Top); + + var currentRow = new Row(new List(), default); + var finalMeasure = new UvMeasure(Orientation, width: 0.0, height: 0.0); + + void CommitRow() + { + // Only adds if the row has a content + if (currentRow.ChildrenRects.Count > 0) + { + _rows.Add(currentRow); + + position.V += currentRow.Size.V + spacingMeasure.V; + } + + position.U = paddingStart.U; + + currentRow = new Row(new List(), default); + } + + void Arrange(UIElement child, bool isLast = false) + { + if (child.Visibility == Visibility.Collapsed) + { + return; + } + + var isFullLine = IsSectionItem(child); + var desiredMeasure = new UvMeasure(Orientation, child.DesiredSize); + + if (isFullLine) + { + if (currentRow.ChildrenRects.Count > 0) + { + CommitRow(); + } + + // Forces the width to fill all the available space + // (Total width - Padding Left - Padding Right) + desiredMeasure.U = parentMeasure.U - paddingStart.U - paddingEnd.U; + + // Adds the Section Header to the row + currentRow.Add(position, desiredMeasure); + + // Updates the global measures + position.U += desiredMeasure.U + spacingMeasure.U; + finalMeasure.U = Math.Max(finalMeasure.U, position.U); + + CommitRow(); + } + else + { + // Checks if the item can fit in the row + if ((desiredMeasure.U + position.U + paddingEnd.U) > parentMeasure.U) + { + CommitRow(); + } + + if (isLast) + { + desiredMeasure.U = parentMeasure.U - position.U; + } + + currentRow.Add(position, desiredMeasure); + + position.U += desiredMeasure.U + spacingMeasure.U; + finalMeasure.U = Math.Max(finalMeasure.U, position.U); + } + } + + var lastIndex = Children.Count - 1; + for (var i = 0; i < lastIndex; i++) + { + Arrange(Children[i]); + } + + Arrange(Children[lastIndex], StretchChild == StretchChild.Last); + + if (currentRow.ChildrenRects.Count > 0) + { + _rows.Add(currentRow); + } + + if (_rows.Count == 0) + { + return paddingStart.Add(paddingEnd).ToSize(Orientation); + } + + var lastRowRect = _rows.Last().Rect; + finalMeasure.V = lastRowRect.Position.V + lastRowRect.Size.V; + return finalMeasure.Add(paddingEnd).ToSize(Orientation); + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Converters/GridItemTemplateSelector.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Converters/GridItemTemplateSelector.cs index c93470e3e3..f638f3f09e 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Converters/GridItemTemplateSelector.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Converters/GridItemTemplateSelector.cs @@ -18,8 +18,23 @@ internal sealed partial class GridItemTemplateSelector : DataTemplateSelector public DataTemplate? Gallery { get; set; } + public DataTemplate? Section { get; set; } + + public DataTemplate? Separator { get; set; } + protected override DataTemplate? SelectTemplateCore(object item, DependencyObject dependencyObject) { + if (item is ListItemViewModel element && element.IsSectionOrSeparator) + { + if (dependencyObject is UIElement li) + { + li.IsTabStop = false; + li.IsHitTestVisible = false; + } + + return string.IsNullOrWhiteSpace(element.Section) ? Separator : Section; + } + return GridProperties switch { SmallGridPropertiesViewModel => Small, diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Converters/ListItemTemplateSelector.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Converters/ListItemTemplateSelector.cs new file mode 100644 index 0000000000..7fb810f5dc --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Converters/ListItemTemplateSelector.cs @@ -0,0 +1,47 @@ +// 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.CmdPal.Core.ViewModels; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; + +namespace Microsoft.CmdPal.UI; + +public sealed partial class ListItemTemplateSelector : DataTemplateSelector +{ + public DataTemplate? ListItem { get; set; } + + public DataTemplate? Separator { get; set; } + + public DataTemplate? Section { get; set; } + + protected override DataTemplate? SelectTemplateCore(object item, DependencyObject container) + { + DataTemplate? dataTemplate = ListItem; + + if (container is ListViewItem listItem) + { + if (item is ListItemViewModel element) + { + if (container is ListViewItem li && element.IsSectionOrSeparator) + { + li.IsEnabled = false; + li.AllowFocusWhenDisabled = false; + li.AllowFocusOnInteraction = false; + li.IsHitTestVisible = false; + dataTemplate = string.IsNullOrWhiteSpace(element.Section) ? Separator : Section; + } + else + { + listItem.IsEnabled = true; + listItem.AllowFocusWhenDisabled = true; + listItem.AllowFocusOnInteraction = true; + listItem.IsHitTestVisible = true; + } + } + } + + return dataTemplate; + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml index 7cf720198a..859a74eb18 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml @@ -28,6 +28,8 @@ 8