mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-02-23 19:49:43 +01:00
Extracting list/grid views from ListPage
This commit is contained in:
@@ -0,0 +1,329 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<UserControl
|
||||
x:Class="Microsoft.CmdPal.UI.Controls.ListViewControl"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:cmdpalUI="using:Microsoft.CmdPal.UI"
|
||||
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
|
||||
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
|
||||
xmlns:coreViewModels="using:Microsoft.CmdPal.Core.ViewModels"
|
||||
xmlns:cpcontrols="using:Microsoft.CmdPal.UI.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:help="using:Microsoft.CmdPal.UI.Helpers"
|
||||
xmlns:local="using:Microsoft.CmdPal.UI.Controls"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:ui="using:CommunityToolkit.WinUI"
|
||||
xmlns:viewModels="using:Microsoft.CmdPal.UI.ViewModels"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<UserControl.Resources>
|
||||
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
|
||||
<converters:StringVisibilityConverter
|
||||
x:Key="StringVisibilityConverter"
|
||||
EmptyValue="Collapsed"
|
||||
NotEmptyValue="Visible" />
|
||||
|
||||
<DataTemplate x:Key="TagTemplate" x:DataType="coreViewModels:TagViewModel">
|
||||
<cpcontrols:Tag
|
||||
AutomationProperties.Name="{x:Bind Text, Mode=OneWay}"
|
||||
BackgroundColor="{x:Bind Background, Mode=OneWay}"
|
||||
FontSize="12"
|
||||
ForegroundColor="{x:Bind Foreground, Mode=OneWay}"
|
||||
Icon="{x:Bind Icon, Mode=OneWay}"
|
||||
Text="{x:Bind Text, Mode=OneWay}"
|
||||
ToolTipService.ToolTip="{x:Bind ToolTip, Mode=OneWay}" />
|
||||
</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 -->
|
||||
<DataTemplate x:Key="ListItemViewModelTemplate" x:DataType="coreViewModels:ListItemViewModel">
|
||||
<Grid
|
||||
Padding="0,12,0,12"
|
||||
AutomationProperties.Name="{x:Bind Title, Mode=OneWay}"
|
||||
ColumnSpacing="12">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="28" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<cpcontrols:IconBox
|
||||
x:Name="IconBorder"
|
||||
Grid.Column="0"
|
||||
Width="20"
|
||||
Height="20"
|
||||
Margin="4,0,4,0"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
|
||||
|
||||
<StackPanel
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
Orientation="Vertical"
|
||||
Spacing="1">
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
CharacterSpacing="12"
|
||||
FontSize="14"
|
||||
Text="{x:Bind Title, Mode=OneWay}"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
TextWrapping="NoWrap" />
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind Subtitle, Mode=OneWay}"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
TextWrapping="NoWrap"
|
||||
Visibility="{x:Bind Subtitle, Mode=OneWay, Converter={StaticResource StringVisibilityConverter}}" />
|
||||
</StackPanel>
|
||||
|
||||
<ItemsControl
|
||||
Grid.RowSpan="2"
|
||||
Grid.Column="2"
|
||||
Margin="0,0,8,0"
|
||||
VerticalAlignment="Center"
|
||||
IsHitTestVisible="False"
|
||||
IsTabStop="False"
|
||||
ItemTemplate="{StaticResource TagTemplate}"
|
||||
ItemsSource="{x:Bind Tags, Mode=OneWay}"
|
||||
Visibility="{x:Bind HasTags, Mode=OneWay}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Horizontal" Spacing="4" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
</ItemsControl>
|
||||
</Grid>
|
||||
</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>
|
||||
</UserControl.Resources>
|
||||
|
||||
<controls:SwitchPresenter
|
||||
HorizontalAlignment="Stretch"
|
||||
TargetType="x:Boolean"
|
||||
Value="{x:Bind ViewModel.ShowEmptyContent, Mode=OneWay}">
|
||||
<controls:Case Value="False">
|
||||
<controls:SwitchPresenter
|
||||
HorizontalAlignment="Stretch"
|
||||
TargetType="x:Boolean"
|
||||
Value="{x:Bind ViewModel.IsGridView, Mode=OneWay}">
|
||||
<controls:Case Value="False">
|
||||
<ListView
|
||||
x:Name="ItemsList"
|
||||
Padding="0,2,0,0"
|
||||
ContextCanceled="Items_OnContextCanceled"
|
||||
ContextRequested="Items_OnContextRequested"
|
||||
DoubleTapped="Items_DoubleTapped"
|
||||
IsDoubleTapEnabled="True"
|
||||
IsItemClickEnabled="True"
|
||||
ItemClick="Items_ItemClick"
|
||||
ItemTemplate="{StaticResource ListItemViewModelTemplate}"
|
||||
ItemsSource="{x:Bind ViewModel.FilteredItems, Mode=OneWay}"
|
||||
RightTapped="Items_RightTapped"
|
||||
SelectionChanged="Items_SelectionChanged">
|
||||
<ListView.ItemContainerTransitions>
|
||||
<TransitionCollection />
|
||||
</ListView.ItemContainerTransitions>
|
||||
</ListView>
|
||||
</controls:Case>
|
||||
<controls:Case Value="True">
|
||||
<GridView
|
||||
x:Name="ItemsGrid"
|
||||
Padding="8"
|
||||
ContextCanceled="Items_OnContextCanceled"
|
||||
ContextRequested="Items_OnContextRequested"
|
||||
DoubleTapped="Items_DoubleTapped"
|
||||
IsDoubleTapEnabled="True"
|
||||
IsItemClickEnabled="True"
|
||||
ItemClick="Items_ItemClick"
|
||||
ItemTemplateSelector="{StaticResource GridItemTemplateSelector}"
|
||||
ItemsSource="{x:Bind ViewModel.FilteredItems, Mode=OneWay}"
|
||||
RightTapped="Items_RightTapped"
|
||||
SelectionChanged="Items_SelectionChanged">
|
||||
<GridView.ItemContainerTransitions>
|
||||
<TransitionCollection />
|
||||
</GridView.ItemContainerTransitions>
|
||||
</GridView>
|
||||
</controls:Case>
|
||||
</controls:SwitchPresenter>
|
||||
</controls:Case>
|
||||
<controls:Case Value="True">
|
||||
<StackPanel
|
||||
Margin="24"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Orientation="Vertical"
|
||||
Spacing="4">
|
||||
<cpcontrols:IconBox
|
||||
x:Name="IconBorder"
|
||||
Width="48"
|
||||
Height="48"
|
||||
Margin="8"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
SourceKey="{x:Bind ViewModel.EmptyContent.Icon, Mode=OneWay}"
|
||||
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:SwitchPresenter>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,395 @@
|
||||
// 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 CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.Helpers;
|
||||
using Microsoft.CmdPal.UI.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Controls.Primitives;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Windows.System;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Controls;
|
||||
|
||||
public sealed partial class ListViewControl : UserControl,
|
||||
IRecipient<NavigateNextCommand>,
|
||||
IRecipient<NavigatePreviousCommand>,
|
||||
IRecipient<ActivateSelectedListItemMessage>,
|
||||
IRecipient<ActivateSecondaryCommandMessage>
|
||||
{
|
||||
private InputSource _lastInputSource;
|
||||
|
||||
public ListViewModel? ViewModel
|
||||
{
|
||||
get => (ListViewModel?)GetValue(ViewModelProperty);
|
||||
set => SetValue(ViewModelProperty, value);
|
||||
}
|
||||
|
||||
// Using a DependencyProperty as the backing store for ViewModel. This enables animation, styling, binding, etc...
|
||||
public static readonly DependencyProperty ViewModelProperty =
|
||||
DependencyProperty.Register(nameof(ViewModel), typeof(ListViewModel), typeof(ListPage), new PropertyMetadata(null, OnViewModelChanged));
|
||||
|
||||
private ListViewBase ItemView
|
||||
{
|
||||
get
|
||||
{
|
||||
return ViewModel?.IsGridView == true ? ItemsGrid : ItemsList;
|
||||
}
|
||||
}
|
||||
|
||||
public ListViewControl()
|
||||
{
|
||||
InitializeComponent();
|
||||
this.ItemView.Loaded += Items_Loaded;
|
||||
this.ItemView.PreviewKeyDown += Items_PreviewKeyDown;
|
||||
this.ItemView.PointerPressed += Items_PointerPressed;
|
||||
|
||||
WeakReferenceMessenger.Default.Register<NavigateNextCommand>(this);
|
||||
WeakReferenceMessenger.Default.Register<NavigatePreviousCommand>(this);
|
||||
WeakReferenceMessenger.Default.Register<ActivateSelectedListItemMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<ActivateSecondaryCommandMessage>(this);
|
||||
}
|
||||
|
||||
private static void OnViewModelChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is ListViewControl @this)
|
||||
{
|
||||
if (e.OldValue is ListViewModel old)
|
||||
{
|
||||
old.PropertyChanged -= @this.ViewModel_PropertyChanged;
|
||||
old.ItemsUpdated -= @this.Page_ItemsUpdated;
|
||||
}
|
||||
|
||||
if (e.NewValue is ListViewModel page)
|
||||
{
|
||||
page.PropertyChanged += @this.ViewModel_PropertyChanged;
|
||||
page.ItemsUpdated += @this.Page_ItemsUpdated;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Called after we've finished updating the whole list for either a
|
||||
// GetItems or a change in the filter.
|
||||
private void Page_ItemsUpdated(ListViewModel sender, object args)
|
||||
{
|
||||
// If for some reason, we don't have a selected item, fix that.
|
||||
//
|
||||
// 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
|
||||
// ItemView_SelectionChanged again to give us another chance to change
|
||||
// the selection from null -> something. Better to just update the
|
||||
// selection once, at the end of all the updating.
|
||||
if (ItemView.SelectedItem is null)
|
||||
{
|
||||
ItemView.SelectedIndex = 0;
|
||||
}
|
||||
|
||||
// Always reset the selected item when the top-level list page changes
|
||||
// its items
|
||||
if (!sender.IsNested)
|
||||
{
|
||||
ItemView.SelectedIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
[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)
|
||||
{
|
||||
if (e.ClickedItem is ListItemViewModel item)
|
||||
{
|
||||
if (_lastInputSource == InputSource.Keyboard)
|
||||
{
|
||||
ViewModel?.InvokeItemCommand.Execute(item);
|
||||
return;
|
||||
}
|
||||
|
||||
var settings = App.Current.Services.GetService<SettingsModel>()!;
|
||||
if (settings.SingleClickActivates)
|
||||
{
|
||||
ViewModel?.InvokeItemCommand.Execute(item);
|
||||
}
|
||||
else
|
||||
{
|
||||
ViewModel?.UpdateSelectedItemCommand.Execute(item);
|
||||
WeakReferenceMessenger.Default.Send<FocusSearchBoxMessage>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Items_DoubleTapped(object sender, DoubleTappedRoutedEventArgs e)
|
||||
{
|
||||
if (ItemView.SelectedItem is ListItemViewModel vm)
|
||||
{
|
||||
var settings = App.Current.Services.GetService<SettingsModel>()!;
|
||||
if (!settings.SingleClickActivates)
|
||||
{
|
||||
ViewModel?.InvokeItemCommand.Execute(vm);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "VS is too aggressive at pruning methods bound in XAML")]
|
||||
private void Items_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
var vm = ViewModel;
|
||||
var li = ItemView.SelectedItem as ListItemViewModel;
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
vm?.UpdateSelectedItemCommand.Execute(li);
|
||||
});
|
||||
|
||||
// There's mysterious behavior here, where the selection seemingly
|
||||
// changes to _nothing_ when we're backspacing to a single character.
|
||||
// And at that point, seemingly the item that's getting removed is not
|
||||
// a member of FilteredItems. Very bizarre.
|
||||
//
|
||||
// Might be able to fix in the future by stashing the removed item
|
||||
// here, then in Page_ItemsUpdated trying to select that cached item if
|
||||
// it's in the list (otherwise, clear the cache), but that seems
|
||||
// aggressively BODGY for something that mostly just works today.
|
||||
if (ItemView.SelectedItem is not null)
|
||||
{
|
||||
ItemView.ScrollIntoView(ItemView.SelectedItem);
|
||||
|
||||
// Automation notification for screen readers
|
||||
var listViewPeer = Microsoft.UI.Xaml.Automation.Peers.ListViewAutomationPeer.CreatePeerForElement(ItemView);
|
||||
if (listViewPeer is not null && li is not null)
|
||||
{
|
||||
var notificationText = li.Title;
|
||||
|
||||
UIHelper.AnnounceActionForAccessibility(
|
||||
ItemsList,
|
||||
notificationText,
|
||||
"CommandPaletteSelectedItemChanged");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Items_RightTapped(object sender, RightTappedRoutedEventArgs e)
|
||||
{
|
||||
if (e.OriginalSource is FrameworkElement element &&
|
||||
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)
|
||||
{
|
||||
listViewScrollViewer.ViewChanged += ListViewScrollViewer_ViewChanged;
|
||||
}
|
||||
}
|
||||
|
||||
private void ListViewScrollViewer_ViewChanged(object? sender, ScrollViewerViewChangedEventArgs e)
|
||||
{
|
||||
var scrollView = sender as ScrollViewer;
|
||||
if (scrollView is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// When we get to the bottom, request more from the extension, if they
|
||||
// have more to give us.
|
||||
// We're checking when we get to 80% of the scroll height, to give the
|
||||
// extension a bit of a heads-up before the user actually gets there.
|
||||
if (scrollView.VerticalOffset >= (scrollView.ScrollableHeight * .8))
|
||||
{
|
||||
ViewModel?.LoadMoreIfNeeded();
|
||||
}
|
||||
}
|
||||
|
||||
public void Receive(NavigateNextCommand message)
|
||||
{
|
||||
// Note: We may want to just have the notion of a 'SelectedCommand' in our VM
|
||||
// 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
|
||||
// At least it's decoupled from the SearchBox now :)
|
||||
if (ItemView.SelectedIndex < ItemView.Items.Count - 1)
|
||||
{
|
||||
ItemView.SelectedIndex++;
|
||||
}
|
||||
else
|
||||
{
|
||||
ItemView.SelectedIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public void Receive(NavigatePreviousCommand message)
|
||||
{
|
||||
if (ItemView.SelectedIndex > 0)
|
||||
{
|
||||
ItemView.SelectedIndex--;
|
||||
}
|
||||
else
|
||||
{
|
||||
ItemView.SelectedIndex = ItemView.Items.Count - 1;
|
||||
}
|
||||
}
|
||||
|
||||
public void Receive(ActivateSelectedListItemMessage message)
|
||||
{
|
||||
if (ViewModel?.ShowEmptyContent ?? false)
|
||||
{
|
||||
ViewModel?.InvokeItemCommand.Execute(null);
|
||||
}
|
||||
else if (ItemView.SelectedItem is ListItemViewModel item)
|
||||
{
|
||||
ViewModel?.InvokeItemCommand.Execute(item);
|
||||
}
|
||||
}
|
||||
|
||||
private ScrollViewer? FindScrollViewer(DependencyObject parent)
|
||||
{
|
||||
if (parent is ScrollViewer)
|
||||
{
|
||||
return (ScrollViewer)parent;
|
||||
}
|
||||
|
||||
for (var i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
|
||||
{
|
||||
var child = VisualTreeHelper.GetChild(parent, i);
|
||||
var result = FindScrollViewer(child);
|
||||
if (result is not null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void Items_OnContextRequested(UIElement sender, ContextRequestedEventArgs e)
|
||||
{
|
||||
var (item, element) = e.OriginalSource switch
|
||||
{
|
||||
// caused by keyboard shortcut (e.g. Context menu key or Shift+F10)
|
||||
SelectorItem selectorItem => (ItemView.ItemFromContainer(selectorItem) as ListItemViewModel, selectorItem),
|
||||
|
||||
// caused by right-click on the ListViewItem
|
||||
FrameworkElement { DataContext: ListItemViewModel itemViewModel } frameworkElement => (itemViewModel, frameworkElement),
|
||||
|
||||
_ => (null, null),
|
||||
};
|
||||
|
||||
if (item is null || element is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (ItemView.SelectedItem != item)
|
||||
{
|
||||
ItemView.SelectedItem = item;
|
||||
}
|
||||
|
||||
ViewModel?.UpdateSelectedItemCommand.Execute(item);
|
||||
|
||||
if (!e.TryGetPosition(element, out var pos))
|
||||
{
|
||||
pos = new(0, element.ActualHeight);
|
||||
}
|
||||
|
||||
_ = DispatcherQueue.TryEnqueue(
|
||||
() =>
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<OpenContextMenuMessage>(
|
||||
new OpenContextMenuMessage(
|
||||
element,
|
||||
Microsoft.UI.Xaml.Controls.Primitives.FlyoutPlacementMode.BottomEdgeAlignedLeft,
|
||||
pos,
|
||||
ContextMenuFilterLocation.Top));
|
||||
});
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void Items_OnContextCanceled(UIElement sender, RoutedEventArgs e)
|
||||
{
|
||||
_ = DispatcherQueue.TryEnqueue(() => WeakReferenceMessenger.Default.Send<CloseContextMenuMessage>());
|
||||
}
|
||||
|
||||
private void Items_PointerPressed(object sender, PointerRoutedEventArgs e) => _lastInputSource = InputSource.Pointer;
|
||||
|
||||
private void Items_PreviewKeyDown(object sender, KeyRoutedEventArgs e)
|
||||
{
|
||||
if (e.Key is VirtualKey.Enter or VirtualKey.Space)
|
||||
{
|
||||
_lastInputSource = InputSource.Keyboard;
|
||||
}
|
||||
}
|
||||
|
||||
private void ViewModel_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
{
|
||||
var prop = e.PropertyName;
|
||||
if (prop == nameof(ViewModel.FilteredItems))
|
||||
{
|
||||
Debug.WriteLine($"ViewModel.FilteredItems {ItemView.SelectedItem}");
|
||||
}
|
||||
}
|
||||
|
||||
public void ResetSelection()
|
||||
{
|
||||
// If for some reason, we don't have a selected item, fix that.
|
||||
//
|
||||
// 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
|
||||
// ItemView_SelectionChanged again to give us another chance to change
|
||||
// the selection from null -> something. Better to just update the
|
||||
// selection once, at the end of all the updating.
|
||||
if (ItemView.SelectedItem is null)
|
||||
{
|
||||
ItemView.SelectedIndex = 0;
|
||||
}
|
||||
|
||||
// Always reset the selected item when the top-level list page changes
|
||||
// its items
|
||||
if (ViewModel?.IsNested == false)
|
||||
{
|
||||
ItemView.SelectedIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public void Receive(ActivateSecondaryCommandMessage message)
|
||||
{
|
||||
if (ItemView.SelectedItem is ListItemViewModel item)
|
||||
{
|
||||
ViewModel?.InvokeSecondaryCommandCommand.Execute(item);
|
||||
}
|
||||
}
|
||||
|
||||
private enum InputSource
|
||||
{
|
||||
None,
|
||||
Keyboard,
|
||||
Pointer,
|
||||
}
|
||||
}
|
||||
@@ -3,339 +3,14 @@
|
||||
x:Class="Microsoft.CmdPal.UI.ListPage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:cmdpalUI="using:Microsoft.CmdPal.UI"
|
||||
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
|
||||
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
|
||||
xmlns:coreViewModels="using:Microsoft.CmdPal.Core.ViewModels"
|
||||
xmlns:cpcontrols="using:Microsoft.CmdPal.UI.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:help="using:Microsoft.CmdPal.UI.Helpers"
|
||||
xmlns:local="using:Microsoft.CmdPal.UI"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:ui="using:CommunityToolkit.WinUI"
|
||||
xmlns:viewModels="using:Microsoft.CmdPal.UI.ViewModels"
|
||||
x:Name="PageRoot"
|
||||
Background="Transparent"
|
||||
DataContext="{x:Bind ViewModel, Mode=OneWay}"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<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
|
||||
x:Key="StringVisibilityConverter"
|
||||
EmptyValue="Collapsed"
|
||||
NotEmptyValue="Visible" />
|
||||
|
||||
<DataTemplate x:Key="TagTemplate" x:DataType="coreViewModels:TagViewModel">
|
||||
<cpcontrols:Tag
|
||||
AutomationProperties.Name="{x:Bind Text, Mode=OneWay}"
|
||||
BackgroundColor="{x:Bind Background, Mode=OneWay}"
|
||||
FontSize="12"
|
||||
ForegroundColor="{x:Bind Foreground, Mode=OneWay}"
|
||||
Icon="{x:Bind Icon, Mode=OneWay}"
|
||||
Text="{x:Bind Text, Mode=OneWay}"
|
||||
ToolTipService.ToolTip="{x:Bind ToolTip, Mode=OneWay}" />
|
||||
</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 -->
|
||||
<DataTemplate x:Key="ListItemViewModelTemplate" x:DataType="coreViewModels:ListItemViewModel">
|
||||
<Grid
|
||||
Padding="0,12,0,12"
|
||||
AutomationProperties.Name="{x:Bind Title, Mode=OneWay}"
|
||||
ColumnSpacing="12">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="28" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<cpcontrols:IconBox
|
||||
x:Name="IconBorder"
|
||||
Grid.Column="0"
|
||||
Width="20"
|
||||
Height="20"
|
||||
Margin="4,0,4,0"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
|
||||
|
||||
<StackPanel
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
Orientation="Vertical"
|
||||
Spacing="1">
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
CharacterSpacing="12"
|
||||
FontSize="14"
|
||||
Text="{x:Bind Title, Mode=OneWay}"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
TextWrapping="NoWrap" />
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind Subtitle, Mode=OneWay}"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
TextWrapping="NoWrap"
|
||||
Visibility="{x:Bind Subtitle, Mode=OneWay, Converter={StaticResource StringVisibilityConverter}}" />
|
||||
</StackPanel>
|
||||
|
||||
<ItemsControl
|
||||
Grid.RowSpan="2"
|
||||
Grid.Column="2"
|
||||
Margin="0,0,8,0"
|
||||
VerticalAlignment="Center"
|
||||
IsHitTestVisible="False"
|
||||
IsTabStop="False"
|
||||
ItemTemplate="{StaticResource TagTemplate}"
|
||||
ItemsSource="{x:Bind Tags, Mode=OneWay}"
|
||||
Visibility="{x:Bind HasTags, Mode=OneWay}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Horizontal" Spacing="4" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
</ItemsControl>
|
||||
</Grid>
|
||||
</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>
|
||||
|
||||
<Grid>
|
||||
<controls:SwitchPresenter
|
||||
HorizontalAlignment="Stretch"
|
||||
TargetType="x:Boolean"
|
||||
Value="{x:Bind ViewModel.ShowEmptyContent, Mode=OneWay}">
|
||||
<controls:Case Value="False">
|
||||
<controls:SwitchPresenter
|
||||
HorizontalAlignment="Stretch"
|
||||
TargetType="x:Boolean"
|
||||
Value="{x:Bind ViewModel.IsGridView, Mode=OneWay}">
|
||||
<controls:Case Value="False">
|
||||
<ListView
|
||||
x:Name="ItemsList"
|
||||
Padding="0,2,0,0"
|
||||
ContextCanceled="Items_OnContextCanceled"
|
||||
ContextRequested="Items_OnContextRequested"
|
||||
DoubleTapped="Items_DoubleTapped"
|
||||
IsDoubleTapEnabled="True"
|
||||
IsItemClickEnabled="True"
|
||||
ItemClick="Items_ItemClick"
|
||||
ItemTemplate="{StaticResource ListItemViewModelTemplate}"
|
||||
ItemsSource="{x:Bind ViewModel.FilteredItems, Mode=OneWay}"
|
||||
RightTapped="Items_RightTapped"
|
||||
SelectionChanged="Items_SelectionChanged">
|
||||
<ListView.ItemContainerTransitions>
|
||||
<TransitionCollection />
|
||||
</ListView.ItemContainerTransitions>
|
||||
</ListView>
|
||||
</controls:Case>
|
||||
<controls:Case Value="True">
|
||||
<GridView
|
||||
x:Name="ItemsGrid"
|
||||
Padding="8"
|
||||
ContextCanceled="Items_OnContextCanceled"
|
||||
ContextRequested="Items_OnContextRequested"
|
||||
DoubleTapped="Items_DoubleTapped"
|
||||
IsDoubleTapEnabled="True"
|
||||
IsItemClickEnabled="True"
|
||||
ItemClick="Items_ItemClick"
|
||||
ItemTemplateSelector="{StaticResource GridItemTemplateSelector}"
|
||||
ItemsSource="{x:Bind ViewModel.FilteredItems, Mode=OneWay}"
|
||||
RightTapped="Items_RightTapped"
|
||||
SelectionChanged="Items_SelectionChanged">
|
||||
<GridView.ItemContainerTransitions>
|
||||
<TransitionCollection />
|
||||
</GridView.ItemContainerTransitions>
|
||||
</GridView>
|
||||
</controls:Case>
|
||||
</controls:SwitchPresenter>
|
||||
</controls:Case>
|
||||
<controls:Case Value="True">
|
||||
<StackPanel
|
||||
Margin="24"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Orientation="Vertical"
|
||||
Spacing="4">
|
||||
<cpcontrols:IconBox
|
||||
x:Name="IconBorder"
|
||||
Width="48"
|
||||
Height="48"
|
||||
Margin="8"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
SourceKey="{x:Bind ViewModel.EmptyContent.Icon, Mode=OneWay}"
|
||||
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:SwitchPresenter>
|
||||
<cpcontrols:ListViewControl x:Name="ListViewControl" ViewModel="{x:Bind ViewModel, Mode=TwoWay}" />
|
||||
</Grid>
|
||||
</Page>
|
||||
|
||||
@@ -2,33 +2,20 @@
|
||||
// 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 CommunityToolkit.Mvvm.Messaging;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.Helpers;
|
||||
using Microsoft.CmdPal.UI.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.CmdPal.UI.Controls;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Controls.Primitives;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Microsoft.UI.Xaml.Navigation;
|
||||
using Windows.System;
|
||||
|
||||
namespace Microsoft.CmdPal.UI;
|
||||
|
||||
public sealed partial class ListPage : Page,
|
||||
IRecipient<NavigateNextCommand>,
|
||||
IRecipient<NavigatePreviousCommand>,
|
||||
IRecipient<ActivateSelectedListItemMessage>,
|
||||
IRecipient<ActivateSecondaryCommandMessage>
|
||||
{
|
||||
private InputSource _lastInputSource;
|
||||
|
||||
private ListViewModel? ViewModel
|
||||
{
|
||||
get => (ListViewModel?)GetValue(ViewModelProperty);
|
||||
@@ -39,21 +26,14 @@ public sealed partial class ListPage : Page,
|
||||
public static readonly DependencyProperty ViewModelProperty =
|
||||
DependencyProperty.Register(nameof(ViewModel), typeof(ListViewModel), typeof(ListPage), new PropertyMetadata(null, OnViewModelChanged));
|
||||
|
||||
private ListViewBase ItemView
|
||||
{
|
||||
get
|
||||
{
|
||||
return ViewModel?.IsGridView == true ? ItemsGrid : ItemsList;
|
||||
}
|
||||
}
|
||||
|
||||
public ListPage()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
this.NavigationCacheMode = NavigationCacheMode.Disabled;
|
||||
this.ItemView.Loaded += Items_Loaded;
|
||||
this.ItemView.PreviewKeyDown += Items_PreviewKeyDown;
|
||||
this.ItemView.PointerPressed += Items_PointerPressed;
|
||||
}
|
||||
|
||||
private static void OnViewModelChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void OnNavigatedTo(NavigationEventArgs e)
|
||||
@@ -64,17 +44,14 @@ public sealed partial class ListPage : Page,
|
||||
}
|
||||
|
||||
if (e.NavigationMode == NavigationMode.Back
|
||||
|| (e.NavigationMode == NavigationMode.New && ItemView.Items.Count > 0))
|
||||
|| (e.NavigationMode == NavigationMode.New))
|
||||
{
|
||||
// Upon navigating _back_ to this page, immediately select the
|
||||
// first item in the list
|
||||
ItemView.SelectedIndex = 0;
|
||||
ListViewControl.ResetSelection();
|
||||
}
|
||||
|
||||
// RegisterAll isn't AOT compatible
|
||||
WeakReferenceMessenger.Default.Register<NavigateNextCommand>(this);
|
||||
WeakReferenceMessenger.Default.Register<NavigatePreviousCommand>(this);
|
||||
WeakReferenceMessenger.Default.Register<ActivateSelectedListItemMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<ActivateSecondaryCommandMessage>(this);
|
||||
|
||||
base.OnNavigatedTo(e);
|
||||
@@ -84,17 +61,8 @@ public sealed partial class ListPage : Page,
|
||||
{
|
||||
base.OnNavigatingFrom(e);
|
||||
|
||||
WeakReferenceMessenger.Default.Unregister<NavigateNextCommand>(this);
|
||||
WeakReferenceMessenger.Default.Unregister<NavigatePreviousCommand>(this);
|
||||
WeakReferenceMessenger.Default.Unregister<ActivateSelectedListItemMessage>(this);
|
||||
WeakReferenceMessenger.Default.Unregister<ActivateSecondaryCommandMessage>(this);
|
||||
|
||||
if (ViewModel is not null)
|
||||
{
|
||||
ViewModel.PropertyChanged -= ViewModel_PropertyChanged;
|
||||
ViewModel.ItemsUpdated -= Page_ItemsUpdated;
|
||||
}
|
||||
|
||||
if (e.NavigationMode != NavigationMode.New)
|
||||
{
|
||||
ViewModel?.SafeCleanup();
|
||||
@@ -107,324 +75,16 @@ public sealed partial class ListPage : Page,
|
||||
GC.Collect();
|
||||
}
|
||||
|
||||
[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)
|
||||
{
|
||||
if (e.ClickedItem is ListItemViewModel item)
|
||||
{
|
||||
if (_lastInputSource == InputSource.Keyboard)
|
||||
{
|
||||
ViewModel?.InvokeItemCommand.Execute(item);
|
||||
return;
|
||||
}
|
||||
|
||||
var settings = App.Current.Services.GetService<SettingsModel>()!;
|
||||
if (settings.SingleClickActivates)
|
||||
{
|
||||
ViewModel?.InvokeItemCommand.Execute(item);
|
||||
}
|
||||
else
|
||||
{
|
||||
ViewModel?.UpdateSelectedItemCommand.Execute(item);
|
||||
WeakReferenceMessenger.Default.Send<FocusSearchBoxMessage>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Items_DoubleTapped(object sender, DoubleTappedRoutedEventArgs e)
|
||||
{
|
||||
if (ItemView.SelectedItem is ListItemViewModel vm)
|
||||
{
|
||||
var settings = App.Current.Services.GetService<SettingsModel>()!;
|
||||
if (!settings.SingleClickActivates)
|
||||
{
|
||||
ViewModel?.InvokeItemCommand.Execute(vm);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "VS is too aggressive at pruning methods bound in XAML")]
|
||||
private void Items_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
var vm = ViewModel;
|
||||
var li = ItemView.SelectedItem as ListItemViewModel;
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
vm?.UpdateSelectedItemCommand.Execute(li);
|
||||
});
|
||||
|
||||
// There's mysterious behavior here, where the selection seemingly
|
||||
// changes to _nothing_ when we're backspacing to a single character.
|
||||
// And at that point, seemingly the item that's getting removed is not
|
||||
// a member of FilteredItems. Very bizarre.
|
||||
//
|
||||
// Might be able to fix in the future by stashing the removed item
|
||||
// here, then in Page_ItemsUpdated trying to select that cached item if
|
||||
// it's in the list (otherwise, clear the cache), but that seems
|
||||
// aggressively BODGY for something that mostly just works today.
|
||||
if (ItemView.SelectedItem is not null)
|
||||
{
|
||||
ItemView.ScrollIntoView(ItemView.SelectedItem);
|
||||
|
||||
// Automation notification for screen readers
|
||||
var listViewPeer = Microsoft.UI.Xaml.Automation.Peers.ListViewAutomationPeer.CreatePeerForElement(ItemView);
|
||||
if (listViewPeer is not null && li is not null)
|
||||
{
|
||||
var notificationText = li.Title;
|
||||
|
||||
UIHelper.AnnounceActionForAccessibility(
|
||||
ItemsList,
|
||||
notificationText,
|
||||
"CommandPaletteSelectedItemChanged");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Items_RightTapped(object sender, RightTappedRoutedEventArgs e)
|
||||
{
|
||||
if (e.OriginalSource is FrameworkElement element &&
|
||||
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)
|
||||
{
|
||||
listViewScrollViewer.ViewChanged += ListViewScrollViewer_ViewChanged;
|
||||
}
|
||||
}
|
||||
|
||||
private void ListViewScrollViewer_ViewChanged(object? sender, ScrollViewerViewChangedEventArgs e)
|
||||
{
|
||||
var scrollView = sender as ScrollViewer;
|
||||
if (scrollView is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// When we get to the bottom, request more from the extension, if they
|
||||
// have more to give us.
|
||||
// We're checking when we get to 80% of the scroll height, to give the
|
||||
// extension a bit of a heads-up before the user actually gets there.
|
||||
if (scrollView.VerticalOffset >= (scrollView.ScrollableHeight * .8))
|
||||
{
|
||||
ViewModel?.LoadMoreIfNeeded();
|
||||
}
|
||||
}
|
||||
|
||||
public void Receive(NavigateNextCommand message)
|
||||
{
|
||||
// Note: We may want to just have the notion of a 'SelectedCommand' in our VM
|
||||
// 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
|
||||
// At least it's decoupled from the SearchBox now :)
|
||||
if (ItemView.SelectedIndex < ItemView.Items.Count - 1)
|
||||
{
|
||||
ItemView.SelectedIndex++;
|
||||
}
|
||||
else
|
||||
{
|
||||
ItemView.SelectedIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public void Receive(NavigatePreviousCommand message)
|
||||
{
|
||||
if (ItemView.SelectedIndex > 0)
|
||||
{
|
||||
ItemView.SelectedIndex--;
|
||||
}
|
||||
else
|
||||
{
|
||||
ItemView.SelectedIndex = ItemView.Items.Count - 1;
|
||||
}
|
||||
}
|
||||
|
||||
public void Receive(ActivateSelectedListItemMessage message)
|
||||
{
|
||||
if (ViewModel?.ShowEmptyContent ?? false)
|
||||
{
|
||||
ViewModel?.InvokeItemCommand.Execute(null);
|
||||
}
|
||||
else if (ItemView.SelectedItem is ListItemViewModel item)
|
||||
{
|
||||
ViewModel?.InvokeItemCommand.Execute(item);
|
||||
}
|
||||
}
|
||||
|
||||
public void Receive(ActivateSecondaryCommandMessage message)
|
||||
{
|
||||
if (ViewModel?.ShowEmptyContent ?? false)
|
||||
{
|
||||
ViewModel?.InvokeSecondaryCommandCommand.Execute(null);
|
||||
}
|
||||
else if (ItemView.SelectedItem is ListItemViewModel item)
|
||||
{
|
||||
ViewModel?.InvokeSecondaryCommandCommand.Execute(item);
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnViewModelChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is ListPage @this)
|
||||
{
|
||||
if (e.OldValue is ListViewModel old)
|
||||
{
|
||||
old.PropertyChanged -= @this.ViewModel_PropertyChanged;
|
||||
old.ItemsUpdated -= @this.Page_ItemsUpdated;
|
||||
}
|
||||
|
||||
if (e.NewValue is ListViewModel page)
|
||||
{
|
||||
page.PropertyChanged += @this.ViewModel_PropertyChanged;
|
||||
page.ItemsUpdated += @this.Page_ItemsUpdated;
|
||||
}
|
||||
else if (e.NewValue is null)
|
||||
{
|
||||
Logger.LogDebug("cleared view model");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Called after we've finished updating the whole list for either a
|
||||
// GetItems or a change in the filter.
|
||||
private void Page_ItemsUpdated(ListViewModel sender, object args)
|
||||
{
|
||||
// If for some reason, we don't have a selected item, fix that.
|
||||
//
|
||||
// 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
|
||||
// ItemView_SelectionChanged again to give us another chance to change
|
||||
// the selection from null -> something. Better to just update the
|
||||
// selection once, at the end of all the updating.
|
||||
if (ItemView.SelectedItem is null)
|
||||
{
|
||||
ItemView.SelectedIndex = 0;
|
||||
}
|
||||
|
||||
// Always reset the selected item when the top-level list page changes
|
||||
// its items
|
||||
if (!sender.IsNested)
|
||||
{
|
||||
ItemView.SelectedIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private void ViewModel_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
{
|
||||
var prop = e.PropertyName;
|
||||
if (prop == nameof(ViewModel.FilteredItems))
|
||||
{
|
||||
Debug.WriteLine($"ViewModel.FilteredItems {ItemView.SelectedItem}");
|
||||
}
|
||||
}
|
||||
|
||||
private ScrollViewer? FindScrollViewer(DependencyObject parent)
|
||||
{
|
||||
if (parent is ScrollViewer)
|
||||
{
|
||||
return (ScrollViewer)parent;
|
||||
}
|
||||
|
||||
for (var i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
|
||||
{
|
||||
var child = VisualTreeHelper.GetChild(parent, i);
|
||||
var result = FindScrollViewer(child);
|
||||
if (result is not null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void Items_OnContextRequested(UIElement sender, ContextRequestedEventArgs e)
|
||||
{
|
||||
var (item, element) = e.OriginalSource switch
|
||||
{
|
||||
// caused by keyboard shortcut (e.g. Context menu key or Shift+F10)
|
||||
SelectorItem selectorItem => (ItemView.ItemFromContainer(selectorItem) as ListItemViewModel, selectorItem),
|
||||
|
||||
// caused by right-click on the ListViewItem
|
||||
FrameworkElement { DataContext: ListItemViewModel itemViewModel } frameworkElement => (itemViewModel, frameworkElement),
|
||||
|
||||
_ => (null, null),
|
||||
};
|
||||
|
||||
if (item is null || element is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (ItemView.SelectedItem != item)
|
||||
{
|
||||
ItemView.SelectedItem = item;
|
||||
}
|
||||
|
||||
ViewModel?.UpdateSelectedItemCommand.Execute(item);
|
||||
|
||||
if (!e.TryGetPosition(element, out var pos))
|
||||
{
|
||||
pos = new(0, element.ActualHeight);
|
||||
}
|
||||
|
||||
_ = DispatcherQueue.TryEnqueue(
|
||||
() =>
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<OpenContextMenuMessage>(
|
||||
new OpenContextMenuMessage(
|
||||
element,
|
||||
Microsoft.UI.Xaml.Controls.Primitives.FlyoutPlacementMode.BottomEdgeAlignedLeft,
|
||||
pos,
|
||||
ContextMenuFilterLocation.Top));
|
||||
});
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void Items_OnContextCanceled(UIElement sender, RoutedEventArgs e)
|
||||
{
|
||||
_ = DispatcherQueue.TryEnqueue(() => WeakReferenceMessenger.Default.Send<CloseContextMenuMessage>());
|
||||
}
|
||||
|
||||
private void Items_PointerPressed(object sender, PointerRoutedEventArgs e) => _lastInputSource = InputSource.Pointer;
|
||||
|
||||
private void Items_PreviewKeyDown(object sender, KeyRoutedEventArgs e)
|
||||
{
|
||||
if (e.Key is VirtualKey.Enter or VirtualKey.Space)
|
||||
{
|
||||
_lastInputSource = InputSource.Keyboard;
|
||||
}
|
||||
}
|
||||
|
||||
private enum InputSource
|
||||
{
|
||||
None,
|
||||
Keyboard,
|
||||
Pointer,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +66,7 @@
|
||||
<None Remove="Controls\ActionBar.xaml" />
|
||||
<None Remove="Controls\SearchBar.xaml" />
|
||||
<None Remove="ListDetailPage.xaml" />
|
||||
<None Remove="ListViewControl.xaml" />
|
||||
<None Remove="LoadingPage.xaml" />
|
||||
<None Remove="MainPage.xaml" />
|
||||
<None Remove="Pages\Settings\ExtensionsPage.xaml" />
|
||||
@@ -189,6 +190,12 @@
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Page Update="ListViewControl.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="Styles\Colors.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
|
||||
Reference in New Issue
Block a user