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