diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ListViewControl.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ListViewControl.xaml
new file mode 100644
index 0000000000..cba09d4767
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ListViewControl.xaml
@@ -0,0 +1,329 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ListViewControl.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ListViewControl.xaml.cs
new file mode 100644
index 0000000000..3b8846f262
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ListViewControl.xaml.cs
@@ -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,
+ IRecipient,
+ IRecipient,
+ IRecipient
+{
+ 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(this);
+ WeakReferenceMessenger.Default.Register(this);
+ WeakReferenceMessenger.Default.Register(this);
+ WeakReferenceMessenger.Default.Register(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()!;
+ if (settings.SingleClickActivates)
+ {
+ ViewModel?.InvokeItemCommand.Execute(item);
+ }
+ else
+ {
+ ViewModel?.UpdateSelectedItemCommand.Execute(item);
+ WeakReferenceMessenger.Default.Send();
+ }
+ }
+ }
+
+ private void Items_DoubleTapped(object sender, DoubleTappedRoutedEventArgs e)
+ {
+ if (ItemView.SelectedItem is ListItemViewModel vm)
+ {
+ var settings = App.Current.Services.GetService()!;
+ 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(
+ 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(
+ 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());
+ }
+
+ 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,
+ }
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml
index 676a676f95..b64606ef65 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml
@@ -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">
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml.cs
index a1544e03fa..17ab3a22d0 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml.cs
@@ -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,
- IRecipient,
- IRecipient,
IRecipient
{
- 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(this);
- WeakReferenceMessenger.Default.Register(this);
- WeakReferenceMessenger.Default.Register(this);
WeakReferenceMessenger.Default.Register(this);
base.OnNavigatedTo(e);
@@ -84,17 +61,8 @@ public sealed partial class ListPage : Page,
{
base.OnNavigatingFrom(e);
- WeakReferenceMessenger.Default.Unregister(this);
- WeakReferenceMessenger.Default.Unregister(this);
- WeakReferenceMessenger.Default.Unregister(this);
WeakReferenceMessenger.Default.Unregister(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()!;
- if (settings.SingleClickActivates)
- {
- ViewModel?.InvokeItemCommand.Execute(item);
- }
- else
- {
- ViewModel?.UpdateSelectedItemCommand.Execute(item);
- WeakReferenceMessenger.Default.Send();
- }
- }
- }
-
- private void Items_DoubleTapped(object sender, DoubleTappedRoutedEventArgs e)
- {
- if (ItemView.SelectedItem is ListItemViewModel vm)
- {
- var settings = App.Current.Services.GetService()!;
- 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(
- 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(
- 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());
}
-
- 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,
- }
}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj b/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj
index 14689413c1..68e79b0652 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj
@@ -66,6 +66,7 @@
+
@@ -189,6 +190,12 @@
PreserveNewest
+
+
+
+ MSBuild:Compile
+
+
MSBuild:Compile