mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-16 03:37:59 +01:00
CmdPal's search bar now accepts page up/down keyboard strokes. (#41886)
<!-- Enter a brief description/summary of your PR here. What does it fix/what does it change/how was it tested (even manually, if necessary)? --> ## Summary of the Pull Request The page up/down keys now function while the search box is focused. <!-- Please review the items on the PR checklist before submitting--> ## PR Checklist - [ X ] Closes: #41877 - [ ] **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 <!-- Provide a more detailed description of the PR, other things fixed, or any additional comments/features here --> ## Detailed Description of the Pull Request / Additional comments Previously, the page up/down keys only performed any action while an item in the list was focused. The page up/down keys did not have any effect while the search box was focused, however the up/down arrows do have effect. This PR enables the page up/down keys while the search box is focused. There is a caveat here. The page up/down behavior is not consistent. I do not see a way to tell the ListView to perform its native page up/down function. Instead, I manually calculate roughly which item to scroll-to. Because of this, the amount of scroll between when the search box is focused and when an item in the ListView is focused is not consistent. <!-- Describe how you validated the behavior. Add automated tests wherever possible, but list manual validation steps taken as well --> ## Validation Steps Performed  In this GIF: 1. CmdPal appears 2. SearchBar focused, down/up arrow keys. 3. SearchBar focused, page down/up keys. 4. Tab to item in ListView 5. ListView item focused down/up arrow keys. 6. ListView item focused page down/up keys. 7. SearchBar focused 8. Filter "abc" 9. SearchBar focused page down/up keys.
This commit is contained in:
@@ -18,6 +18,7 @@ using Microsoft.UI.Xaml.Controls.Primitives;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Microsoft.UI.Xaml.Navigation;
|
||||
using Windows.Foundation;
|
||||
using Windows.System;
|
||||
|
||||
namespace Microsoft.CmdPal.UI;
|
||||
@@ -25,6 +26,8 @@ namespace Microsoft.CmdPal.UI;
|
||||
public sealed partial class ListPage : Page,
|
||||
IRecipient<NavigateNextCommand>,
|
||||
IRecipient<NavigatePreviousCommand>,
|
||||
IRecipient<NavigatePageDownCommand>,
|
||||
IRecipient<NavigatePageUpCommand>,
|
||||
IRecipient<ActivateSelectedListItemMessage>,
|
||||
IRecipient<ActivateSecondaryCommandMessage>
|
||||
{
|
||||
@@ -82,6 +85,8 @@ public sealed partial class ListPage : Page,
|
||||
// RegisterAll isn't AOT compatible
|
||||
WeakReferenceMessenger.Default.Register<NavigateNextCommand>(this);
|
||||
WeakReferenceMessenger.Default.Register<NavigatePreviousCommand>(this);
|
||||
WeakReferenceMessenger.Default.Register<NavigatePageDownCommand>(this);
|
||||
WeakReferenceMessenger.Default.Register<NavigatePageUpCommand>(this);
|
||||
WeakReferenceMessenger.Default.Register<ActivateSelectedListItemMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<ActivateSecondaryCommandMessage>(this);
|
||||
|
||||
@@ -94,6 +99,8 @@ public sealed partial class ListPage : Page,
|
||||
|
||||
WeakReferenceMessenger.Default.Unregister<NavigateNextCommand>(this);
|
||||
WeakReferenceMessenger.Default.Unregister<NavigatePreviousCommand>(this);
|
||||
WeakReferenceMessenger.Default.Unregister<NavigatePageDownCommand>(this);
|
||||
WeakReferenceMessenger.Default.Unregister<NavigatePageUpCommand>(this);
|
||||
WeakReferenceMessenger.Default.Unregister<ActivateSelectedListItemMessage>(this);
|
||||
WeakReferenceMessenger.Default.Unregister<ActivateSecondaryCommandMessage>(this);
|
||||
|
||||
@@ -181,9 +188,9 @@ public sealed partial class ListPage : Page,
|
||||
var notificationText = li.Title;
|
||||
|
||||
UIHelper.AnnounceActionForAccessibility(
|
||||
ItemsList,
|
||||
notificationText,
|
||||
"CommandPaletteSelectedItemChanged");
|
||||
ItemsList,
|
||||
notificationText,
|
||||
"CommandPaletteSelectedItemChanged");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -296,6 +303,142 @@ public sealed partial class ListPage : Page,
|
||||
}
|
||||
}
|
||||
|
||||
public void Receive(NavigatePageDownCommand message)
|
||||
{
|
||||
var indexes = CalculateTargetIndexPageUpDownScrollTo(true);
|
||||
if (indexes is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (indexes.Value.CurrentIndex != indexes.Value.TargetIndex)
|
||||
{
|
||||
ItemView.SelectedIndex = indexes.Value.TargetIndex;
|
||||
ItemView.ScrollIntoView(ItemView.SelectedItem);
|
||||
}
|
||||
}
|
||||
|
||||
public void Receive(NavigatePageUpCommand message)
|
||||
{
|
||||
var indexes = CalculateTargetIndexPageUpDownScrollTo(false);
|
||||
if (indexes is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (indexes.Value.CurrentIndex != indexes.Value.TargetIndex)
|
||||
{
|
||||
ItemView.SelectedIndex = indexes.Value.TargetIndex;
|
||||
ItemView.ScrollIntoView(ItemView.SelectedItem);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the item index to target when performing a page up or page down
|
||||
/// navigation. The calculation attempts to estimate how many items fit into
|
||||
/// the visible viewport by measuring actual container heights currently visible
|
||||
/// within the internal ScrollViewer. If measurements are not available a
|
||||
/// fallback estimate is used.
|
||||
/// </summary>
|
||||
/// <param name="isPageDown">True to calculate a page-down target, false for page-up.</param>
|
||||
/// <returns>
|
||||
/// A tuple containing the current index and the calculated target index, or null
|
||||
/// if a valid calculation could not be performed (for example, missing ScrollViewer).
|
||||
/// </returns>
|
||||
private (int CurrentIndex, int TargetIndex)? CalculateTargetIndexPageUpDownScrollTo(bool isPageDown)
|
||||
{
|
||||
var scroll = FindScrollViewer(ItemView);
|
||||
if (scroll is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var viewportHeight = scroll.ViewportHeight;
|
||||
if (viewportHeight <= 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var currentIndex = ItemView.SelectedIndex < 0 ? 0 : ItemView.SelectedIndex;
|
||||
var itemCount = ItemView.Items.Count;
|
||||
|
||||
// Compute visible item heights within the ScrollViewer viewport
|
||||
const int firstVisibleIndexNotFound = -1;
|
||||
var firstVisibleIndex = firstVisibleIndexNotFound;
|
||||
var visibleHeights = new List<double>(itemCount);
|
||||
|
||||
for (var i = 0; i < itemCount; i++)
|
||||
{
|
||||
if (ItemView.ContainerFromIndex(i) is FrameworkElement container)
|
||||
{
|
||||
try
|
||||
{
|
||||
var transform = container.TransformToVisual(scroll);
|
||||
var topLeft = transform.TransformPoint(new Point(0, 0));
|
||||
var bottom = topLeft.Y + container.ActualHeight;
|
||||
|
||||
// If any part of the container is inside the viewport, consider it visible
|
||||
if (topLeft.Y >= 0 && bottom <= viewportHeight)
|
||||
{
|
||||
if (firstVisibleIndex == firstVisibleIndexNotFound)
|
||||
{
|
||||
firstVisibleIndex = i;
|
||||
}
|
||||
|
||||
visibleHeights.Add(container.ActualHeight > 0 ? container.ActualHeight : 0);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore transform errors and continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var itemsPerPage = 0;
|
||||
|
||||
// Calculate how many items fit in the viewport based on their actual heights
|
||||
if (visibleHeights.Count > 0)
|
||||
{
|
||||
double accumulated = 0;
|
||||
for (var i = 0; i < visibleHeights.Count; i++)
|
||||
{
|
||||
accumulated += visibleHeights[i] <= 0 ? 1 : visibleHeights[i];
|
||||
itemsPerPage++;
|
||||
if (accumulated >= viewportHeight)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// fallback: estimate using first measured container height
|
||||
double itemHeight = 0;
|
||||
for (var i = currentIndex; i < itemCount; i++)
|
||||
{
|
||||
if (ItemView.ContainerFromIndex(i) is FrameworkElement { ActualHeight: > 0 } c)
|
||||
{
|
||||
itemHeight = c.ActualHeight;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (itemHeight <= 0)
|
||||
{
|
||||
itemHeight = 1;
|
||||
}
|
||||
|
||||
itemsPerPage = Math.Max(1, (int)Math.Floor(viewportHeight / itemHeight));
|
||||
}
|
||||
|
||||
var targetIndex = isPageDown
|
||||
? Math.Min(itemCount - 1, currentIndex + Math.Max(1, itemsPerPage))
|
||||
: Math.Max(0, currentIndex - Math.Max(1, itemsPerPage));
|
||||
|
||||
return (currentIndex, targetIndex);
|
||||
}
|
||||
|
||||
private static void OnViewModelChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is ListPage @this)
|
||||
@@ -351,11 +494,11 @@ public sealed partial class ListPage : Page,
|
||||
}
|
||||
}
|
||||
|
||||
private ScrollViewer? FindScrollViewer(DependencyObject parent)
|
||||
private static ScrollViewer? FindScrollViewer(DependencyObject parent)
|
||||
{
|
||||
if (parent is ScrollViewer)
|
||||
if (parent is ScrollViewer viewer)
|
||||
{
|
||||
return (ScrollViewer)parent;
|
||||
return viewer;
|
||||
}
|
||||
|
||||
for (var i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
|
||||
|
||||
Reference in New Issue
Block a user