CmdPal: Fix grid views (#43991)

## Summary of the Pull Request

This PR fixes the crash due to binding to a trimmed property. For this
it converts runtime bindings on GridView to use `{x:Bind}` so this issue
can't happen in the future.

- Fixes a crash related to the `Visibility` property in gallery/grid
views when trimmed during AOT builds.
- Fixes ShowTitle and ShowSubtitle properties, they are now taken into
account in a view.
- Improves UI layout, removes some margins and maches the corner radius
of the item contaienr with the item content in the gallery view.
- Refactores gallery and grid views to move logic from the view to the
view model so we can x:Bind to them.
- Replaces `{Binding}` with `{x:Bind}` to improve performance and enable
compile-time binding validation.
- Properties related to grids are splatted on to the common
`IGridPropertiesViewModel` interface. Subclassing would add extra
overhead without substential benefit.
- Adds new samples to showcase various grid view configurations.

## Pictures? Pictures!

A) Gallery view (with title and subtitle)
<img width="909" height="583" alt="image"
src="https://github.com/user-attachments/assets/b807e7a8-412f-4817-8121-e3470c49e0c0"
/>

B) Gallery view (only title)
<img width="903" height="582" alt="image"
src="https://github.com/user-attachments/assets/b619d63f-04d0-42f2-9207-de256dc5e481"
/>

C) Gallery view (no title or subtitle)
<img width="900" height="583" alt="image"
src="https://github.com/user-attachments/assets/c48cd1fc-8f51-40c1-8bce-607916e9f742"
/>

D) Small icons
<img width="907" height="582" alt="image"
src="https://github.com/user-attachments/assets/8327da0a-fa45-443f-b52c-f0f1edd7b861"
/>

E) Medium icons (with labels)
<img width="914" height="588" alt="image"
src="https://github.com/user-attachments/assets/dee9fab1-54e8-45f8-96d7-502b121a6ac2"
/>

F) Medium icons (no labels)
<img width="915" height="588" alt="image"
src="https://github.com/user-attachments/assets/a32e8af2-6cb1-4106-91db-ca396253c0a3"
/>


<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] Closes: #43973
<!-- - [ ] 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

<!-- 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:
Jiří Polášek
2025-12-01 02:32:30 +01:00
committed by GitHub
parent 1ba5a258e9
commit 4d3c223402
13 changed files with 404 additions and 115 deletions

View File

