CmdPal: Did someone say grid pages? (#40832)

Closes #38269

Still working on this one, but essentially allows a list page to become
a grid page by specifying a `GridProperties` property like so:

```C#
public AllAppsPage()
{
    PlaceholderText = Resources.search_installed_apps_placeholder;
    GridProperties = new MediumGridLayout();
}
```

> This is a very early version and very subject to change. Much to
clean, but feedback & suggestions are welcome.

## Current preview

### SmallGridLayout

<img width="998" height="607" alt="image"
src="https://github.com/user-attachments/assets/ebdf11fd-6c86-4fc3-bf49-bcbb5d32caa4"
/>


### MediumGridLayout

<img width="976" height="586" alt="image"
src="https://github.com/user-attachments/assets/82daa2e9-548e-4864-8885-1c486ca9f891"
/>

### GalleryGridLayout

<img width="988" height="600" alt="image"
src="https://github.com/user-attachments/assets/23ca486a-35c7-467a-b200-4f6ee5f4a95c"
/>

---------

Co-authored-by: Mike Griese <migrie@microsoft.com>
This commit is contained in:
Michael Jolley
2025-09-08 05:25:07 -05:00
committed by GitHub
parent 63042dad31
commit 1887c22e87
20 changed files with 595 additions and 105 deletions

View File

@@ -320,4 +320,4 @@ MRUINFO
REGSTR REGSTR
# Misc Win32 APIs and PInvokes # Misc Win32 APIs and PInvokes
INVOKEIDLIST INVOKEIDLIST

View File

@@ -1451,7 +1451,6 @@ rstringalnum
rstringalpha rstringalpha
rstringdigit rstringdigit
rtb rtb
RTB
RTLREADING RTLREADING
rtm rtm
runas runas

View File

@@ -0,0 +1,34 @@
// 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.Models;
using Microsoft.CommandPalette.Extensions;
namespace Microsoft.CmdPal.Core.ViewModels;
public class GalleryGridPropertiesViewModel : IGridPropertiesViewModel
{
private readonly ExtensionObject<IGalleryGridLayout> _model;
public GalleryGridPropertiesViewModel(IGalleryGridLayout galleryGridLayout)
{
_model = new(galleryGridLayout);
}
public bool ShowTitle { get; set; }
public bool ShowSubtitle { get; set; }
public void InitializeProperties()
{
var model = _model.Unsafe;
if (model is null)
{
return; // throw?
}
ShowTitle = model.ShowTitle;
ShowSubtitle = model.ShowSubtitle;
}
}

View File

@@ -0,0 +1,10 @@
// 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.
namespace Microsoft.CmdPal.Core.ViewModels;
public interface IGridPropertiesViewModel
{
void InitializeProperties();
}

View File

@@ -45,6 +45,10 @@ public partial class ListViewModel : PageViewModel, IDisposable
(!_isFetching) && (!_isFetching) &&
IsLoading == false; IsLoading == false;
public bool IsGridView { get; private set; }
public IGridPropertiesViewModel? GridProperties { get; private set; }
// Remember - "observable" properties from the model (via PropChanged) // Remember - "observable" properties from the model (via PropChanged)
// cannot be marked [ObservableProperty] // cannot be marked [ObservableProperty]
public bool ShowDetails { get; private set; } public bool ShowDetails { get; private set; }
@@ -516,6 +520,13 @@ public partial class ListViewModel : PageViewModel, IDisposable
_isDynamic = model is IDynamicListPage; _isDynamic = model is IDynamicListPage;
IsGridView = model.GridProperties is not null;
UpdateProperty(nameof(IsGridView));
GridProperties = LoadGridPropertiesViewModel(model.GridProperties);
GridProperties?.InitializeProperties();
UpdateProperty(nameof(GridProperties));
ShowDetails = model.ShowDetails; ShowDetails = model.ShowDetails;
UpdateProperty(nameof(ShowDetails)); UpdateProperty(nameof(ShowDetails));
@@ -537,9 +548,27 @@ public partial class ListViewModel : PageViewModel, IDisposable
model.ItemsChanged += Model_ItemsChanged; model.ItemsChanged += Model_ItemsChanged;
} }
private IGridPropertiesViewModel? LoadGridPropertiesViewModel(IGridProperties? gridProperties)
{
if (gridProperties is IMediumGridLayout mediumGridLayout)
{
return new MediumGridPropertiesViewModel(mediumGridLayout);
}
else if (gridProperties is IGalleryGridLayout galleryGridLayout)
{
return new GalleryGridPropertiesViewModel(galleryGridLayout);
}
else if (gridProperties is ISmallGridLayout smallGridLayout)
{
return new SmallGridPropertiesViewModel(smallGridLayout);
}
return null;
}
public void LoadMoreIfNeeded() public void LoadMoreIfNeeded()
{ {
var model = this._model.Unsafe; var model = _model.Unsafe;
if (model is null) if (model is null)
{ {
return; return;
@@ -583,7 +612,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
{ {
base.FetchProperty(propertyName); base.FetchProperty(propertyName);
var model = this._model.Unsafe; var model = _model.Unsafe;
if (model is null) if (model is null)
{ {
return; // throw? return; // throw?
@@ -591,14 +620,20 @@ public partial class ListViewModel : PageViewModel, IDisposable
switch (propertyName) switch (propertyName)
{ {
case nameof(GridProperties):
IsGridView = model.GridProperties is not null;
GridProperties = LoadGridPropertiesViewModel(model.GridProperties);
GridProperties?.InitializeProperties();
UpdateProperty(nameof(IsGridView));
break;
case nameof(ShowDetails): case nameof(ShowDetails):
this.ShowDetails = model.ShowDetails; ShowDetails = model.ShowDetails;
break; break;
case nameof(PlaceholderText): case nameof(PlaceholderText):
this._modelPlaceholderText = model.PlaceholderText; _modelPlaceholderText = model.PlaceholderText;
break; break;
case nameof(SearchText): case nameof(SearchText):
this.SearchText = model.SearchText; SearchText = model.SearchText;
break; break;
case nameof(EmptyContent): case nameof(EmptyContent):
EmptyContent = new(new(model.EmptyContent), PageContext); EmptyContent = new(new(model.EmptyContent), PageContext);

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.Models;
using Microsoft.CommandPalette.Extensions;
namespace Microsoft.CmdPal.Core.ViewModels;
public class MediumGridPropertiesViewModel : IGridPropertiesViewModel
{
private readonly ExtensionObject<IMediumGridLayout> _model;
public MediumGridPropertiesViewModel(IMediumGridLayout mediumGridLayout)
{
_model = new(mediumGridLayout);
}
public bool ShowTitle { get; set; }
public void InitializeProperties()
{
var model = _model.Unsafe;
if (model is null)
{
return; // throw?
}
ShowTitle = model.ShowTitle;
}
}

View File

@@ -0,0 +1,22 @@
// 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.Models;
using Microsoft.CommandPalette.Extensions;
namespace Microsoft.CmdPal.Core.ViewModels;
public class SmallGridPropertiesViewModel : IGridPropertiesViewModel
{
private readonly ExtensionObject<ISmallGridLayout> _model;
public SmallGridPropertiesViewModel(ISmallGridLayout smallGridLayout)
{
_model = new(smallGridLayout);
}
public void InitializeProperties()
{
}
}

View File

@@ -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.CmdPal.Core.ViewModels;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace Microsoft.CmdPal.UI;
internal sealed partial class GridItemTemplateSelector : DataTemplateSelector
{
public IGridPropertiesViewModel? GridProperties { get; set; }
public DataTemplate? Small { get; set; }
public DataTemplate? Medium { get; set; }
public DataTemplate? Gallery { get; set; }
protected override DataTemplate? SelectTemplateCore(object item, DependencyObject dependencyObject)
{
DataTemplate? dataTemplate = Medium;
if (GridProperties is SmallGridPropertiesViewModel)
{
dataTemplate = Small;
}
else if (GridProperties is MediumGridPropertiesViewModel)
{
dataTemplate = Medium;
}
else if (GridProperties is GalleryGridPropertiesViewModel)
{
dataTemplate = Gallery;
}
return dataTemplate;
}
}

View File

@@ -3,6 +3,7 @@
x:Class="Microsoft.CmdPal.UI.ListPage" x:Class="Microsoft.CmdPal.UI.ListPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
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:controls="using:CommunityToolkit.WinUI.Controls" xmlns:controls="using:CommunityToolkit.WinUI.Controls"
xmlns:converters="using:CommunityToolkit.WinUI.Converters" xmlns:converters="using:CommunityToolkit.WinUI.Converters"
xmlns:coreViewModels="using:Microsoft.CmdPal.Core.ViewModels" xmlns:coreViewModels="using:Microsoft.CmdPal.Core.ViewModels"
@@ -11,8 +12,11 @@
xmlns:help="using:Microsoft.CmdPal.UI.Helpers" xmlns:help="using:Microsoft.CmdPal.UI.Helpers"
xmlns:local="using:Microsoft.CmdPal.UI" 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" xmlns:viewModels="using:Microsoft.CmdPal.UI.ViewModels"
x:Name="PageRoot"
Background="Transparent" Background="Transparent"
DataContext="{x:Bind ViewModel, Mode=OneWay}"
mc:Ignorable="d"> mc:Ignorable="d">
<Page.Resources> <Page.Resources>
@@ -23,6 +27,7 @@
IsSourceGrouped="True" IsSourceGrouped="True"
Source="{x:Bind ViewModel.Items, Mode=OneWay}" />--> Source="{x:Bind ViewModel.Items, Mode=OneWay}" />-->
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
<converters:StringVisibilityConverter <converters:StringVisibilityConverter
x:Key="StringVisibilityConverter" x:Key="StringVisibilityConverter"
EmptyValue="Collapsed" EmptyValue="Collapsed"
@@ -39,6 +44,14 @@
ToolTipService.ToolTip="{x:Bind ToolTip, Mode=OneWay}" /> ToolTipService.ToolTip="{x:Bind ToolTip, Mode=OneWay}" />
</DataTemplate> </DataTemplate>
<cmdpalUI:GridItemTemplateSelector
x:Key="GridItemTemplateSelector"
x:DataType="coreViewModels:ListItemViewModel"
Gallery="{StaticResource GalleryGridItemViewModelTemplate}"
GridProperties="{x:Bind ViewModel.GridProperties}"
Medium="{StaticResource MediumGridItemViewModelTemplate}"
Small="{StaticResource SmallGridItemViewModelTemplate}" />
<!-- 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
@@ -102,6 +115,145 @@
</ItemsControl> </ItemsControl>
</Grid> </Grid>
</DataTemplate> </DataTemplate>
<!-- Grid item templates for visual grid representation -->
<DataTemplate x:Key="SmallGridItemViewModelTemplate" x:DataType="coreViewModels:ListItemViewModel">
<StackPanel
Width="60"
Height="60"
Padding="8,16"
HorizontalAlignment="Center"
VerticalAlignment="Center"
AutomationProperties.Name="{x:Bind Title}"
BorderThickness="0"
CornerRadius="8"
Orientation="Vertical"
ToolTipService.ToolTip="{x:Bind Title}">
<cpcontrols:IconBox
x:Name="GridIconBorder"
Width="28"
Height="28"
Margin="0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Foreground="{ThemeResource TextFillColorPrimary}"
SourceKey="{x:Bind Icon, Mode=OneWay}"
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="MediumGridItemViewModelTemplate" x:DataType="coreViewModels:ListItemViewModel">
<StackPanel
Width="100"
Height="100"
Padding="8,16"
HorizontalAlignment="Center"
VerticalAlignment="Center"
AutomationProperties.Name="{x:Bind Title}"
BorderThickness="0"
CornerRadius="8"
Orientation="Vertical"
ToolTipService.ToolTip="{x:Bind Title}">
<cpcontrols:IconBox
x:Name="GridIconBorder"
Width="36"
Height="36"
Margin="0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
CharacterSpacing="12"
FontSize="14"
Foreground="{ThemeResource TextFillColorPrimary}"
SourceKey="{x:Bind Icon, Mode=OneWay}"
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
<TextBlock
x:Name="TitleTextBlock"
MaxHeight="40"
Margin="0,8,0,4"
HorizontalAlignment="Center"
VerticalAlignment="Center"
CharacterSpacing="12"
FontSize="12"
Text="{x:Bind Title}"
TextAlignment="Center"
TextTrimming="WordEllipsis"
TextWrapping="Wrap"
Visibility="{Binding ElementName=PageRoot, Path=DataContext.GridProperties.ShowTitle, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="GalleryGridItemViewModelTemplate" x:DataType="coreViewModels:ListItemViewModel">
<StackPanel
Width="160"
Margin="4"
Padding="0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
AutomationProperties.Name="{x:Bind Title}"
BorderThickness="0"
CornerRadius="4"
Orientation="Vertical"
ToolTipService.ToolTip="{x:Bind Title}">
<Grid
Width="160"
Height="160"
Margin="0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
CornerRadius="4">
<Grid.RowDefinitions>
<RowDefinition />
</Grid.RowDefinitions>
<Viewbox
Grid.Row="1"
HorizontalAlignment="Center"
Stretch="UniformToFill"
StretchDirection="Both">
<cpcontrols:IconBox
CornerRadius="4"
Foreground="{ThemeResource TextFillColorPrimary}"
SourceKey="{x:Bind Icon, Mode=OneWay}"
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
</Viewbox>
</Grid>
<StackPanel Padding="4" Orientation="Vertical">
<TextBlock
x:Name="TitleTextBlock"
MaxWidth="152"
MaxHeight="40"
Margin="0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
CharacterSpacing="12"
FontSize="14"
Foreground="{ThemeResource TextFillColorPrimary}"
Text="{x:Bind Title}"
TextAlignment="Center"
TextTrimming="WordEllipsis"
TextWrapping="NoWrap" />
<TextBlock
x:Name="SubTitleTextBlock"
MaxWidth="152"
MaxHeight="40"
Margin="0,4,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
CharacterSpacing="11"
FontSize="11"
Foreground="{ThemeResource TextFillColorTertiary}"
Text="{x:Bind Subtitle}"
TextAlignment="Center"
TextTrimming="WordEllipsis"
TextWrapping="NoWrap" />
</StackPanel>
</StackPanel>
</DataTemplate>
</Page.Resources> </Page.Resources>
<Grid> <Grid>
@@ -110,66 +262,50 @@
TargetType="x:Boolean" TargetType="x:Boolean"
Value="{x:Bind ViewModel.ShowEmptyContent, Mode=OneWay}"> Value="{x:Bind ViewModel.ShowEmptyContent, Mode=OneWay}">
<controls:Case Value="False"> <controls:Case Value="False">
<ListView <controls:SwitchPresenter
x:Name="ItemsList" HorizontalAlignment="Stretch"
Padding="0,2,0,0" TargetType="x:Boolean"
ContextCanceled="ItemsList_OnContextCanceled" Value="{x:Bind ViewModel.IsGridView, Mode=OneWay}">
ContextRequested="ItemsList_OnContextRequested" <controls:Case Value="False">
DoubleTapped="ItemsList_DoubleTapped" <ListView
IsDoubleTapEnabled="True" x:Name="ItemsList"
IsItemClickEnabled="True" Padding="0,2,0,0"
ItemClick="ItemsList_ItemClick" ContextCanceled="Items_OnContextCanceled"
ItemTemplate="{StaticResource ListItemViewModelTemplate}" ContextRequested="Items_OnContextRequested"
ItemsSource="{x:Bind ViewModel.FilteredItems, Mode=OneWay}" DoubleTapped="Items_DoubleTapped"
SelectionChanged="ItemsList_SelectionChanged"> IsDoubleTapEnabled="True"
<ListView.ItemContainerTransitions> IsItemClickEnabled="True"
<TransitionCollection /> ItemClick="Items_ItemClick"
</ListView.ItemContainerTransitions> ItemTemplate="{StaticResource ListItemViewModelTemplate}"
<!--<ListView.GroupStyle> ItemsSource="{x:Bind ViewModel.FilteredItems, Mode=OneWay}"
<GroupStyle HidesIfEmpty="True"> RightTapped="Items_RightTapped"
<GroupStyle.HeaderTemplate> SelectionChanged="Items_SelectionChanged">
<DataTemplate> <ListView.ItemContainerTransitions>
<TextBlock <TransitionCollection />
Margin="0,16,0,0" </ListView.ItemContainerTransitions>
Foreground="{ThemeResource TextFillColorSecondaryBrush}" </ListView>
Text="{Binding Key, Mode=OneWay}" /> </controls:Case>
</DataTemplate> <controls:Case Value="True">
</GroupStyle.HeaderTemplate> <GridView
</GroupStyle> x:Name="ItemsGrid"
</ListView.GroupStyle>--> Padding="8"
</ListView> ContextCanceled="Items_OnContextCanceled"
</controls:Case> ContextRequested="Items_OnContextRequested"
<controls:Case Value="True"> DoubleTapped="Items_DoubleTapped"
<StackPanel IsDoubleTapEnabled="True"
Margin="24" IsItemClickEnabled="True"
HorizontalAlignment="Center" ItemClick="Items_ItemClick"
VerticalAlignment="Center" ItemTemplateSelector="{StaticResource GridItemTemplateSelector}"
Orientation="Vertical" ItemsSource="{x:Bind ViewModel.FilteredItems, Mode=OneWay}"
Spacing="4"> RightTapped="Items_RightTapped"
<cpcontrols:IconBox SelectionChanged="Items_SelectionChanged">
x:Name="IconBorder" <GridView.ItemContainerTransitions>
Width="48" <TransitionCollection />
Height="48" </GridView.ItemContainerTransitions>
Margin="8" </GridView>
Foreground="{ThemeResource TextFillColorSecondaryBrush}" </controls:Case>
SourceKey="{x:Bind ViewModel.EmptyContent.Icon, Mode=OneWay}" </controls:SwitchPresenter>
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
<TextBlock
Margin="0,4,0,0"
HorizontalAlignment="Center"
FontWeight="SemiBold"
Text="{x:Bind ViewModel.EmptyContent.Title, Mode=OneWay}"
TextAlignment="Center"
TextWrapping="Wrap" />
<TextBlock
HorizontalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="{x:Bind ViewModel.EmptyContent.Subtitle, Mode=OneWay}"
TextAlignment="Center"
TextWrapping="Wrap" />
</StackPanel>
</controls:Case> </controls:Case>
</controls:SwitchPresenter> </controls:SwitchPresenter>
</Grid> </Grid>
</Page> </Page>

View File

@@ -13,6 +13,7 @@ using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Input; using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media; using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation; using Microsoft.UI.Xaml.Navigation;
@@ -38,13 +39,21 @@ public sealed partial class ListPage : Page,
public static readonly DependencyProperty ViewModelProperty = public static readonly DependencyProperty ViewModelProperty =
DependencyProperty.Register(nameof(ViewModel), typeof(ListViewModel), typeof(ListPage), new PropertyMetadata(null, OnViewModelChanged)); DependencyProperty.Register(nameof(ViewModel), typeof(ListViewModel), typeof(ListPage), new PropertyMetadata(null, OnViewModelChanged));
private ListViewBase ItemView
{
get
{
return ViewModel?.IsGridView == true ? ItemsGrid : ItemsList;
}
}
public ListPage() public ListPage()
{ {
this.InitializeComponent(); this.InitializeComponent();
this.NavigationCacheMode = NavigationCacheMode.Disabled; this.NavigationCacheMode = NavigationCacheMode.Disabled;
this.ItemsList.Loaded += ItemsList_Loaded; this.ItemView.Loaded += Items_Loaded;
this.ItemsList.PreviewKeyDown += ItemsList_PreviewKeyDown; this.ItemView.PreviewKeyDown += Items_PreviewKeyDown;
this.ItemsList.PointerPressed += ItemsList_PointerPressed; this.ItemView.PointerPressed += Items_PointerPressed;
} }
protected override void OnNavigatedTo(NavigationEventArgs e) protected override void OnNavigatedTo(NavigationEventArgs e)
@@ -55,11 +64,11 @@ public sealed partial class ListPage : Page,
} }
if (e.NavigationMode == NavigationMode.Back if (e.NavigationMode == NavigationMode.Back
|| (e.NavigationMode == NavigationMode.New && ItemsList.Items.Count > 0)) || (e.NavigationMode == NavigationMode.New && ItemView.Items.Count > 0))
{ {
// Upon navigating _back_ to this page, immediately select the // Upon navigating _back_ to this page, immediately select the
// first item in the list // first item in the list
ItemsList.SelectedIndex = 0; ItemView.SelectedIndex = 0;
} }
// RegisterAll isn't AOT compatible // RegisterAll isn't AOT compatible
@@ -90,7 +99,6 @@ public sealed partial class ListPage : Page,
{ {
ViewModel?.SafeCleanup(); ViewModel?.SafeCleanup();
CleanupHelper.Cleanup(this); CleanupHelper.Cleanup(this);
Bindings.StopTracking();
} }
// Clean-up event listeners // Clean-up event listeners
@@ -100,7 +108,7 @@ public sealed partial class ListPage : Page,
} }
[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 ItemsList_ItemClick(object sender, ItemClickEventArgs e) private void Items_ItemClick(object sender, ItemClickEventArgs e)
{ {
if (e.ClickedItem is ListItemViewModel item) if (e.ClickedItem is ListItemViewModel item)
{ {
@@ -123,9 +131,9 @@ public sealed partial class ListPage : Page,
} }
} }
private void ItemsList_DoubleTapped(object sender, DoubleTappedRoutedEventArgs e) private void Items_DoubleTapped(object sender, DoubleTappedRoutedEventArgs e)
{ {
if (ItemsList.SelectedItem is ListItemViewModel vm) if (ItemView.SelectedItem is ListItemViewModel vm)
{ {
var settings = App.Current.Services.GetService<SettingsModel>()!; var settings = App.Current.Services.GetService<SettingsModel>()!;
if (!settings.SingleClickActivates) if (!settings.SingleClickActivates)
@@ -136,10 +144,10 @@ public sealed partial class ListPage : Page,
} }
[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 ItemsList_SelectionChanged(object sender, SelectionChangedEventArgs e) private void Items_SelectionChanged(object sender, SelectionChangedEventArgs e)
{ {
var vm = ViewModel; var vm = ViewModel;
var li = ItemsList.SelectedItem as ListItemViewModel; var li = ItemView.SelectedItem as ListItemViewModel;
_ = Task.Run(() => _ = Task.Run(() =>
{ {
vm?.UpdateSelectedItemCommand.Execute(li); vm?.UpdateSelectedItemCommand.Execute(li);
@@ -154,12 +162,12 @@ 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 (ItemsList.SelectedItem is not null) if (ItemView.SelectedItem is not null)
{ {
ItemsList.ScrollIntoView(ItemsList.SelectedItem); ItemView.ScrollIntoView(ItemView.SelectedItem);
// Automation notification for screen readers // Automation notification for screen readers
var listViewPeer = Microsoft.UI.Xaml.Automation.Peers.ListViewAutomationPeer.CreatePeerForElement(ItemsList); 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; var notificationText = li.Title;
@@ -172,10 +180,37 @@ public sealed partial class ListPage : Page,
} }
} }
private void ItemsList_Loaded(object sender, RoutedEventArgs e) private void Items_RightTapped(object sender, RightTappedRoutedEventArgs e)
{ {
// Find the ScrollViewer in the ListView if (e.OriginalSource is FrameworkElement element &&
var listViewScrollViewer = FindScrollViewer(this.ItemsList); element.DataContext is ListItemViewModel item)
{
if (ItemView.SelectedItem != item)
{
ItemView.SelectedItem = item;
}
ViewModel?.UpdateSelectedItemCommand.Execute(item);
var pos = e.GetPosition(element);
_ = DispatcherQueue.TryEnqueue(
() =>
{
WeakReferenceMessenger.Default.Send<OpenContextMenuMessage>(
new OpenContextMenuMessage(
element,
Microsoft.UI.Xaml.Controls.Primitives.FlyoutPlacementMode.BottomEdgeAlignedLeft,
pos,
ContextMenuFilterLocation.Top));
});
}
}
private void Items_Loaded(object sender, RoutedEventArgs e)
{
// Find the ScrollViewer in the ItemView (ItemsList or ItemsGrid)
var listViewScrollViewer = FindScrollViewer(this.ItemView);
if (listViewScrollViewer is not null) if (listViewScrollViewer is not null)
{ {
@@ -207,25 +242,25 @@ public sealed partial class ListPage : Page,
// And then have these commands manipulate that state being bound to the UI instead // And then have these commands manipulate that state being bound to the UI instead
// We may want to see how other non-list UIs need to behave to make this decision // We may want to see how other non-list UIs need to behave to make this decision
// At least it's decoupled from the SearchBox now :) // At least it's decoupled from the SearchBox now :)
if (ItemsList.SelectedIndex < ItemsList.Items.Count - 1) if (ItemView.SelectedIndex < ItemView.Items.Count - 1)
{ {
ItemsList.SelectedIndex++; ItemView.SelectedIndex++;
} }
else else
{ {
ItemsList.SelectedIndex = 0; ItemView.SelectedIndex = 0;
} }
} }
public void Receive(NavigatePreviousCommand message) public void Receive(NavigatePreviousCommand message)
{ {
if (ItemsList.SelectedIndex > 0) if (ItemView.SelectedIndex > 0)
{ {
ItemsList.SelectedIndex--; ItemView.SelectedIndex--;
} }
else else
{ {
ItemsList.SelectedIndex = ItemsList.Items.Count - 1; ItemView.SelectedIndex = ItemView.Items.Count - 1;
} }
} }
@@ -235,7 +270,7 @@ public sealed partial class ListPage : Page,
{ {
ViewModel?.InvokeItemCommand.Execute(null); ViewModel?.InvokeItemCommand.Execute(null);
} }
else if (ItemsList.SelectedItem is ListItemViewModel item) else if (ItemView.SelectedItem is ListItemViewModel item)
{ {
ViewModel?.InvokeItemCommand.Execute(item); ViewModel?.InvokeItemCommand.Execute(item);
} }
@@ -247,7 +282,7 @@ public sealed partial class ListPage : Page,
{ {
ViewModel?.InvokeSecondaryCommandCommand.Execute(null); ViewModel?.InvokeSecondaryCommandCommand.Execute(null);
} }
else if (ItemsList.SelectedItem is ListItemViewModel item) else if (ItemView.SelectedItem is ListItemViewModel item)
{ {
ViewModel?.InvokeSecondaryCommandCommand.Execute(item); ViewModel?.InvokeSecondaryCommandCommand.Execute(item);
} }
@@ -283,19 +318,19 @@ public sealed partial class ListPage : Page,
// //
// It's important to do this here, because once there's no selection // It's important to do this here, because once there's no selection
// (which can happen as the list updates) we won't get an // (which can happen as the list updates) we won't get an
// ItemsList_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 (ItemsList.SelectedItem is null) if (ItemView.SelectedItem is null)
{ {
ItemsList.SelectedIndex = 0; ItemView.SelectedIndex = 0;
} }
// Always reset the selected item when the top-level list page changes // Always reset the selected item when the top-level list page changes
// its items // its items
if (!sender.IsNested) if (!sender.IsNested)
{ {
ItemsList.SelectedIndex = 0; ItemView.SelectedIndex = 0;
} }
} }
@@ -304,7 +339,7 @@ public sealed partial class ListPage : Page,
var prop = e.PropertyName; var prop = e.PropertyName;
if (prop == nameof(ViewModel.FilteredItems)) if (prop == nameof(ViewModel.FilteredItems))
{ {
Debug.WriteLine($"ViewModel.FilteredItems {ItemsList.SelectedItem}"); Debug.WriteLine($"ViewModel.FilteredItems {ItemView.SelectedItem}");
} }
} }
@@ -328,12 +363,12 @@ public sealed partial class ListPage : Page,
return null; return null;
} }
private void ItemsList_OnContextRequested(UIElement sender, ContextRequestedEventArgs e) private void Items_OnContextRequested(UIElement sender, ContextRequestedEventArgs e)
{ {
var (item, element) = e.OriginalSource switch var (item, element) = e.OriginalSource switch
{ {
// caused by keyboard shortcut (e.g. Context menu key or Shift+F10) // caused by keyboard shortcut (e.g. Context menu key or Shift+F10)
ListViewItem listViewItem => (ItemsList.ItemFromContainer(listViewItem) as ListItemViewModel, listViewItem), SelectorItem selectorItem => (ItemView.ItemFromContainer(selectorItem) as ListItemViewModel, selectorItem),
// caused by right-click on the ListViewItem // caused by right-click on the ListViewItem
FrameworkElement { DataContext: ListItemViewModel itemViewModel } frameworkElement => (itemViewModel, frameworkElement), FrameworkElement { DataContext: ListItemViewModel itemViewModel } frameworkElement => (itemViewModel, frameworkElement),
@@ -346,9 +381,9 @@ public sealed partial class ListPage : Page,
return; return;
} }
if (ItemsList.SelectedItem != item) if (ItemView.SelectedItem != item)
{ {
ItemsList.SelectedItem = item; ItemView.SelectedItem = item;
} }
ViewModel?.UpdateSelectedItemCommand.Execute(item); ViewModel?.UpdateSelectedItemCommand.Execute(item);
@@ -371,14 +406,14 @@ public sealed partial class ListPage : Page,
e.Handled = true; e.Handled = true;
} }
private void ItemsList_OnContextCanceled(UIElement sender, RoutedEventArgs e) private void Items_OnContextCanceled(UIElement sender, RoutedEventArgs e)
{ {
_ = DispatcherQueue.TryEnqueue(() => WeakReferenceMessenger.Default.Send<CloseContextMenuMessage>()); _ = DispatcherQueue.TryEnqueue(() => WeakReferenceMessenger.Default.Send<CloseContextMenuMessage>());
} }
private void ItemsList_PointerPressed(object sender, PointerRoutedEventArgs e) => _lastInputSource = InputSource.Pointer; private void Items_PointerPressed(object sender, PointerRoutedEventArgs e) => _lastInputSource = InputSource.Pointer;
private void ItemsList_PreviewKeyDown(object sender, KeyRoutedEventArgs e) private void Items_PreviewKeyDown(object sender, KeyRoutedEventArgs e)
{ {
if (e.Key is VirtualKey.Enter or VirtualKey.Space) if (e.Key is VirtualKey.Enter or VirtualKey.Space)
{ {

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 584 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 613 KiB

View File

@@ -0,0 +1,66 @@
// 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 SampleGalleryListPage : ListPage
{
public SampleGalleryListPage()
{
Icon = new IconInfo("\uE7C5");
Name = "Sample Gallery List Page";
GridProperties = new GalleryGridLayout();
}
public override IListItem[] GetItems()
{
return [
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"),
},
];
}
}

View File

@@ -33,6 +33,11 @@ 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())
{
Title = "Gallery List Page Command",
Subtitle = "Displays items as a gallery",
},
new ListItem(new OnLoadPage()) new ListItem(new OnLoadPage())
{ {
Title = "Demo of OnLoad/OnUnload", Title = "Demo of OnLoad/OnUnload",

View File

@@ -0,0 +1,32 @@
// 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.
namespace Microsoft.CommandPalette.Extensions.Toolkit;
public partial class GalleryGridLayout : BaseObservable, IGalleryGridLayout
{
public virtual bool ShowTitle
{
get => field;
set
{
field = value;
OnPropertyChanged(nameof(ShowTitle));
}
}
= true;
public virtual bool ShowSubtitle
{
get => field;
set
{
field = value;
OnPropertyChanged(nameof(ShowSubtitle));
}
}
= true;
}

View File

@@ -0,0 +1,20 @@
// 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.
namespace Microsoft.CommandPalette.Extensions.Toolkit;
public partial class MediumGridLayout : BaseObservable, IMediumGridLayout
{
public virtual bool ShowTitle
{
get => field;
set
{
field = value;
OnPropertyChanged(nameof(ShowTitle));
}
}
= true;
}

View File

@@ -0,0 +1,9 @@
// 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.
namespace Microsoft.CommandPalette.Extensions.Toolkit;
public partial class SmallGridLayout : BaseObservable, ISmallGridLayout
{
}

View File

@@ -273,10 +273,26 @@ namespace Microsoft.CommandPalette.Extensions
String Section { get; }; String Section { get; };
String TextToSuggest { get; }; String TextToSuggest { get; };
} }
[uuid("50C6F080-1CBE-4CE4-B92F-DA2F116ED524")]
[contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)]
interface IGridProperties requires INotifyPropChanged { }
[uuid("05914D59-6ECB-4992-9CF2-5982B5120A26")]
[contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)]
interface ISmallGridLayout requires IGridProperties { }
[contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)] [contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)]
interface IGridProperties { interface IMediumGridLayout requires IGridProperties
Windows.Foundation.Size TileSize { get; }; {
Boolean ShowTitle { get; };
}
[contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)]
interface IGalleryGridLayout requires IGridProperties
{
Boolean ShowTitle { get; };
Boolean ShowSubtitle { get; };
} }
[contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)] [contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)]