mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-16 11:48:06 +01:00
## Summary of the Pull Request <!-- Please review the items on the PR checklist before submitting--> ## PR Checklist - [ x ] **Closes:** #38392 - [ x ] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [ ] **Tests:** Added/updated and all pass - [ ] **Localization:** All end-user-facing strings can be localized - [ ] **Dev docs:** Added/updated - [ ] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx ## Detailed Description of the Pull Request / Additional comments Add AutomationNotification to ItemsList_SelectionChanged so that when user uses keyboard navigation, it sends the title to be read by the screen reader ## Validation Steps Performed https://github.com/user-attachments/assets/34a11e55-18ce-440f-97d8-e6ea60c57f78 Co-authored-by: Mike Griese <migrie@microsoft.com>
345 lines
12 KiB
C#
345 lines
12 KiB
C#
// 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 ManagedCommon;
|
|
using Microsoft.CmdPal.Core.ViewModels;
|
|
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
|
using Microsoft.CmdPal.UI.ViewModels;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.UI.Xaml;
|
|
using Microsoft.UI.Xaml.Controls;
|
|
using Microsoft.UI.Xaml.Input;
|
|
using Microsoft.UI.Xaml.Media;
|
|
using Microsoft.UI.Xaml.Navigation;
|
|
|
|
namespace Microsoft.CmdPal.UI;
|
|
|
|
public sealed partial class ListPage : Page,
|
|
IRecipient<NavigateNextCommand>,
|
|
IRecipient<NavigatePreviousCommand>,
|
|
IRecipient<ActivateSelectedListItemMessage>,
|
|
IRecipient<ActivateSecondaryCommandMessage>
|
|
{
|
|
private 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));
|
|
|
|
public ListPage()
|
|
{
|
|
this.InitializeComponent();
|
|
this.NavigationCacheMode = NavigationCacheMode.Disabled;
|
|
this.ItemsList.Loaded += ItemsList_Loaded;
|
|
}
|
|
|
|
protected override void OnNavigatedTo(NavigationEventArgs e)
|
|
{
|
|
if (e.Parameter is ListViewModel lvm)
|
|
{
|
|
ViewModel = lvm;
|
|
}
|
|
|
|
if (e.NavigationMode == NavigationMode.Back
|
|
|| (e.NavigationMode == NavigationMode.New && ItemsList.Items.Count > 0))
|
|
{
|
|
// Upon navigating _back_ to this page, immediately select the
|
|
// first item in the list
|
|
ItemsList.SelectedIndex = 0;
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
|
|
{
|
|
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 != null)
|
|
{
|
|
ViewModel.PropertyChanged -= ViewModel_PropertyChanged;
|
|
ViewModel.ItemsUpdated -= Page_ItemsUpdated;
|
|
}
|
|
|
|
if (e.NavigationMode != NavigationMode.New)
|
|
{
|
|
ViewModel?.SafeCleanup();
|
|
CleanupHelper.Cleanup(this);
|
|
Bindings.StopTracking();
|
|
}
|
|
|
|
// Clean-up event listeners
|
|
ViewModel = null;
|
|
|
|
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 ItemsList_ItemClick(object sender, ItemClickEventArgs e)
|
|
{
|
|
if (e.ClickedItem is ListItemViewModel item)
|
|
{
|
|
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 ItemsList_DoubleTapped(object sender, DoubleTappedRoutedEventArgs e)
|
|
{
|
|
if (ItemsList.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 ItemsList_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
|
{
|
|
var vm = ViewModel;
|
|
var li = ItemsList.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 (ItemsList.SelectedItem != null)
|
|
{
|
|
ItemsList.ScrollIntoView(ItemsList.SelectedItem);
|
|
|
|
// Automation notification for screen readers
|
|
var listViewPeer = Microsoft.UI.Xaml.Automation.Peers.ListViewAutomationPeer.CreatePeerForElement(ItemsList);
|
|
if (listViewPeer != null && li != null)
|
|
{
|
|
var notificationText = li.Title;
|
|
listViewPeer.RaiseNotificationEvent(
|
|
Microsoft.UI.Xaml.Automation.Peers.AutomationNotificationKind.Other,
|
|
Microsoft.UI.Xaml.Automation.Peers.AutomationNotificationProcessing.MostRecent,
|
|
notificationText,
|
|
"CommandPaletteSelectedItemChanged");
|
|
}
|
|
}
|
|
}
|
|
|
|
private void ItemsList_Loaded(object sender, RoutedEventArgs e)
|
|
{
|
|
// Find the ScrollViewer in the ListView
|
|
var listViewScrollViewer = FindScrollViewer(this.ItemsList);
|
|
|
|
if (listViewScrollViewer != null)
|
|
{
|
|
listViewScrollViewer.ViewChanged += ListViewScrollViewer_ViewChanged;
|
|
}
|
|
}
|
|
|
|
private void ListViewScrollViewer_ViewChanged(object? sender, ScrollViewerViewChangedEventArgs e)
|
|
{
|
|
var scrollView = sender as ScrollViewer;
|
|
if (scrollView == 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 (ItemsList.SelectedIndex < ItemsList.Items.Count - 1)
|
|
{
|
|
ItemsList.SelectedIndex++;
|
|
}
|
|
else
|
|
{
|
|
ItemsList.SelectedIndex = 0;
|
|
}
|
|
}
|
|
|
|
public void Receive(NavigatePreviousCommand message)
|
|
{
|
|
if (ItemsList.SelectedIndex > 0)
|
|
{
|
|
ItemsList.SelectedIndex--;
|
|
}
|
|
else
|
|
{
|
|
ItemsList.SelectedIndex = ItemsList.Items.Count - 1;
|
|
}
|
|
}
|
|
|
|
public void Receive(ActivateSelectedListItemMessage message)
|
|
{
|
|
if (ViewModel?.ShowEmptyContent ?? false)
|
|
{
|
|
ViewModel?.InvokeItemCommand.Execute(null);
|
|
}
|
|
else if (ItemsList.SelectedItem is ListItemViewModel item)
|
|
{
|
|
ViewModel?.InvokeItemCommand.Execute(item);
|
|
}
|
|
}
|
|
|
|
public void Receive(ActivateSecondaryCommandMessage message)
|
|
{
|
|
if (ViewModel?.ShowEmptyContent ?? false)
|
|
{
|
|
ViewModel?.InvokeSecondaryCommandCommand.Execute(null);
|
|
}
|
|
else if (ItemsList.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 == 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
|
|
// ItemsList_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 (ItemsList.SelectedItem == null)
|
|
{
|
|
ItemsList.SelectedIndex = 0;
|
|
}
|
|
|
|
// Always reset the selected item when the top-level list page changes
|
|
// its items
|
|
if (!sender.IsNested)
|
|
{
|
|
ItemsList.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 {ItemsList.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 != null)
|
|
{
|
|
return result;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private void ItemsList_RightTapped(object sender, RightTappedRoutedEventArgs e)
|
|
{
|
|
if (e.OriginalSource is FrameworkElement element &&
|
|
element.DataContext is ListItemViewModel item)
|
|
{
|
|
if (ItemsList.SelectedItem != item)
|
|
{
|
|
ItemsList.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));
|
|
});
|
|
}
|
|
}
|
|
}
|