@@ -11,15 +11,15 @@ public class GalleryGridPropertiesViewModel : IGridPropertiesViewModel
{ {
private readonly ExtensionObject<IGalleryGridLayout> _model; private readonly ExtensionObject<IGalleryGridLayout> _model;
public bool ShowTitle { get; private set; }
public bool ShowSubtitle { get; private set; }
public GalleryGridPropertiesViewModel(IGalleryGridLayout galleryGridLayout) public GalleryGridPropertiesViewModel(IGalleryGridLayout galleryGridLayout)
{ {
_model = new(galleryGridLayout); _model = new(galleryGridLayout);
} }
public bool ShowTitle { get; set; }
public bool ShowSubtitle { get; set; }
public void InitializeProperties() public void InitializeProperties()
{ {
var model = _model.Unsafe; var model = _model.Unsafe;

View File

@@ -6,5 +6,9 @@ namespace Microsoft.CmdPal.Core.ViewModels;
public interface IGridPropertiesViewModel public interface IGridPropertiesViewModel
{ {
bool ShowTitle { get; }
bool ShowSubtitle { get; }
void InitializeProperties(); void InitializeProperties();
} }

View File

@@ -10,10 +10,9 @@ using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Core.ViewModels; namespace Microsoft.CmdPal.Core.ViewModels;
public partial class ListItemViewModel(IListItem model, WeakReference<IPageContext> context) public partial class ListItemViewModel : CommandItemViewModel
: CommandItemViewModel(new(model), context)
{ {
public new ExtensionObject<IListItem> Model { get; } = new(model); public new ExtensionObject<IListItem> Model { get; }
public List<TagViewModel>? Tags { get; set; } public List<TagViewModel>? Tags { get; set; }
@@ -32,6 +31,40 @@ public partial class ListItemViewModel(IListItem model, WeakReference<IPageConte
public string AccessibleName { get; private set; } = string.Empty; public string AccessibleName { get; private set; } = string.Empty;
public bool ShowTitle { get; private set; }
public bool ShowSubtitle { get; private set; }
public bool LayoutShowsTitle
{
get;
set
{
if (SetProperty(ref field, value))
{
UpdateShowsTitle();
}
}
}
public bool LayoutShowsSubtitle
{
get;
set
{
if (SetProperty(ref field, value))
{
UpdateShowsSubtitle();
}
}
}
public ListItemViewModel(IListItem model, WeakReference<IPageContext> context)
: base(new(model), context)
{
Model = new ExtensionObject<IListItem>(model);
}
public override void InitializeProperties() public override void InitializeProperties()
{ {
if (IsInitialized) if (IsInitialized)
@@ -93,16 +126,18 @@ public partial class ListItemViewModel(IListItem model, WeakReference<IPageConte
switch (propertyName) switch (propertyName)
{ {
case nameof(Tags): case nameof(model.Tags):
UpdateTags(model.Tags); UpdateTags(model.Tags);
break; break;
case nameof(TextToSuggest): case nameof(model.TextToSuggest):
this.TextToSuggest = model.TextToSuggest ?? string.Empty; TextToSuggest = model.TextToSuggest ?? string.Empty;
UpdateProperty(nameof(TextToSuggest));
break; break;
case nameof(Section): case nameof(model.Section):
this.Section = model.Section ?? string.Empty; Section = model.Section ?? string.Empty;
UpdateProperty(nameof(Section));
break; break;
case nameof(Details): case nameof(model.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();
@@ -110,16 +145,24 @@ public partial class ListItemViewModel(IListItem model, WeakReference<IPageConte
UpdateProperty(nameof(HasDetails)); UpdateProperty(nameof(HasDetails));
UpdateShowDetailsCommand(); UpdateShowDetailsCommand();
break; break;
case nameof(MoreCommands): case nameof(model.MoreCommands):
UpdateProperty(nameof(MoreCommands));
AddShowDetailsCommands(); AddShowDetailsCommands();
break; break;
case nameof(Title): case nameof(model.Title):
case nameof(Subtitle): UpdateProperty(nameof(Title));
UpdateShowsTitle();
UpdateAccessibleName(); UpdateAccessibleName();
break; break;
case nameof(model.Subtitle):
UpdateProperty(nameof(Subtitle));
UpdateShowsSubtitle();
UpdateAccessibleName();
break;
default:
UpdateProperty(propertyName);
break;
} }
UpdateProperty(propertyName);
} }
// TODO: Do we want filters to match descriptions and other properties? Tags, etc... Yes? // TODO: Do we want filters to match descriptions and other properties? Tags, etc... Yes?
@@ -206,11 +249,32 @@ public partial class ListItemViewModel(IListItem model, WeakReference<IPageConte
// many COM exception issues. // many COM exception issues.
Tags = [.. newTags]; Tags = [.. newTags];
UpdateProperty(nameof(Tags)); // We're already in UI thread, so just raise the events
UpdateProperty(nameof(HasTags)); OnPropertyChanged(nameof(Tags));
OnPropertyChanged(nameof(HasTags));
}); });
} }
private void UpdateShowsTitle()
{
var oldShowTitle = ShowTitle;
ShowTitle = LayoutShowsTitle;
if (oldShowTitle != ShowTitle)
{
UpdateProperty(nameof(ShowTitle));
}
}
private void UpdateShowsSubtitle()
{
var oldShowSubtitle = ShowSubtitle;
ShowSubtitle = LayoutShowsSubtitle && !string.IsNullOrWhiteSpace(Subtitle);
if (oldShowSubtitle != ShowSubtitle)
{
UpdateProperty(nameof(ShowSubtitle));
}
}
protected override void UnsafeCleanup() protected override void UnsafeCleanup()
{ {
base.UnsafeCleanup(); base.UnsafeCleanup();

View File

@@ -24,8 +24,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
// Observable from MVVM Toolkit will auto create public properties that use INotifyPropertyChange change // Observable from MVVM Toolkit will auto create public properties that use INotifyPropertyChange change
// https://learn.microsoft.com/dotnet/communitytoolkit/mvvm/observablegroupedcollections for grouping support // https://learn.microsoft.com/dotnet/communitytoolkit/mvvm/observablegroupedcollections for grouping support
[ObservableProperty] public ObservableCollection<ListItemViewModel> FilteredItems { get; } = [];
public partial ObservableCollection<ListItemViewModel> FilteredItems { get; set; } = [];
public FiltersViewModel? Filters { get; set; } public FiltersViewModel? Filters { get; set; }
@@ -224,6 +223,8 @@ public partial class ListViewModel : PageViewModel, IDisposable
// TODO we can probably further optimize this by also keeping a // TODO we can probably further optimize this by also keeping a
// HashSet of every ExtensionObject we currently have, and only // HashSet of every ExtensionObject we currently have, and only
// building new viewmodels for the ones we haven't already built. // building new viewmodels for the ones we haven't already built.
var showsTitle = GridProperties?.ShowTitle ?? true;
var showsSubtitle = GridProperties?.ShowSubtitle ?? true;
foreach (var item in newItems) foreach (var item in newItems)
{ {
// Check for cancellation during item processing // Check for cancellation during item processing
@@ -237,6 +238,8 @@ public partial class ListViewModel : PageViewModel, IDisposable
// If an item fails to load, silently ignore it. // If an item fails to load, silently ignore it.
if (viewModel.SafeFastInit()) if (viewModel.SafeFastInit())
{ {
viewModel.LayoutShowsTitle = showsTitle;
viewModel.LayoutShowsSubtitle = showsSubtitle;
newViewModels.Add(viewModel); newViewModels.Add(viewModel);
} }
} }
@@ -583,6 +586,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
GridProperties = LoadGridPropertiesViewModel(model.GridProperties); GridProperties = LoadGridPropertiesViewModel(model.GridProperties);
GridProperties?.InitializeProperties(); GridProperties?.InitializeProperties();
UpdateProperty(nameof(GridProperties)); UpdateProperty(nameof(GridProperties));
ApplyLayoutToItems();
ShowDetails = model.ShowDetails; ShowDetails = model.ShowDetails;
UpdateProperty(nameof(ShowDetails)); UpdateProperty(nameof(ShowDetails));
@@ -608,22 +612,15 @@ public partial class ListViewModel : PageViewModel, IDisposable
model.ItemsChanged += Model_ItemsChanged; model.ItemsChanged += Model_ItemsChanged;
} }
private IGridPropertiesViewModel? LoadGridPropertiesViewModel(IGridProperties? gridProperties) private static IGridPropertiesViewModel? LoadGridPropertiesViewModel(IGridProperties? gridProperties)
{ {
if (gridProperties is IMediumGridLayout mediumGridLayout) return gridProperties switch
{ {
return new MediumGridPropertiesViewModel(mediumGridLayout); IMediumGridLayout mediumGridLayout => new MediumGridPropertiesViewModel(mediumGridLayout),
} IGalleryGridLayout galleryGridLayout => new GalleryGridPropertiesViewModel(galleryGridLayout),
else if (gridProperties is IGalleryGridLayout galleryGridLayout) ISmallGridLayout smallGridLayout => new SmallGridPropertiesViewModel(smallGridLayout),
{ _ => null,
return new GalleryGridPropertiesViewModel(galleryGridLayout); };
}
else if (gridProperties is ISmallGridLayout smallGridLayout)
{
return new SmallGridPropertiesViewModel(smallGridLayout);
}
return null;
} }
public void LoadMoreIfNeeded() public void LoadMoreIfNeeded()
@@ -685,6 +682,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
GridProperties = LoadGridPropertiesViewModel(model.GridProperties); GridProperties = LoadGridPropertiesViewModel(model.GridProperties);
GridProperties?.InitializeProperties(); GridProperties?.InitializeProperties();
UpdateProperty(nameof(IsGridView)); UpdateProperty(nameof(IsGridView));
ApplyLayoutToItems();
break; break;
case nameof(ShowDetails): case nameof(ShowDetails):
ShowDetails = model.ShowDetails; ShowDetails = model.ShowDetails;
@@ -730,6 +728,21 @@ public partial class ListViewModel : PageViewModel, IDisposable
}); });
} }
private void ApplyLayoutToItems()
{
lock (_listLock)
{
var showsTitle = GridProperties?.ShowTitle ?? true;
var showsSubtitle = GridProperties?.ShowSubtitle ?? true;
foreach (var item in Items)
{
item.LayoutShowsTitle = showsTitle;
item.LayoutShowsSubtitle = showsSubtitle;
}
}
}
public void Dispose() public void Dispose()
{ {
GC.SuppressFinalize(this); GC.SuppressFinalize(this);

View File

@@ -11,13 +11,15 @@ public class MediumGridPropertiesViewModel : IGridPropertiesViewModel
{ {
private readonly ExtensionObject<IMediumGridLayout> _model; private readonly ExtensionObject<IMediumGridLayout> _model;
public bool ShowTitle { get; private set; }
public bool ShowSubtitle => false;
public MediumGridPropertiesViewModel(IMediumGridLayout mediumGridLayout) public MediumGridPropertiesViewModel(IMediumGridLayout mediumGridLayout)
{ {
_model = new(mediumGridLayout); _model = new(mediumGridLayout);
} }
public bool ShowTitle { get; set; }
public void InitializeProperties() public void InitializeProperties()
{ {
var model = _model.Unsafe; var model = _model.Unsafe;

View File

@@ -11,6 +11,10 @@ public class SmallGridPropertiesViewModel : IGridPropertiesViewModel
{ {
private readonly ExtensionObject<ISmallGridLayout> _model; private readonly ExtensionObject<ISmallGridLayout> _model;
public bool ShowTitle => false;
public bool ShowSubtitle => false;
public SmallGridPropertiesViewModel(ISmallGridLayout smallGridLayout) public SmallGridPropertiesViewModel(ISmallGridLayout smallGridLayout)
{ {
_model = new(smallGridLayout); _model = new(smallGridLayout);

View File

@@ -0,0 +1,31 @@
// 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;
internal sealed partial class GridItemContainerStyleSelector : StyleSelector
{
public IGridPropertiesViewModel? GridProperties { get; set; }
public Style? Small { get; set; }
public Style? Medium { get; set; }
public Style? Gallery { get; set; }
protected override Style? SelectStyleCore(object item, DependencyObject container)
{
return GridProperties switch
{
SmallGridPropertiesViewModel => Small,
MediumGridPropertiesViewModel => Medium,
GalleryGridPropertiesViewModel => Gallery,
_ => Medium,
};
}
}

View File

@@ -20,21 +20,12 @@ internal sealed partial class GridItemTemplateSelector : DataTemplateSelector
protected override DataTemplate? SelectTemplateCore(object item, DependencyObject dependencyObject) protected override DataTemplate? SelectTemplateCore(object item, DependencyObject dependencyObject)
{ {
DataTemplate? dataTemplate = Medium; return GridProperties switch
if (GridProperties is SmallGridPropertiesViewModel)
{ {
dataTemplate = Small; SmallGridPropertiesViewModel => Small,
} MediumGridPropertiesViewModel => Medium,
else if (GridProperties is MediumGridPropertiesViewModel) GalleryGridPropertiesViewModel => Gallery,
{ _ => Medium,
dataTemplate = Medium; };
}
else if (GridProperties is GalleryGridPropertiesViewModel)
{
dataTemplate = Gallery;
}
return dataTemplate;
} }
} }

View File

@@ -5,33 +5,151 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cmdpalUI="using:Microsoft.CmdPal.UI" xmlns:cmdpalUI="using:Microsoft.CmdPal.UI"
xmlns:controls="using:CommunityToolkit.WinUI.Controls" xmlns:controls="using:CommunityToolkit.WinUI.Controls"
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
xmlns:coreViewModels="using:Microsoft.CmdPal.Core.ViewModels" xmlns:coreViewModels="using:Microsoft.CmdPal.Core.ViewModels"
xmlns:cpcontrols="using:Microsoft.CmdPal.UI.Controls" xmlns:cpcontrols="using:Microsoft.CmdPal.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:help="using:Microsoft.CmdPal.UI.Helpers" xmlns:help="using:Microsoft.CmdPal.UI.Helpers"
xmlns:local="using:Microsoft.CmdPal.UI"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="using:CommunityToolkit.WinUI"
xmlns:viewModels="using:Microsoft.CmdPal.UI.ViewModels"
x:Name="PageRoot" x:Name="PageRoot"
Background="Transparent" Background="Transparent"
DataContext="{x:Bind ViewModel, Mode=OneWay}" DataContext="{x:Bind ViewModel, Mode=OneWay}"
mc:Ignorable="d"> mc:Ignorable="d">
<Page.Resources> <Page.Resources>
<!-- TODO: Figure out what we want to do here for filtering/grouping and where -->
<!-- https://learn.microsoft.com/windows/windows-app-sdk/api/winrt/microsoft.ui.xaml.data.collectionviewsource -->
<!--<CollectionViewSource
x:Name="ItemsCVS"
IsSourceGrouped="True"
Source="{x:Bind ViewModel.Items, Mode=OneWay}" />-->
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" /> <!--
<converters:StringVisibilityConverter GridViewItemCornerRadius is the corner radius defined in GridView template; make
x:Key="StringVisibilityConverter" it bigger to match the radii of the gallery
EmptyValue="Collapsed" -->
NotEmptyValue="Visible" /> <CornerRadius x:Key="GalleryGridViewItemContainerCornerRadius">6</CornerRadius>
<CornerRadius x:Key="IconGridViewItemContainerCornerRadius">4</CornerRadius>
<CornerRadius x:Key="GalleryGridViewItemRadius">4</CornerRadius>
<CornerRadius x:Key="SmallGridViewItemCornerRadius">8</CornerRadius>
<CornerRadius x:Key="MediumGridViewItemCornerRadius">8</CornerRadius>
<Style x:Key="IconGridViewItemStyle" TargetType="GridViewItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="GridViewItem">
<ListViewItemPresenter
x:Name="Root"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
CheckBoxBorderBrush="{ThemeResource GridViewItemCheckBoxBorderBrush}"
CheckBoxBrush="{ThemeResource GridViewItemCheckBoxBrush}"
CheckBoxCornerRadius="{ThemeResource GridViewItemCheckBoxCornerRadius}"
CheckBoxDisabledBorderBrush="{ThemeResource GridViewItemCheckBoxDisabledBorderBrush}"
CheckBoxDisabledBrush="{ThemeResource GridViewItemCheckBoxDisabledBrush}"
CheckBoxPointerOverBorderBrush="{ThemeResource GridViewItemCheckBoxPointerOverBorderBrush}"
CheckBoxPointerOverBrush="{ThemeResource GridViewItemCheckBoxPointerOverBrush}"
CheckBoxPressedBorderBrush="{ThemeResource GridViewItemCheckBoxPressedBorderBrush}"
CheckBoxPressedBrush="{ThemeResource GridViewItemCheckBoxPressedBrush}"
CheckBoxSelectedBrush="{ThemeResource GridViewItemCheckBoxSelectedBrush}"
CheckBoxSelectedDisabledBrush="{ThemeResource GridViewItemCheckBoxSelectedDisabledBrush}"
CheckBoxSelectedPointerOverBrush="{ThemeResource GridViewItemCheckBoxSelectedPointerOverBrush}"
CheckBoxSelectedPressedBrush="{ThemeResource GridViewItemCheckBoxSelectedPressedBrush}"
CheckBrush="{ThemeResource GridViewItemCheckBrush}"
CheckDisabledBrush="{ThemeResource GridViewItemCheckDisabledBrush}"
CheckMode="{ThemeResource GridViewItemCheckMode}"
CheckPressedBrush="{ThemeResource GridViewItemCheckPressedBrush}"
ContentMargin="{TemplateBinding Padding}"
ContentTransitions="{TemplateBinding ContentTransitions}"
Control.IsTemplateFocusTarget="True"
CornerRadius="{StaticResource IconGridViewItemContainerCornerRadius}"
DisabledOpacity="{ThemeResource ListViewItemDisabledThemeOpacity}"
DragBackground="{ThemeResource GridViewItemDragBackground}"
DragForeground="{ThemeResource GridViewItemDragForeground}"
DragOpacity="{ThemeResource ListViewItemDragThemeOpacity}"
FocusBorderBrush="{ThemeResource GridViewItemFocusBorderBrush}"
FocusVisualMargin="{TemplateBinding FocusVisualMargin}"
FocusVisualPrimaryBrush="{TemplateBinding FocusVisualPrimaryBrush}"
FocusVisualPrimaryThickness="{TemplateBinding FocusVisualPrimaryThickness}"
FocusVisualSecondaryBrush="{TemplateBinding FocusVisualSecondaryBrush}"
FocusVisualSecondaryThickness="{TemplateBinding FocusVisualSecondaryThickness}"
PlaceholderBackground="{ThemeResource GridViewItemPlaceholderBackground}"
PointerOverBackground="{ThemeResource GridViewItemBackgroundPointerOver}"
PointerOverBorderBrush="{ThemeResource GridViewItemPointerOverBorderBrush}"
PointerOverForeground="{ThemeResource GridViewItemForegroundPointerOver}"
PressedBackground="{ThemeResource GridViewItemBackgroundPressed}"
ReorderHintOffset="{ThemeResource GridViewItemReorderHintThemeOffset}"
SelectedBackground="{ThemeResource GridViewItemBackgroundSelected}"
SelectedBorderBrush="{ThemeResource GridViewItemSelectedBorderBrush}"
SelectedBorderThickness="{ThemeResource GridViewItemSelectedBorderThickness}"
SelectedDisabledBackground="{ThemeResource GridViewItemBackgroundSelectedDisabled}"
SelectedDisabledBorderBrush="{ThemeResource GridViewItemSelectedDisabledBorderBrush}"
SelectedForeground="{ThemeResource GridViewItemForegroundSelected}"
SelectedInnerBorderBrush="{ThemeResource GridViewItemSelectedInnerBorderBrush}"
SelectedPointerOverBackground="{ThemeResource GridViewItemBackgroundSelectedPointerOver}"
SelectedPointerOverBorderBrush="{ThemeResource GridViewItemSelectedPointerOverBorderBrush}"
SelectedPressedBackground="{ThemeResource GridViewItemBackgroundSelectedPressed}"
SelectedPressedBorderBrush="{ThemeResource GridViewItemSelectedPressedBorderBrush}"
SelectionCheckMarkVisualEnabled="{ThemeResource GridViewItemSelectionCheckMarkVisualEnabled}" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="GalleryGridViewItemStyle" TargetType="GridViewItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="GridViewItem">
<ListViewItemPresenter
x:Name="Root"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
CheckBoxBorderBrush="{ThemeResource GridViewItemCheckBoxBorderBrush}"
CheckBoxBrush="{ThemeResource GridViewItemCheckBoxBrush}"
CheckBoxCornerRadius="{ThemeResource GridViewItemCheckBoxCornerRadius}"
CheckBoxDisabledBorderBrush="{ThemeResource GridViewItemCheckBoxDisabledBorderBrush}"
CheckBoxDisabledBrush="{ThemeResource GridViewItemCheckBoxDisabledBrush}"
CheckBoxPointerOverBorderBrush="{ThemeResource GridViewItemCheckBoxPointerOverBorderBrush}"
CheckBoxPointerOverBrush="{ThemeResource GridViewItemCheckBoxPointerOverBrush}"
CheckBoxPressedBorderBrush="{ThemeResource GridViewItemCheckBoxPressedBorderBrush}"
CheckBoxPressedBrush="{ThemeResource GridViewItemCheckBoxPressedBrush}"
CheckBoxSelectedBrush="{ThemeResource GridViewItemCheckBoxSelectedBrush}"
CheckBoxSelectedDisabledBrush="{ThemeResource GridViewItemCheckBoxSelectedDisabledBrush}"
CheckBoxSelectedPointerOverBrush="{ThemeResource GridViewItemCheckBoxSelectedPointerOverBrush}"
CheckBoxSelectedPressedBrush="{ThemeResource GridViewItemCheckBoxSelectedPressedBrush}"
CheckBrush="{ThemeResource GridViewItemCheckBrush}"
CheckDisabledBrush="{ThemeResource GridViewItemCheckDisabledBrush}"
CheckMode="{ThemeResource GridViewItemCheckMode}"
CheckPressedBrush="{ThemeResource GridViewItemCheckPressedBrush}"
ContentMargin="{TemplateBinding Padding}"
ContentTransitions="{TemplateBinding ContentTransitions}"
Control.IsTemplateFocusTarget="True"
CornerRadius="{StaticResource GalleryGridViewItemContainerCornerRadius}"
DisabledOpacity="{ThemeResource ListViewItemDisabledThemeOpacity}"
DragBackground="{ThemeResource GridViewItemDragBackground}"
DragForeground="{ThemeResource GridViewItemDragForeground}"
DragOpacity="{ThemeResource ListViewItemDragThemeOpacity}"
FocusBorderBrush="{ThemeResource GridViewItemFocusBorderBrush}"
FocusVisualMargin="{TemplateBinding FocusVisualMargin}"
FocusVisualPrimaryBrush="{TemplateBinding FocusVisualPrimaryBrush}"
FocusVisualPrimaryThickness="{TemplateBinding FocusVisualPrimaryThickness}"
FocusVisualSecondaryBrush="{TemplateBinding FocusVisualSecondaryBrush}"
FocusVisualSecondaryThickness="{TemplateBinding FocusVisualSecondaryThickness}"
PlaceholderBackground="{ThemeResource GridViewItemPlaceholderBackground}"
PointerOverBackground="{ThemeResource GridViewItemBackgroundPointerOver}"
PointerOverBorderBrush="{ThemeResource GridViewItemPointerOverBorderBrush}"
PointerOverForeground="{ThemeResource GridViewItemForegroundPointerOver}"
PressedBackground="{ThemeResource GridViewItemBackgroundPressed}"
ReorderHintOffset="{ThemeResource GridViewItemReorderHintThemeOffset}"
SelectedBackground="{ThemeResource GridViewItemBackgroundSelected}"
SelectedBorderBrush="{ThemeResource GridViewItemSelectedBorderBrush}"
SelectedBorderThickness="{ThemeResource GridViewItemSelectedBorderThickness}"
SelectedDisabledBackground="{ThemeResource GridViewItemBackgroundSelectedDisabled}"
SelectedDisabledBorderBrush="{ThemeResource GridViewItemSelectedDisabledBorderBrush}"
SelectedForeground="{ThemeResource GridViewItemForegroundSelected}"
SelectedInnerBorderBrush="{ThemeResource GridViewItemSelectedInnerBorderBrush}"
SelectedPointerOverBackground="{ThemeResource GridViewItemBackgroundSelectedPointerOver}"
SelectedPointerOverBorderBrush="{ThemeResource GridViewItemSelectedPointerOverBorderBrush}"
SelectedPressedBackground="{ThemeResource GridViewItemBackgroundSelectedPressed}"
SelectedPressedBorderBrush="{ThemeResource GridViewItemSelectedPressedBorderBrush}"
SelectionCheckMarkVisualEnabled="{ThemeResource GridViewItemSelectionCheckMarkVisualEnabled}" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<DataTemplate x:Key="TagTemplate" x:DataType="coreViewModels:TagViewModel"> <DataTemplate x:Key="TagTemplate" x:DataType="coreViewModels:TagViewModel">
<cpcontrols:Tag <cpcontrols:Tag
@@ -48,10 +166,17 @@
x:Key="GridItemTemplateSelector" x:Key="GridItemTemplateSelector"
x:DataType="coreViewModels:ListItemViewModel" x:DataType="coreViewModels:ListItemViewModel"
Gallery="{StaticResource GalleryGridItemViewModelTemplate}" Gallery="{StaticResource GalleryGridItemViewModelTemplate}"
GridProperties="{x:Bind ViewModel.GridProperties}" GridProperties="{x:Bind ViewModel.GridProperties, Mode=OneWay}"
Medium="{StaticResource MediumGridItemViewModelTemplate}" Medium="{StaticResource MediumGridItemViewModelTemplate}"
Small="{StaticResource SmallGridItemViewModelTemplate}" /> Small="{StaticResource SmallGridItemViewModelTemplate}" />
<cmdpalUI:GridItemContainerStyleSelector
x:Key="GridItemContainerStyleSelector"
Gallery="{StaticResource GalleryGridViewItemStyle}"
GridProperties="{x:Bind ViewModel.GridProperties, Mode=OneWay}"
Medium="{StaticResource IconGridViewItemStyle}"
Small="{StaticResource IconGridViewItemStyle}" />
<!-- https://learn.microsoft.com/windows/apps/design/controls/itemsview#specify-the-look-of-the-items --> <!-- https://learn.microsoft.com/windows/apps/design/controls/itemsview#specify-the-look-of-the-items -->
<DataTemplate x:Key="ListItemViewModelTemplate" x:DataType="coreViewModels:ListItemViewModel"> <DataTemplate x:Key="ListItemViewModelTemplate" x:DataType="coreViewModels:ListItemViewModel">
<Grid <Grid
@@ -94,7 +219,7 @@
Text="{x:Bind Subtitle, Mode=OneWay}" Text="{x:Bind Subtitle, Mode=OneWay}"
TextTrimming="CharacterEllipsis" TextTrimming="CharacterEllipsis"
TextWrapping="NoWrap" TextWrapping="NoWrap"
Visibility="{x:Bind Subtitle, Mode=OneWay, Converter={StaticResource StringVisibilityConverter}}" /> Visibility="{x:Bind ShowSubtitle, Mode=OneWay}" />
</StackPanel> </StackPanel>
<ItemsControl <ItemsControl
@@ -124,11 +249,11 @@
Padding="8,16" Padding="8,16"
HorizontalAlignment="Center" HorizontalAlignment="Center"
VerticalAlignment="Center" VerticalAlignment="Center"
AutomationProperties.Name="{x:Bind Title}" AutomationProperties.Name="{x:Bind Title, Mode=OneWay}"
BorderThickness="0" BorderThickness="0"
CornerRadius="8" CornerRadius="{StaticResource SmallGridViewItemCornerRadius}"
Orientation="Vertical" Orientation="Vertical"
ToolTipService.ToolTip="{x:Bind Title}"> ToolTipService.ToolTip="{x:Bind Title, Mode=OneWay}">
<cpcontrols:IconBox <cpcontrols:IconBox
x:Name="GridIconBorder" x:Name="GridIconBorder"
@@ -145,23 +270,22 @@
</DataTemplate> </DataTemplate>
<DataTemplate x:Key="MediumGridItemViewModelTemplate" x:DataType="coreViewModels:ListItemViewModel"> <DataTemplate x:Key="MediumGridItemViewModelTemplate" x:DataType="coreViewModels:ListItemViewModel">
<StackPanel <Grid
Width="100" Width="100"
Height="100" Height="100"
Padding="8,16" Padding="8"
HorizontalAlignment="Center" AutomationProperties.Name="{x:Bind Title, Mode=OneWay}"
VerticalAlignment="Center" CornerRadius="{StaticResource MediumGridViewItemCornerRadius}"
AutomationProperties.Name="{x:Bind Title}" ToolTipService.ToolTip="{x:Bind Title, Mode=OneWay}">
BorderThickness="0" <Grid.RowDefinitions>
CornerRadius="8" <RowDefinition Height="*" />
Orientation="Vertical" <RowDefinition Height="Auto" />
ToolTipService.ToolTip="{x:Bind Title}"> </Grid.RowDefinitions>
<cpcontrols:IconBox <cpcontrols:IconBox
x:Name="GridIconBorder" x:Name="GridIconBorder"
Grid.Row="0"
Width="36" Width="36"
Height="36" Height="36"
Margin="0"
HorizontalAlignment="Center" HorizontalAlignment="Center"
VerticalAlignment="Center" VerticalAlignment="Center"
CharacterSpacing="12" CharacterSpacing="12"
@@ -169,21 +293,20 @@
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}" />
<TextBlock <TextBlock
x:Name="TitleTextBlock" x:Name="TitleTextBlock"
MaxHeight="40" Grid.Row="1"
Margin="0,8,0,4" Height="32"
Margin="0,8,0,0"
HorizontalAlignment="Center" HorizontalAlignment="Center"
VerticalAlignment="Center"
CharacterSpacing="12" CharacterSpacing="12"
FontSize="12" FontSize="12"
Text="{x:Bind Title}" Text="{x:Bind Title, Mode=OneWay}"
TextAlignment="Center" TextAlignment="Center"
TextTrimming="WordEllipsis" TextTrimming="WordEllipsis"
TextWrapping="Wrap" TextWrapping="Wrap"
Visibility="{Binding ElementName=PageRoot, Path=DataContext.GridProperties.ShowTitle, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" /> Visibility="{x:Bind LayoutShowsTitle, Mode=OneWay}" />
</StackPanel> </Grid>
</DataTemplate> </DataTemplate>
<DataTemplate x:Key="GalleryGridItemViewModelTemplate" x:DataType="coreViewModels:ListItemViewModel"> <DataTemplate x:Key="GalleryGridItemViewModelTemplate" x:DataType="coreViewModels:ListItemViewModel">
@@ -193,11 +316,11 @@
Padding="0" Padding="0"
HorizontalAlignment="Center" HorizontalAlignment="Center"
VerticalAlignment="Center" VerticalAlignment="Center"
AutomationProperties.Name="{x:Bind Title}" AutomationProperties.Name="{x:Bind Title, Mode=OneWay}"
BorderThickness="0" BorderThickness="0"
CornerRadius="4" CornerRadius="{StaticResource GalleryGridViewItemRadius}"
Orientation="Vertical" Orientation="Vertical"
ToolTipService.ToolTip="{x:Bind Title}"> ToolTipService.ToolTip="{x:Bind Title, Mode=OneWay}">
<Grid <Grid
Width="160" Width="160"
@@ -205,12 +328,8 @@
Margin="0" Margin="0"
HorizontalAlignment="Center" HorizontalAlignment="Center"
VerticalAlignment="Center" VerticalAlignment="Center"
CornerRadius="4"> CornerRadius="{StaticResource GalleryGridViewItemRadius}">
<Grid.RowDefinitions>
<RowDefinition />
</Grid.RowDefinitions>
<Viewbox <Viewbox
Grid.Row="1"
HorizontalAlignment="Center" HorizontalAlignment="Center"
Stretch="UniformToFill" Stretch="UniformToFill"
StretchDirection="Both"> StretchDirection="Both">
@@ -222,35 +341,39 @@
</Viewbox> </Viewbox>
</Grid> </Grid>
<StackPanel Padding="4" Orientation="Vertical"> <StackPanel
Padding="4"
Orientation="Vertical"
Spacing="4"
Visibility="{x:Bind help:BindTransformers.VisibleWhenAny(ShowTitle, ShowSubtitle)}">
<TextBlock <TextBlock
x:Name="TitleTextBlock" x:Name="TitleTextBlock"
MaxWidth="152" MaxWidth="152"
MaxHeight="40" MaxHeight="40"
Margin="0"
HorizontalAlignment="Left" HorizontalAlignment="Left"
VerticalAlignment="Center" VerticalAlignment="Center"
CharacterSpacing="12" CharacterSpacing="12"
FontSize="14" FontSize="14"
Foreground="{ThemeResource TextFillColorPrimary}" Foreground="{ThemeResource TextFillColorPrimary}"
Text="{x:Bind Title}" Text="{x:Bind Title, Mode=OneWay}"
TextAlignment="Center" TextAlignment="Center"
TextTrimming="WordEllipsis" TextTrimming="WordEllipsis"
TextWrapping="NoWrap" /> TextWrapping="NoWrap"
Visibility="{x:Bind ShowTitle, Mode=OneWay}" />
<TextBlock <TextBlock
x:Name="SubTitleTextBlock" x:Name="SubTitleTextBlock"
MaxWidth="152" MaxWidth="152"
MaxHeight="40" MaxHeight="40"
Margin="0,4,0,0"
HorizontalAlignment="Left" HorizontalAlignment="Left"
VerticalAlignment="Center" VerticalAlignment="Center"
CharacterSpacing="11" CharacterSpacing="11"
FontSize="11" FontSize="11"
Foreground="{ThemeResource TextFillColorTertiary}" Foreground="{ThemeResource TextFillColorTertiary}"
Text="{x:Bind Subtitle}" Text="{x:Bind Subtitle, Mode=OneWay}"
TextAlignment="Center" TextAlignment="Center"
TextTrimming="WordEllipsis" TextTrimming="WordEllipsis"
TextWrapping="NoWrap" /> TextWrapping="NoWrap"
Visibility="{x:Bind ShowSubtitle, Mode=OneWay}" />
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
</DataTemplate> </DataTemplate>
@@ -295,6 +418,7 @@
IsDoubleTapEnabled="True" IsDoubleTapEnabled="True"
IsItemClickEnabled="True" IsItemClickEnabled="True"
ItemClick="Items_ItemClick" ItemClick="Items_ItemClick"
ItemContainerStyleSelector="{StaticResource GridItemContainerStyleSelector}"
ItemTemplateSelector="{StaticResource GridItemTemplateSelector}" ItemTemplateSelector="{StaticResource GridItemTemplateSelector}"
ItemsSource="{x:Bind ViewModel.FilteredItems, Mode=OneWay}" ItemsSource="{x:Bind ViewModel.FilteredItems, Mode=OneWay}"
RightTapped="Items_RightTapped" RightTapped="Items_RightTapped"
@@ -302,6 +426,7 @@
<GridView.ItemContainerTransitions> <GridView.ItemContainerTransitions>
<TransitionCollection /> <TransitionCollection />
</GridView.ItemContainerTransitions> </GridView.ItemContainerTransitions>
<GridView.ItemContainerStyle />
</GridView> </GridView>
</controls:Case> </controls:Case>
</controls:SwitchPresenter> </controls:SwitchPresenter>

View File

@@ -15,4 +15,7 @@ internal static class BindTransformers
public static Visibility EmptyOrWhitespaceToCollapsed(string? input) public static Visibility EmptyOrWhitespaceToCollapsed(string? input)
=> string.IsNullOrWhiteSpace(input) ? Visibility.Collapsed : Visibility.Visible; => string.IsNullOrWhiteSpace(input) ? Visibility.Collapsed : Visibility.Visible;
public static Visibility VisibleWhenAny(bool value1, bool value2)
=> (value1 || value2) ? Visibility.Visible : Visibility.Collapsed;
} }

View File

@@ -9,13 +9,6 @@ namespace SamplePagesExtension;
internal sealed partial class SampleGalleryListPage : ListPage internal sealed partial class SampleGalleryListPage : ListPage
{ {
public SampleGalleryListPage()
{
Icon = new IconInfo("\uE7C5");
Name = "Sample Gallery List Page";
GridProperties = new GalleryGridLayout();
}
public override IListItem[] GetItems() public override IListItem[] GetItems()
{ {
return [ return [

View File

@@ -0,0 +1,59 @@
// 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;
internal sealed partial class SampleGridsListPage : ListPage
{
private readonly IListItem[] _items =
[
new ListItem(new SampleGalleryListPage { GridProperties = new GalleryGridLayout { ShowTitle = true, ShowSubtitle = true } })
{
Title = "Gallery list page (title and subtitle)",
Subtitle = "A sample gallery list page with images",
Icon = IconHelpers.FromRelativePath("Assets/Images/Swirls.png"),
},
new ListItem(new SampleGalleryListPage { GridProperties = new GalleryGridLayout { ShowTitle = true, ShowSubtitle = false } })
{
Title = "Gallery list page (title, no subtitle)",
Subtitle = "A sample gallery list page with images",
Icon = IconHelpers.FromRelativePath("Assets/Images/Swirls.png"),
},
new ListItem(new SampleGalleryListPage { GridProperties = new GalleryGridLayout { ShowTitle = false, ShowSubtitle = false } })
{
Title = "Gallery list page (no title, no subtitle)",
Subtitle = "A sample gallery list page with images",
Icon = IconHelpers.FromRelativePath("Assets/Images/Swirls.png"),
},
new ListItem(new SampleGalleryListPage { GridProperties = new SmallGridLayout() })
{
Title = "Small grid list page",
Subtitle = "A sample grid list page with text items",
Icon = IconHelpers.FromRelativePath("Assets/Images/Win-Digital.png"),
},
new ListItem(new SampleGalleryListPage { GridProperties = new MediumGridLayout { ShowTitle = true } })
{
Title = "Medium grid (with title)",
Subtitle = "A sample grid list page with text items",
Icon = IconHelpers.FromRelativePath("Assets/Images/Win-Digital.png"),
},
new ListItem(new SampleGalleryListPage { GridProperties = new MediumGridLayout { ShowTitle = false } })
{
Title = "Medium grid (hidden title)",
Subtitle = "A sample grid list page with text items",
Icon = IconHelpers.FromRelativePath("Assets/Images/Win-Digital.png"),
}
];
public SampleGridsListPage()
{
Icon = new IconInfo("\uE7C5");
Name = "Grid and gallery lists";
}
public override IListItem[] GetItems() => _items;
}

View File

@@ -34,9 +34,9 @@ public partial class SamplesListPage : ListPage
Title = "Dynamic List Page Command", Title = "Dynamic List Page Command",
Subtitle = "Changes the list of items in response to the typed query", Subtitle = "Changes the list of items in response to the typed query",
}, },
new ListItem(new SampleGalleryListPage()) new ListItem(new SampleGridsListPage())
{ {
Title = "Gallery List Page Command", Title = "Grid views and galleries",
Subtitle = "Displays items as a gallery", Subtitle = "Displays items as a gallery",
}, },
new ListItem(new OnLoadPage()) new ListItem(new OnLoadPage())