mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-15 19:27:56 +01:00
[CmdPal] Add Sections and Separators for List Pages and Grid Pages (#43952)
<!-- Enter a brief description/summary of your PR here. What does it fix/what does it change/how was it tested (even manually, if necessary)? --> ## Summary of the Pull Request This pull request adds sections and separators to ListPages and GridPages <!-- Please review the items on the PR checklist before submitting--> ## PR Checklist - [x] Closes: #38267 <!-- - [ ] Closes: #yyy (add separate lines for additional resolved issues) --> - [ ] **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 <!-- Provide a more detailed description of the PR, other things fixed, or any additional comments/features here --> ## 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 <img width="792" height="523" alt="Screenshot 2025-11-27 173618" src="https://github.com/user-attachments/assets/f9f9a64d-3ec1-4f7e-922b-997a3a4d074d" /> ### Window Walker <img width="785" height="518" alt="Screenshot 2025-11-27 173728" src="https://github.com/user-attachments/assets/230f647d-210a-4b60-9068-c8fff890d2c9" /> ### Winget <img width="809" height="497" alt="Screenshot 2025-11-27 174006" src="https://github.com/user-attachments/assets/547529c1-7600-4438-8c3e-e872e0327650" /> ### Search files <img width="819" height="536" alt="image" src="https://github.com/user-attachments/assets/e86accc0-3f85-412d-8fb0-914a5479baff" /> ### Grid Pages <img width="804" height="964" alt="Screenshot 2025-11-27 174055" src="https://github.com/user-attachments/assets/a3bba7db-95df-47ec-9cfb-f38775ab960e" /> <!-- Describe how you validated the behavior. Add automated tests wherever possible, but list manual validation steps taken as well --> ## Validation Steps Performed
This commit is contained in:
2
.github/actions/spell-check/expect.txt
vendored
2
.github/actions/spell-check/expect.txt
vendored
@@ -1854,6 +1854,8 @@ uitests
|
|||||||
UITo
|
UITo
|
||||||
ULONGLONG
|
ULONGLONG
|
||||||
ums
|
ums
|
||||||
|
UMax
|
||||||
|
UMin
|
||||||
uncompilable
|
uncompilable
|
||||||
UNCPRIORITY
|
UNCPRIORITY
|
||||||
UNDNAME
|
UNDNAME
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ public partial class ListItemViewModel : CommandItemViewModel
|
|||||||
|
|
||||||
public string Section { get; private set; } = string.Empty;
|
public string Section { get; private set; } = string.Empty;
|
||||||
|
|
||||||
|
public bool IsSectionOrSeparator { get; private set; }
|
||||||
|
|
||||||
public DetailsViewModel? Details { get; private set; }
|
public DetailsViewModel? Details { get; private set; }
|
||||||
|
|
||||||
[MemberNotNullWhen(true, nameof(Details))]
|
[MemberNotNullWhen(true, nameof(Details))]
|
||||||
@@ -82,14 +84,18 @@ public partial class ListItemViewModel : CommandItemViewModel
|
|||||||
}
|
}
|
||||||
|
|
||||||
UpdateTags(li.Tags);
|
UpdateTags(li.Tags);
|
||||||
|
|
||||||
Section = li.Section ?? string.Empty;
|
Section = li.Section ?? string.Empty;
|
||||||
|
IsSectionOrSeparator = IsSeparator(li);
|
||||||
UpdateProperty(nameof(Section));
|
UpdateProperty(nameof(Section), nameof(IsSectionOrSeparator));
|
||||||
|
|
||||||
UpdateAccessibleName();
|
UpdateAccessibleName();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool IsSeparator(IListItem item)
|
||||||
|
{
|
||||||
|
return item.Command is null;
|
||||||
|
}
|
||||||
|
|
||||||
public override void SlowInitializeProperties()
|
public override void SlowInitializeProperties()
|
||||||
{
|
{
|
||||||
base.SlowInitializeProperties();
|
base.SlowInitializeProperties();
|
||||||
@@ -104,8 +110,7 @@ public partial class ListItemViewModel : CommandItemViewModel
|
|||||||
{
|
{
|
||||||
Details = new(extensionDetails, PageContext);
|
Details = new(extensionDetails, PageContext);
|
||||||
Details.InitializeProperties();
|
Details.InitializeProperties();
|
||||||
UpdateProperty(nameof(Details));
|
UpdateProperty(nameof(Details), nameof(HasDetails));
|
||||||
UpdateProperty(nameof(HasDetails));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
AddShowDetailsCommands();
|
AddShowDetailsCommands();
|
||||||
@@ -135,14 +140,18 @@ public partial class ListItemViewModel : CommandItemViewModel
|
|||||||
break;
|
break;
|
||||||
case nameof(model.Section):
|
case nameof(model.Section):
|
||||||
Section = model.Section ?? string.Empty;
|
Section = model.Section ?? string.Empty;
|
||||||
UpdateProperty(nameof(Section));
|
IsSectionOrSeparator = IsSeparator(model);
|
||||||
|
UpdateProperty(nameof(Section), nameof(IsSectionOrSeparator));
|
||||||
break;
|
break;
|
||||||
case nameof(model.Details):
|
case nameof(model.Command):
|
||||||
|
IsSectionOrSeparator = IsSeparator(model);
|
||||||
|
UpdateProperty(nameof(IsSectionOrSeparator));
|
||||||
|
break;
|
||||||
|
case nameof(Details):
|
||||||
var extensionDetails = model.Details;
|
var extensionDetails = model.Details;
|
||||||
Details = extensionDetails is not null ? new(extensionDetails, PageContext) : null;
|
Details = extensionDetails is not null ? new(extensionDetails, PageContext) : null;
|
||||||
Details?.InitializeProperties();
|
Details?.InitializeProperties();
|
||||||
UpdateProperty(nameof(Details));
|
UpdateProperty(nameof(Details), nameof(HasDetails));
|
||||||
UpdateProperty(nameof(HasDetails));
|
|
||||||
UpdateShowDetailsCommand();
|
UpdateShowDetailsCommand();
|
||||||
break;
|
break;
|
||||||
case nameof(model.MoreCommands):
|
case nameof(model.MoreCommands):
|
||||||
@@ -194,8 +203,7 @@ public partial class ListItemViewModel : CommandItemViewModel
|
|||||||
MoreCommands.Add(showDetailsContextItemViewModel);
|
MoreCommands.Add(showDetailsContextItemViewModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateProperty(nameof(MoreCommands));
|
UpdateProperty(nameof(MoreCommands), nameof(AllCommands));
|
||||||
UpdateProperty(nameof(AllCommands));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,8 +235,7 @@ public partial class ListItemViewModel : CommandItemViewModel
|
|||||||
showDetailsContextItemViewModel.SlowInitializeProperties();
|
showDetailsContextItemViewModel.SlowInitializeProperties();
|
||||||
MoreCommands.Add(showDetailsContextItemViewModel);
|
MoreCommands.Add(showDetailsContextItemViewModel);
|
||||||
|
|
||||||
UpdateProperty(nameof(MoreCommands));
|
UpdateProperty(nameof(MoreCommands), nameof(AllCommands));
|
||||||
UpdateProperty(nameof(AllCommands));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,13 +3,14 @@
|
|||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using Microsoft.CmdPal.Core.ViewModels;
|
|
||||||
using Microsoft.CommandPalette.Extensions;
|
using Microsoft.CommandPalette.Extensions;
|
||||||
|
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||||
|
|
||||||
namespace Microsoft.CmdPal.Core.ViewModels;
|
namespace Microsoft.CmdPal.Core.ViewModels;
|
||||||
|
|
||||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
|
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
|
||||||
public partial class SeparatorViewModel() :
|
public partial class SeparatorViewModel() :
|
||||||
|
CommandItem,
|
||||||
IContextItemViewModel,
|
IContextItemViewModel,
|
||||||
IFilterItemViewModel,
|
IFilterItemViewModel,
|
||||||
ISeparatorContextItem,
|
ISeparatorContextItem,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Arranges elements by wrapping them to fit the available space.
|
||||||
|
/// When <see cref="Orientation"/> is set to Orientation.Horizontal, element are arranged in rows until the available width is reached and then to a new row.
|
||||||
|
/// When <see cref="Orientation"/> is set to Orientation.Vertical, element are arranged in columns until the available height is reached.
|
||||||
|
/// </summary>
|
||||||
|
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<UvRect> 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<UvRect> 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),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a uniform Horizontal distance (in pixels) between items when <see cref="Orientation"/> is set to Horizontal,
|
||||||
|
/// or between columns of items when <see cref="Orientation"/> is set to Vertical.
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Identifies the <see cref="HorizontalSpacing"/> dependency property.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly DependencyProperty HorizontalSpacingProperty =
|
||||||
|
DependencyProperty.Register(
|
||||||
|
nameof(HorizontalSpacing),
|
||||||
|
typeof(double),
|
||||||
|
typeof(WrapPanel),
|
||||||
|
new PropertyMetadata(0d, LayoutPropertyChanged));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a uniform Vertical distance (in pixels) between items when <see cref="Orientation"/> is set to Vertical,
|
||||||
|
/// or between rows of items when <see cref="Orientation"/> is set to Horizontal.
|
||||||
|
/// </summary>
|
||||||
|
public double VerticalSpacing
|
||||||
|
{
|
||||||
|
get { return (double)GetValue(VerticalSpacingProperty); }
|
||||||
|
set { SetValue(VerticalSpacingProperty, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Identifies the <see cref="VerticalSpacing"/> dependency property.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly DependencyProperty VerticalSpacingProperty =
|
||||||
|
DependencyProperty.Register(
|
||||||
|
nameof(VerticalSpacing),
|
||||||
|
typeof(double),
|
||||||
|
typeof(WrapPanel),
|
||||||
|
new PropertyMetadata(0d, LayoutPropertyChanged));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
public Orientation Orientation
|
||||||
|
{
|
||||||
|
get { return (Orientation)GetValue(OrientationProperty); }
|
||||||
|
set { SetValue(OrientationProperty, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Identifies the <see cref="Orientation"/> dependency property.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly DependencyProperty OrientationProperty =
|
||||||
|
DependencyProperty.Register(
|
||||||
|
nameof(Orientation),
|
||||||
|
typeof(Orientation),
|
||||||
|
typeof(WrapPanel),
|
||||||
|
new PropertyMetadata(Orientation.Horizontal, LayoutPropertyChanged));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the distance between the border and its child object.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// 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.
|
||||||
|
/// </returns>
|
||||||
|
public Thickness Padding
|
||||||
|
{
|
||||||
|
get { return (Thickness)GetValue(PaddingProperty); }
|
||||||
|
set { SetValue(PaddingProperty, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Identifies the Padding dependency property.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The identifier for the <see cref="Padding"/> dependency property.</returns>
|
||||||
|
public static readonly DependencyProperty PaddingProperty =
|
||||||
|
DependencyProperty.Register(
|
||||||
|
nameof(Padding),
|
||||||
|
typeof(Thickness),
|
||||||
|
typeof(WrapPanel),
|
||||||
|
new PropertyMetadata(default(Thickness), LayoutPropertyChanged));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating how to arrange child items
|
||||||
|
/// </summary>
|
||||||
|
public StretchChild StretchChild
|
||||||
|
{
|
||||||
|
get { return (StretchChild)GetValue(StretchChildProperty); }
|
||||||
|
set { SetValue(StretchChildProperty, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Identifies the <see cref="StretchChild"/> dependency property.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The identifier for the <see cref="StretchChild"/> dependency property.</returns>
|
||||||
|
public static readonly DependencyProperty StretchChildProperty =
|
||||||
|
DependencyProperty.Register(
|
||||||
|
nameof(StretchChild),
|
||||||
|
typeof(StretchChild),
|
||||||
|
typeof(WrapPanel),
|
||||||
|
new PropertyMetadata(StretchChild.None, LayoutPropertyChanged));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
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<Row> _rows = new List<Row>();
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
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<UvRect>(), 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<UvRect>(), 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,8 +18,23 @@ internal sealed partial class GridItemTemplateSelector : DataTemplateSelector
|
|||||||
|
|
||||||
public DataTemplate? Gallery { get; set; }
|
public DataTemplate? Gallery { get; set; }
|
||||||
|
|
||||||
|
public DataTemplate? Section { get; set; }
|
||||||
|
|
||||||
|
public DataTemplate? Separator { get; set; }
|
||||||
|
|
||||||
protected override DataTemplate? SelectTemplateCore(object item, DependencyObject dependencyObject)
|
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
|
return GridProperties switch
|
||||||
{
|
{
|
||||||
SmallGridPropertiesViewModel => Small,
|
SmallGridPropertiesViewModel => Small,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,6 +28,8 @@
|
|||||||
<CornerRadius x:Key="MediumGridViewItemCornerRadius">8</CornerRadius>
|
<CornerRadius x:Key="MediumGridViewItemCornerRadius">8</CornerRadius>
|
||||||
|
|
||||||
<Style x:Key="IconGridViewItemStyle" TargetType="GridViewItem">
|
<Style x:Key="IconGridViewItemStyle" TargetType="GridViewItem">
|
||||||
|
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||||
|
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||||
<Setter Property="Template">
|
<Setter Property="Template">
|
||||||
<Setter.Value>
|
<Setter.Value>
|
||||||
<ControlTemplate TargetType="GridViewItem">
|
<ControlTemplate TargetType="GridViewItem">
|
||||||
@@ -90,6 +92,8 @@
|
|||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style x:Key="GalleryGridViewItemStyle" TargetType="GridViewItem">
|
<Style x:Key="GalleryGridViewItemStyle" TargetType="GridViewItem">
|
||||||
|
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||||
|
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||||
<Setter Property="Template">
|
<Setter Property="Template">
|
||||||
<Setter.Value>
|
<Setter.Value>
|
||||||
<ControlTemplate TargetType="GridViewItem">
|
<ControlTemplate TargetType="GridViewItem">
|
||||||
@@ -168,8 +172,17 @@
|
|||||||
Gallery="{StaticResource GalleryGridItemViewModelTemplate}"
|
Gallery="{StaticResource GalleryGridItemViewModelTemplate}"
|
||||||
GridProperties="{x:Bind ViewModel.GridProperties, Mode=OneWay}"
|
GridProperties="{x:Bind ViewModel.GridProperties, Mode=OneWay}"
|
||||||
Medium="{StaticResource MediumGridItemViewModelTemplate}"
|
Medium="{StaticResource MediumGridItemViewModelTemplate}"
|
||||||
|
Section="{StaticResource ListSectionViewModelTemplate}"
|
||||||
|
Separator="{StaticResource ListSeparatorViewModelTemplate}"
|
||||||
Small="{StaticResource SmallGridItemViewModelTemplate}" />
|
Small="{StaticResource SmallGridItemViewModelTemplate}" />
|
||||||
|
|
||||||
|
<cmdpalUI:ListItemTemplateSelector
|
||||||
|
x:Key="ListItemTemplateSelector"
|
||||||
|
x:DataType="coreViewModels:ListItemViewModel"
|
||||||
|
ListItem="{StaticResource ListItemViewModelTemplate}"
|
||||||
|
Section="{StaticResource ListSectionViewModelTemplate}"
|
||||||
|
Separator="{StaticResource ListSeparatorViewModelTemplate}" />
|
||||||
|
|
||||||
<cmdpalUI:GridItemContainerStyleSelector
|
<cmdpalUI:GridItemContainerStyleSelector
|
||||||
x:Key="GridItemContainerStyleSelector"
|
x:Key="GridItemContainerStyleSelector"
|
||||||
Gallery="{StaticResource GalleryGridViewItemStyle}"
|
Gallery="{StaticResource GalleryGridViewItemStyle}"
|
||||||
@@ -241,12 +254,46 @@
|
|||||||
</Grid>
|
</Grid>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
|
|
||||||
|
<DataTemplate x:Key="ListSeparatorViewModelTemplate" x:DataType="coreViewModels:ListItemViewModel">
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Rectangle
|
||||||
|
Grid.Column="1"
|
||||||
|
Height="1"
|
||||||
|
Margin="0,2,0,2"
|
||||||
|
Fill="{ThemeResource DividerStrokeColorDefaultBrush}" />
|
||||||
|
</Grid>
|
||||||
|
</DataTemplate>
|
||||||
|
|
||||||
|
<DataTemplate x:Key="ListSectionViewModelTemplate" x:DataType="coreViewModels:ListItemViewModel">
|
||||||
|
<Grid
|
||||||
|
Margin="0"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
cpcontrols:WrapPanel.IsFullLine="True"
|
||||||
|
ColumnSpacing="8"
|
||||||
|
IsTabStop="False"
|
||||||
|
IsTapEnabled="True">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBlock
|
||||||
|
Grid.Column="0"
|
||||||
|
Foreground="{ThemeResource TextFillColorDisabled}"
|
||||||
|
Style="{ThemeResource CaptionTextBlockStyle}"
|
||||||
|
Text="{x:Bind Section}" />
|
||||||
|
<Rectangle
|
||||||
|
Grid.Column="1"
|
||||||
|
Height="1"
|
||||||
|
Fill="{ThemeResource DividerStrokeColorDefaultBrush}" />
|
||||||
|
</Grid>
|
||||||
|
</DataTemplate>
|
||||||
|
|
||||||
<!-- Grid item templates for visual grid representation -->
|
<!-- Grid item templates for visual grid representation -->
|
||||||
<DataTemplate x:Key="SmallGridItemViewModelTemplate" x:DataType="coreViewModels:ListItemViewModel">
|
<DataTemplate x:Key="SmallGridItemViewModelTemplate" x:DataType="coreViewModels:ListItemViewModel">
|
||||||
<StackPanel
|
<StackPanel
|
||||||
Width="60"
|
|
||||||
Height="60"
|
|
||||||
Padding="8,16"
|
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
AutomationProperties.Name="{x:Bind Title, Mode=OneWay}"
|
AutomationProperties.Name="{x:Bind Title, Mode=OneWay}"
|
||||||
@@ -265,7 +312,6 @@
|
|||||||
Foreground="{ThemeResource TextFillColorPrimary}"
|
Foreground="{ThemeResource TextFillColorPrimary}"
|
||||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
||||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
|
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
|
||||||
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
|
|
||||||
@@ -399,7 +445,7 @@
|
|||||||
IsDoubleTapEnabled="True"
|
IsDoubleTapEnabled="True"
|
||||||
IsItemClickEnabled="True"
|
IsItemClickEnabled="True"
|
||||||
ItemClick="Items_ItemClick"
|
ItemClick="Items_ItemClick"
|
||||||
ItemTemplate="{StaticResource ListItemViewModelTemplate}"
|
ItemTemplateSelector="{StaticResource ListItemTemplateSelector}"
|
||||||
ItemsSource="{x:Bind ViewModel.FilteredItems, Mode=OneWay}"
|
ItemsSource="{x:Bind ViewModel.FilteredItems, Mode=OneWay}"
|
||||||
RightTapped="Items_RightTapped"
|
RightTapped="Items_RightTapped"
|
||||||
SelectionChanged="Items_SelectionChanged">
|
SelectionChanged="Items_SelectionChanged">
|
||||||
@@ -411,7 +457,7 @@
|
|||||||
<controls:Case Value="True">
|
<controls:Case Value="True">
|
||||||
<GridView
|
<GridView
|
||||||
x:Name="ItemsGrid"
|
x:Name="ItemsGrid"
|
||||||
Padding="8"
|
Padding="16,0"
|
||||||
ContextCanceled="Items_OnContextCanceled"
|
ContextCanceled="Items_OnContextCanceled"
|
||||||
ContextRequested="Items_OnContextRequested"
|
ContextRequested="Items_OnContextRequested"
|
||||||
DoubleTapped="Items_DoubleTapped"
|
DoubleTapped="Items_DoubleTapped"
|
||||||
@@ -423,10 +469,14 @@
|
|||||||
ItemsSource="{x:Bind ViewModel.FilteredItems, Mode=OneWay}"
|
ItemsSource="{x:Bind ViewModel.FilteredItems, Mode=OneWay}"
|
||||||
RightTapped="Items_RightTapped"
|
RightTapped="Items_RightTapped"
|
||||||
SelectionChanged="Items_SelectionChanged">
|
SelectionChanged="Items_SelectionChanged">
|
||||||
|
<GridView.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate>
|
||||||
|
<cpcontrols:WrapPanel HorizontalSpacing="8" Orientation="Horizontal" />
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</GridView.ItemsPanel>
|
||||||
<GridView.ItemContainerTransitions>
|
<GridView.ItemContainerTransitions>
|
||||||
<TransitionCollection />
|
<TransitionCollection />
|
||||||
</GridView.ItemContainerTransitions>
|
</GridView.ItemContainerTransitions>
|
||||||
<GridView.ItemContainerStyle />
|
|
||||||
</GridView>
|
</GridView>
|
||||||
</controls:Case>
|
</controls:Case>
|
||||||
</controls:SwitchPresenter>
|
</controls:SwitchPresenter>
|
||||||
|
|||||||
@@ -76,12 +76,18 @@ public sealed partial class ListPage : Page,
|
|||||||
|
|
||||||
ViewModel = listViewModel;
|
ViewModel = listViewModel;
|
||||||
|
|
||||||
if (e.NavigationMode == NavigationMode.Back
|
if (e.NavigationMode == NavigationMode.Back)
|
||||||
|| (e.NavigationMode == NavigationMode.New && ItemView.Items.Count > 0))
|
|
||||||
{
|
{
|
||||||
// Upon navigating _back_ to this page, immediately select the
|
// Must dispatch the selection to run at a lower priority; otherwise, GetFirstSelectableIndex
|
||||||
// first item in the list
|
// may return an incorrect index because item containers are not yet rendered.
|
||||||
ItemView.SelectedIndex = 0;
|
_ = DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Low, () =>
|
||||||
|
{
|
||||||
|
var firstUsefulIndex = GetFirstSelectableIndex();
|
||||||
|
if (firstUsefulIndex != -1)
|
||||||
|
{
|
||||||
|
ItemView.SelectedIndex = firstUsefulIndex;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegisterAll isn't AOT compatible
|
// RegisterAll isn't AOT compatible
|
||||||
@@ -128,6 +134,29 @@ public sealed partial class ListPage : Page,
|
|||||||
GC.Collect();
|
GC.Collect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Finds the index of the first item in the list that is not a separator.
|
||||||
|
/// Returns -1 if the list is empty or only contains separators.
|
||||||
|
/// </summary>
|
||||||
|
private int GetFirstSelectableIndex()
|
||||||
|
{
|
||||||
|
var items = ItemView.Items;
|
||||||
|
if (items is null || items.Count == 0)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < items.Count; i++)
|
||||||
|
{
|
||||||
|
if (!IsSeparator(items[i]))
|
||||||
|
{
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "VS is too aggressive at pruning methods bound in XAML")]
|
[System.Diagnostics.CodeAnalysis.SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "VS is too aggressive at pruning methods bound in XAML")]
|
||||||
private void Items_ItemClick(object sender, ItemClickEventArgs e)
|
private void Items_ItemClick(object sender, ItemClickEventArgs e)
|
||||||
{
|
{
|
||||||
@@ -183,19 +212,33 @@ public sealed partial class ListPage : Page,
|
|||||||
// here, then in Page_ItemsUpdated trying to select that cached item if
|
// here, then in Page_ItemsUpdated trying to select that cached item if
|
||||||
// it's in the list (otherwise, clear the cache), but that seems
|
// it's in the list (otherwise, clear the cache), but that seems
|
||||||
// aggressively BODGY for something that mostly just works today.
|
// aggressively BODGY for something that mostly just works today.
|
||||||
if (ItemView.SelectedItem is not null)
|
if (ItemView.SelectedItem is not null && !IsSeparator(ItemView.SelectedItem))
|
||||||
{
|
{
|
||||||
ItemView.ScrollIntoView(ItemView.SelectedItem);
|
var items = ItemView.Items;
|
||||||
|
var firstUsefulIndex = GetFirstSelectableIndex();
|
||||||
|
var shouldScroll = false;
|
||||||
|
|
||||||
|
if (e.RemovedItems.Count > 0)
|
||||||
|
{
|
||||||
|
shouldScroll = true;
|
||||||
|
}
|
||||||
|
else if (ItemView.SelectedIndex > firstUsefulIndex)
|
||||||
|
{
|
||||||
|
shouldScroll = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldScroll)
|
||||||
|
{
|
||||||
|
ItemView.ScrollIntoView(ItemView.SelectedItem);
|
||||||
|
}
|
||||||
|
|
||||||
// Automation notification for screen readers
|
// Automation notification for screen readers
|
||||||
var listViewPeer = Microsoft.UI.Xaml.Automation.Peers.ListViewAutomationPeer.CreatePeerForElement(ItemView);
|
var listViewPeer = Microsoft.UI.Xaml.Automation.Peers.ListViewAutomationPeer.CreatePeerForElement(ItemView);
|
||||||
if (listViewPeer is not null && li is not null)
|
if (listViewPeer is not null && li is not null)
|
||||||
{
|
{
|
||||||
var notificationText = li.Title;
|
|
||||||
|
|
||||||
UIHelper.AnnounceActionForAccessibility(
|
UIHelper.AnnounceActionForAccessibility(
|
||||||
ItemsList,
|
ItemsList,
|
||||||
notificationText,
|
li.Title,
|
||||||
"CommandPaletteSelectedItemChanged");
|
"CommandPaletteSelectedItemChanged");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -271,14 +314,7 @@ public sealed partial class ListPage : Page,
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
// For list views, use simple linear navigation
|
// For list views, use simple linear navigation
|
||||||
if (ItemView.SelectedIndex < ItemView.Items.Count - 1)
|
NavigateDown();
|
||||||
{
|
|
||||||
ItemView.SelectedIndex++;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ItemView.SelectedIndex = 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -291,15 +327,7 @@ public sealed partial class ListPage : Page,
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// For list views, use simple linear navigation
|
NavigateUp();
|
||||||
if (ItemView.SelectedIndex > 0)
|
|
||||||
{
|
|
||||||
ItemView.SelectedIndex--;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ItemView.SelectedIndex = ItemView.Items.Count - 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,7 +394,10 @@ public sealed partial class ListPage : Page,
|
|||||||
if (indexes.Value.CurrentIndex != indexes.Value.TargetIndex)
|
if (indexes.Value.CurrentIndex != indexes.Value.TargetIndex)
|
||||||
{
|
{
|
||||||
ItemView.SelectedIndex = indexes.Value.TargetIndex;
|
ItemView.SelectedIndex = indexes.Value.TargetIndex;
|
||||||
ItemView.ScrollIntoView(ItemView.SelectedItem);
|
if (ItemView.SelectedItem is not null)
|
||||||
|
{
|
||||||
|
ItemView.ScrollIntoView(ItemView.SelectedItem);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -381,7 +412,10 @@ public sealed partial class ListPage : Page,
|
|||||||
if (indexes.Value.CurrentIndex != indexes.Value.TargetIndex)
|
if (indexes.Value.CurrentIndex != indexes.Value.TargetIndex)
|
||||||
{
|
{
|
||||||
ItemView.SelectedIndex = indexes.Value.TargetIndex;
|
ItemView.SelectedIndex = indexes.Value.TargetIndex;
|
||||||
ItemView.ScrollIntoView(ItemView.SelectedItem);
|
if (ItemView.SelectedItem is not null)
|
||||||
|
{
|
||||||
|
ItemView.ScrollIntoView(ItemView.SelectedItem);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -524,17 +558,65 @@ public sealed partial class ListPage : Page,
|
|||||||
// ItemView_SelectionChanged again to give us another chance to change
|
// ItemView_SelectionChanged again to give us another chance to change
|
||||||
// the selection from null -> something. Better to just update the
|
// the selection from null -> something. Better to just update the
|
||||||
// selection once, at the end of all the updating.
|
// selection once, at the end of all the updating.
|
||||||
if (ItemView.SelectedItem is null)
|
// The selection logic must be deferred to the DispatcherQueue
|
||||||
|
// to ensure the UI has processed the updated ItemsSource binding,
|
||||||
|
// preventing ItemView.Items from appearing empty/null immediately after update.
|
||||||
|
_ = DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Low, () =>
|
||||||
{
|
{
|
||||||
ItemView.SelectedIndex = 0;
|
var items = ItemView.Items;
|
||||||
}
|
|
||||||
|
|
||||||
// Always reset the selected item when the top-level list page changes
|
// If the list is null or empty, clears the selection and return
|
||||||
// its items
|
if (items is null || items.Count == 0)
|
||||||
if (!sender.IsNested)
|
{
|
||||||
{
|
ItemView.SelectedIndex = -1;
|
||||||
ItemView.SelectedIndex = 0;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Finds the first item that is not a separator
|
||||||
|
var firstUsefulIndex = GetFirstSelectableIndex();
|
||||||
|
|
||||||
|
// If there is only separators in the list, don't select anything.
|
||||||
|
if (firstUsefulIndex == -1)
|
||||||
|
{
|
||||||
|
ItemView.SelectedIndex = -1;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var shouldUpdateSelection = false;
|
||||||
|
|
||||||
|
// If it's a top level list update we force the reset to the top useful item
|
||||||
|
if (!sender.IsNested)
|
||||||
|
{
|
||||||
|
shouldUpdateSelection = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No current selection or current selection is null
|
||||||
|
else if (ItemView.SelectedItem is null)
|
||||||
|
{
|
||||||
|
shouldUpdateSelection = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The current selected item is a separator
|
||||||
|
else if (IsSeparator(ItemView.SelectedItem))
|
||||||
|
{
|
||||||
|
shouldUpdateSelection = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The selected item does not exist in the new list
|
||||||
|
else if (!items.Contains(ItemView.SelectedItem))
|
||||||
|
{
|
||||||
|
shouldUpdateSelection = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldUpdateSelection)
|
||||||
|
{
|
||||||
|
if (firstUsefulIndex != -1)
|
||||||
|
{
|
||||||
|
ItemView.SelectedIndex = firstUsefulIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ViewModel_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
private void ViewModel_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||||
@@ -604,6 +686,11 @@ public sealed partial class ListPage : Page,
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (IsSeparator(ItemView.Items[i]))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (ItemView.ContainerFromIndex(i) is FrameworkElement c && c.ActualWidth > 0 && c.ActualHeight > 0)
|
if (ItemView.ContainerFromIndex(i) is FrameworkElement c && c.ActualWidth > 0 && c.ActualHeight > 0)
|
||||||
{
|
{
|
||||||
var p = c.TransformToVisual(ItemView).TransformPoint(new Point(0, 0));
|
var p = c.TransformToVisual(ItemView).TransformPoint(new Point(0, 0));
|
||||||
@@ -764,6 +851,102 @@ public sealed partial class ListPage : Page,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Code stealed from <see cref="Controls.ContextMenu.NavigateUp"/>
|
||||||
|
/// </summary>
|
||||||
|
private void NavigateUp()
|
||||||
|
{
|
||||||
|
var newIndex = ItemView.SelectedIndex;
|
||||||
|
|
||||||
|
if (ItemView.SelectedIndex > 0)
|
||||||
|
{
|
||||||
|
newIndex--;
|
||||||
|
|
||||||
|
while (
|
||||||
|
newIndex >= 0 &&
|
||||||
|
IsSeparator(ItemView.Items[newIndex]) &&
|
||||||
|
newIndex != ItemView.SelectedIndex)
|
||||||
|
{
|
||||||
|
newIndex--;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newIndex < 0)
|
||||||
|
{
|
||||||
|
newIndex = ItemView.Items.Count - 1;
|
||||||
|
|
||||||
|
while (
|
||||||
|
newIndex >= 0 &&
|
||||||
|
IsSeparator(ItemView.Items[newIndex]) &&
|
||||||
|
newIndex != ItemView.SelectedIndex)
|
||||||
|
{
|
||||||
|
newIndex--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
newIndex = ItemView.Items.Count - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
ItemView.SelectedIndex = newIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Code stealed from <see cref="Controls.ContextMenu.NavigateDown"/>
|
||||||
|
/// </summary>
|
||||||
|
private void NavigateDown()
|
||||||
|
{
|
||||||
|
var newIndex = ItemView.SelectedIndex;
|
||||||
|
|
||||||
|
if (ItemView.SelectedIndex == ItemView.Items.Count - 1)
|
||||||
|
{
|
||||||
|
newIndex = 0;
|
||||||
|
while (
|
||||||
|
newIndex < ItemView.Items.Count &&
|
||||||
|
IsSeparator(ItemView.Items[newIndex]))
|
||||||
|
{
|
||||||
|
newIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newIndex >= ItemView.Items.Count)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
newIndex++;
|
||||||
|
|
||||||
|
while (
|
||||||
|
newIndex < ItemView.Items.Count &&
|
||||||
|
IsSeparator(ItemView.Items[newIndex]) &&
|
||||||
|
newIndex != ItemView.SelectedIndex)
|
||||||
|
{
|
||||||
|
newIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newIndex >= ItemView.Items.Count)
|
||||||
|
{
|
||||||
|
newIndex = 0;
|
||||||
|
|
||||||
|
while (
|
||||||
|
newIndex < ItemView.Items.Count &&
|
||||||
|
IsSeparator(ItemView.Items[newIndex]) &&
|
||||||
|
newIndex != ItemView.SelectedIndex)
|
||||||
|
{
|
||||||
|
newIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ItemView.SelectedIndex = newIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Code stealed from <see cref="Controls.ContextMenu.IsSeparator(object)"/>
|
||||||
|
/// </summary>
|
||||||
|
private bool IsSeparator(object? item) => item is ListItemViewModel li && li.IsSectionOrSeparator;
|
||||||
|
|
||||||
private enum InputSource
|
private enum InputSource
|
||||||
{
|
{
|
||||||
None,
|
None,
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
// 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.CommandPalette.Extensions;
|
||||||
|
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||||
|
|
||||||
|
namespace SamplePagesExtension.Pages.SectionsPages;
|
||||||
|
|
||||||
|
internal sealed partial class SampleListPageWithSections : ListPage
|
||||||
|
{
|
||||||
|
public SampleListPageWithSections()
|
||||||
|
{
|
||||||
|
Icon = new IconInfo("\uE7C5");
|
||||||
|
Name = "Sample Gallery List Page";
|
||||||
|
}
|
||||||
|
|
||||||
|
public SampleListPageWithSections(IGridProperties gridProperties)
|
||||||
|
{
|
||||||
|
Icon = new IconInfo("\uE7C5");
|
||||||
|
Name = "Sample Gallery List Page";
|
||||||
|
GridProperties = gridProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override IListItem[] GetItems()
|
||||||
|
{
|
||||||
|
var sectionList = new Section("This is a section list", [
|
||||||
|
new ListItem(new NoOpCommand())
|
||||||
|
{
|
||||||
|
Title = "Sample Title",
|
||||||
|
Subtitle = "I don't do anything",
|
||||||
|
Icon = IconHelpers.FromRelativePath("Assets/Images/RedRectangle.png"),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
var anotherSectionList = new Section("This is another section list", [
|
||||||
|
new ListItem(new NoOpCommand())
|
||||||
|
{
|
||||||
|
Title = "Another Title",
|
||||||
|
Subtitle = "I don't do anything",
|
||||||
|
Icon = IconHelpers.FromRelativePath("Assets/Images/Space.png"),
|
||||||
|
},
|
||||||
|
new ListItem(new NoOpCommand())
|
||||||
|
{
|
||||||
|
Title = "More Titles",
|
||||||
|
Subtitle = "I don't do anything",
|
||||||
|
Icon = IconHelpers.FromRelativePath("Assets/Images/Swirls.png"),
|
||||||
|
},
|
||||||
|
new ListItem(new NoOpCommand())
|
||||||
|
{
|
||||||
|
Title = "Stop With The Titles",
|
||||||
|
Subtitle = "I don't do anything",
|
||||||
|
Icon = IconHelpers.FromRelativePath("Assets/Images/Win-Digital.png"),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
var yesTheresAnother = new Section("There's another", [
|
||||||
|
new ListItem(new NoOpCommand())
|
||||||
|
{
|
||||||
|
Title = "Sample Title",
|
||||||
|
Subtitle = "I don't do anything",
|
||||||
|
Icon = IconHelpers.FromRelativePath("Assets/Images/RedRectangle.png"),
|
||||||
|
},
|
||||||
|
new ListItem(new NoOpCommand())
|
||||||
|
{
|
||||||
|
Title = "Another Title",
|
||||||
|
Subtitle = "I don't do anything",
|
||||||
|
Icon = IconHelpers.FromRelativePath("Assets/Images/Swirls.png"),
|
||||||
|
},
|
||||||
|
new ListItem(new NoOpCommand())
|
||||||
|
{
|
||||||
|
Title = "More Titles",
|
||||||
|
Subtitle = "I don't do anything",
|
||||||
|
Icon = IconHelpers.FromRelativePath("Assets/Images/Win-Digital.png"),
|
||||||
|
},
|
||||||
|
new ListItem(new NoOpCommand())
|
||||||
|
{
|
||||||
|
Title = "Stop With The Titles",
|
||||||
|
Subtitle = "I don't do anything",
|
||||||
|
Icon = IconHelpers.FromRelativePath("Assets/Images/RedRectangle.png"),
|
||||||
|
},
|
||||||
|
new ListItem(new NoOpCommand())
|
||||||
|
{
|
||||||
|
Title = "Another Title",
|
||||||
|
Subtitle = "I don't do anything",
|
||||||
|
Icon = IconHelpers.FromRelativePath("Assets/Images/Space.png"),
|
||||||
|
},
|
||||||
|
new ListItem(new NoOpCommand())
|
||||||
|
{
|
||||||
|
Title = "More Titles",
|
||||||
|
Subtitle = "I don't do anything",
|
||||||
|
Icon = IconHelpers.FromRelativePath("Assets/Images/Swirls.png"),
|
||||||
|
},
|
||||||
|
new ListItem(new NoOpCommand())
|
||||||
|
{
|
||||||
|
Title = "Stop With The Titles",
|
||||||
|
Subtitle = "I don't do anything",
|
||||||
|
Icon = IconHelpers.FromRelativePath("Assets/Images/Win-Digital.png"),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
return [
|
||||||
|
..sectionList,
|
||||||
|
..anotherSectionList,
|
||||||
|
new Separator(),
|
||||||
|
new ListItem(new NoOpCommand())
|
||||||
|
{
|
||||||
|
Title = "Separators also work",
|
||||||
|
Subtitle = "But I still don't do anything",
|
||||||
|
Icon = IconHelpers.FromRelativePath("Assets/Images/Win-Digital.png"),
|
||||||
|
},
|
||||||
|
..yesTheresAnother
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
// 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.CommandPalette.Extensions;
|
||||||
|
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||||
|
using SamplePagesExtension.Pages.SectionsPages;
|
||||||
|
|
||||||
|
namespace SamplePagesExtension.Pages;
|
||||||
|
|
||||||
|
internal sealed partial class SectionsIndexPage : ListPage
|
||||||
|
{
|
||||||
|
public SectionsIndexPage()
|
||||||
|
{
|
||||||
|
Name = "Sections Index Page";
|
||||||
|
Icon = new IconInfo("\uF168");
|
||||||
|
}
|
||||||
|
|
||||||
|
public override IListItem[] GetItems()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
new ListItem(new SampleListPageWithSections())
|
||||||
|
{
|
||||||
|
Title = "A list page with sections",
|
||||||
|
},
|
||||||
|
new ListItem(new SampleListPageWithSections(new SmallGridLayout()))
|
||||||
|
{
|
||||||
|
Title = "A small grid page with sections",
|
||||||
|
},
|
||||||
|
new ListItem(new SampleListPageWithSections(new MediumGridLayout()))
|
||||||
|
{
|
||||||
|
Title = "A medium grid page with sections",
|
||||||
|
},
|
||||||
|
new ListItem(new SampleListPageWithSections(new GalleryGridLayout()))
|
||||||
|
{
|
||||||
|
Title = "A Gallery grid page with sections",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,6 +24,11 @@ public partial class SamplesListPage : ListPage
|
|||||||
Title = "List Page With Details",
|
Title = "List Page With Details",
|
||||||
Subtitle = "A list of items, each with additional details to display",
|
Subtitle = "A list of items, each with additional details to display",
|
||||||
},
|
},
|
||||||
|
new ListItem(new SectionsIndexPage())
|
||||||
|
{
|
||||||
|
Title = "List Pages With Sections",
|
||||||
|
Subtitle = "A list of items, with sections header",
|
||||||
|
},
|
||||||
new ListItem(new SampleUpdatingItemsPage())
|
new ListItem(new SampleUpdatingItemsPage())
|
||||||
{
|
{
|
||||||
Title = "List page with items that change",
|
Title = "List page with items that change",
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
// 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;
|
||||||
|
|
||||||
|
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||||
|
|
||||||
|
public sealed partial class Section : IEnumerable<IListItem>
|
||||||
|
{
|
||||||
|
public IListItem[] Items { get; set; } = [];
|
||||||
|
|
||||||
|
public string SectionTitle { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
private Separator CreateSectionListItem()
|
||||||
|
{
|
||||||
|
return new Separator(SectionTitle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Section(string sectionName, IListItem[] items)
|
||||||
|
{
|
||||||
|
SectionTitle = sectionName;
|
||||||
|
var listItems = items.ToList();
|
||||||
|
|
||||||
|
if (listItems.Count > 0)
|
||||||
|
{
|
||||||
|
listItems.Insert(0, CreateSectionListItem());
|
||||||
|
Items = [.. listItems];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Section()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerator<IListItem> GetEnumerator() => Items.ToList().GetEnumerator();
|
||||||
|
|
||||||
|
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||||
|
}
|
||||||
@@ -4,6 +4,40 @@
|
|||||||
|
|
||||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||||
|
|
||||||
public partial class Separator : ISeparatorContextItem, ISeparatorFilterItem
|
public partial class Separator : IListItem, ISeparatorContextItem, ISeparatorFilterItem
|
||||||
{
|
{
|
||||||
|
public Separator(string? title = "")
|
||||||
|
: base()
|
||||||
|
{
|
||||||
|
Section = title ?? string.Empty;
|
||||||
|
Command = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IDetails? Details => null;
|
||||||
|
|
||||||
|
public string? Section { get; private set; }
|
||||||
|
|
||||||
|
public ITag[]? Tags => null;
|
||||||
|
|
||||||
|
public string? TextToSuggest => null;
|
||||||
|
|
||||||
|
public ICommand? Command { get; private set; }
|
||||||
|
|
||||||
|
public IIconInfo? Icon => null;
|
||||||
|
|
||||||
|
public IContextItem[]? MoreCommands => null;
|
||||||
|
|
||||||
|
public string? Subtitle => null;
|
||||||
|
|
||||||
|
public string? Title
|
||||||
|
{
|
||||||
|
get => Section;
|
||||||
|
set => Section = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public event Windows.Foundation.TypedEventHandler<object, IPropChangedEventArgs>? PropChanged
|
||||||
|
{
|
||||||
|
add { }
|
||||||
|
remove { }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user