mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-16 11:48:06 +01:00
CmdPal: Arrow keys move logical grid pages (#43870)
<!-- 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 <!-- Please review the items on the PR checklist before submitting--> ## PR Checklist - [X] Closes: #41939 <!-- - [ ] Closes: #yyy (add separate lines for additional resolved issues) --> - [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 <!-- 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 <!-- Describe how you validated the behavior. Add automated tests wherever possible, but list manual validation steps taken as well --> ## Validation Steps Performed Before  After  I noticed the double "active" line around the items when the ListPage is focused. I was unable to find where that is defined. Ideally, the black-border would go away. I tested with AOT turned on. The behavior accounts for suggestions. If the SearchBar is focused and there is a suggestion, right-arrow will [continue] to complete the suggestion.
This commit is contained in:
@@ -0,0 +1,10 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
namespace Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Used to navigate left in a grid view when pressing the Left arrow key in the SearchBox.
|
||||||
|
/// </summary>
|
||||||
|
public record NavigateLeftCommand;
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
namespace Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Used to navigate right in a grid view when pressing the Right arrow key in the SearchBox.
|
||||||
|
/// </summary>
|
||||||
|
public record NavigateRightCommand;
|
||||||
@@ -208,22 +208,33 @@ public sealed partial class SearchBar : UserControl,
|
|||||||
|
|
||||||
e.Handled = true;
|
e.Handled = true;
|
||||||
}
|
}
|
||||||
|
else if (e.Key == VirtualKey.Left)
|
||||||
|
{
|
||||||
|
// Check if we're in a grid view, and if so, send grid navigation command
|
||||||
|
var isGridView = CurrentPageViewModel is ListViewModel { IsGridView: true };
|
||||||
|
|
||||||
|
// Special handling is required if we're in grid view.
|
||||||
|
if (isGridView)
|
||||||
|
{
|
||||||
|
WeakReferenceMessenger.Default.Send<NavigateLeftCommand>();
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
else if (e.Key == VirtualKey.Right)
|
else if (e.Key == VirtualKey.Right)
|
||||||
{
|
{
|
||||||
// Check if the "replace search text with suggestion" feature from 0.4-0.5 is enabled.
|
// Check if the "replace search text with suggestion" feature from 0.4-0.5 is enabled.
|
||||||
// If it isn't, then only use the suggestion when the caret is at the end of the input.
|
// If it isn't, then only use the suggestion when the caret is at the end of the input.
|
||||||
if (!IsTextToSuggestEnabled)
|
if (!IsTextToSuggestEnabled)
|
||||||
{
|
{
|
||||||
if (_textToSuggest != null &&
|
if (!string.IsNullOrEmpty(_textToSuggest) &&
|
||||||
FilterBox.SelectionStart == FilterBox.Text.Length)
|
FilterBox.SelectionStart == FilterBox.Text.Length)
|
||||||
{
|
{
|
||||||
FilterBox.Text = _textToSuggest;
|
FilterBox.Text = _textToSuggest;
|
||||||
FilterBox.Select(_textToSuggest.Length, 0);
|
FilterBox.Select(_textToSuggest.Length, 0);
|
||||||
e.Handled = true;
|
e.Handled = true;
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Here, we're using the "replace search text with suggestion" feature.
|
// Here, we're using the "replace search text with suggestion" feature.
|
||||||
if (InSuggestion)
|
if (InSuggestion)
|
||||||
@@ -232,6 +243,20 @@ public sealed partial class SearchBar : UserControl,
|
|||||||
_lastText = null;
|
_lastText = null;
|
||||||
DoFilterBoxUpdate();
|
DoFilterBoxUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wouldn't want to perform text completion *and* move the selected item, so only perform this if text suggestion wasn't performed.
|
||||||
|
if (!e.Handled)
|
||||||
|
{
|
||||||
|
// Check if we're in a grid view, and if so, send grid navigation command
|
||||||
|
var isGridView = CurrentPageViewModel is ListViewModel { IsGridView: true };
|
||||||
|
|
||||||
|
// Special handling is required if we're in grid view.
|
||||||
|
if (isGridView)
|
||||||
|
{
|
||||||
|
WeakReferenceMessenger.Default.Send<NavigateRightCommand>();
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (e.Key == VirtualKey.Down)
|
else if (e.Key == VirtualKey.Down)
|
||||||
{
|
{
|
||||||
@@ -274,6 +299,8 @@ public sealed partial class SearchBar : UserControl,
|
|||||||
|
|
||||||
e.Key == VirtualKey.Up ||
|
e.Key == VirtualKey.Up ||
|
||||||
e.Key == VirtualKey.Down ||
|
e.Key == VirtualKey.Down ||
|
||||||
|
e.Key == VirtualKey.Left ||
|
||||||
|
e.Key == VirtualKey.Right ||
|
||||||
|
|
||||||
e.Key == VirtualKey.RightMenu ||
|
e.Key == VirtualKey.RightMenu ||
|
||||||
e.Key == VirtualKey.LeftMenu ||
|
e.Key == VirtualKey.LeftMenu ||
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ namespace Microsoft.CmdPal.UI;
|
|||||||
public sealed partial class ListPage : Page,
|
public sealed partial class ListPage : Page,
|
||||||
IRecipient<NavigateNextCommand>,
|
IRecipient<NavigateNextCommand>,
|
||||||
IRecipient<NavigatePreviousCommand>,
|
IRecipient<NavigatePreviousCommand>,
|
||||||
|
IRecipient<NavigateLeftCommand>,
|
||||||
|
IRecipient<NavigateRightCommand>,
|
||||||
IRecipient<NavigatePageDownCommand>,
|
IRecipient<NavigatePageDownCommand>,
|
||||||
IRecipient<NavigatePageUpCommand>,
|
IRecipient<NavigatePageUpCommand>,
|
||||||
IRecipient<ActivateSelectedListItemMessage>,
|
IRecipient<ActivateSelectedListItemMessage>,
|
||||||
@@ -85,6 +87,8 @@ public sealed partial class ListPage : Page,
|
|||||||
// RegisterAll isn't AOT compatible
|
// RegisterAll isn't AOT compatible
|
||||||
WeakReferenceMessenger.Default.Register<NavigateNextCommand>(this);
|
WeakReferenceMessenger.Default.Register<NavigateNextCommand>(this);
|
||||||
WeakReferenceMessenger.Default.Register<NavigatePreviousCommand>(this);
|
WeakReferenceMessenger.Default.Register<NavigatePreviousCommand>(this);
|
||||||
|
WeakReferenceMessenger.Default.Register<NavigateLeftCommand>(this);
|
||||||
|
WeakReferenceMessenger.Default.Register<NavigateRightCommand>(this);
|
||||||
WeakReferenceMessenger.Default.Register<NavigatePageDownCommand>(this);
|
WeakReferenceMessenger.Default.Register<NavigatePageDownCommand>(this);
|
||||||
WeakReferenceMessenger.Default.Register<NavigatePageUpCommand>(this);
|
WeakReferenceMessenger.Default.Register<NavigatePageUpCommand>(this);
|
||||||
WeakReferenceMessenger.Default.Register<ActivateSelectedListItemMessage>(this);
|
WeakReferenceMessenger.Default.Register<ActivateSelectedListItemMessage>(this);
|
||||||
@@ -99,6 +103,8 @@ public sealed partial class ListPage : Page,
|
|||||||
|
|
||||||
WeakReferenceMessenger.Default.Unregister<NavigateNextCommand>(this);
|
WeakReferenceMessenger.Default.Unregister<NavigateNextCommand>(this);
|
||||||
WeakReferenceMessenger.Default.Unregister<NavigatePreviousCommand>(this);
|
WeakReferenceMessenger.Default.Unregister<NavigatePreviousCommand>(this);
|
||||||
|
WeakReferenceMessenger.Default.Unregister<NavigateLeftCommand>(this);
|
||||||
|
WeakReferenceMessenger.Default.Unregister<NavigateRightCommand>(this);
|
||||||
WeakReferenceMessenger.Default.Unregister<NavigatePageDownCommand>(this);
|
WeakReferenceMessenger.Default.Unregister<NavigatePageDownCommand>(this);
|
||||||
WeakReferenceMessenger.Default.Unregister<NavigatePageUpCommand>(this);
|
WeakReferenceMessenger.Default.Unregister<NavigatePageUpCommand>(this);
|
||||||
WeakReferenceMessenger.Default.Unregister<ActivateSelectedListItemMessage>(this);
|
WeakReferenceMessenger.Default.Unregister<ActivateSelectedListItemMessage>(this);
|
||||||
@@ -257,6 +263,14 @@ public sealed partial class ListPage : Page,
|
|||||||
// And then have these commands manipulate that state being bound to the UI instead
|
// 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
|
// 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 :)
|
// At least it's decoupled from the SearchBox now :)
|
||||||
|
if (ViewModel?.IsGridView == true)
|
||||||
|
{
|
||||||
|
// For grid views, use spatial navigation (down)
|
||||||
|
HandleGridArrowNavigation(VirtualKey.Down);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// For list views, use simple linear navigation
|
||||||
if (ItemView.SelectedIndex < ItemView.Items.Count - 1)
|
if (ItemView.SelectedIndex < ItemView.Items.Count - 1)
|
||||||
{
|
{
|
||||||
ItemView.SelectedIndex++;
|
ItemView.SelectedIndex++;
|
||||||
@@ -266,9 +280,18 @@ public sealed partial class ListPage : Page,
|
|||||||
ItemView.SelectedIndex = 0;
|
ItemView.SelectedIndex = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void Receive(NavigatePreviousCommand message)
|
public void Receive(NavigatePreviousCommand message)
|
||||||
{
|
{
|
||||||
|
if (ViewModel?.IsGridView == true)
|
||||||
|
{
|
||||||
|
// For grid views, use spatial navigation (up)
|
||||||
|
HandleGridArrowNavigation(VirtualKey.Up);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// For list views, use simple linear navigation
|
||||||
if (ItemView.SelectedIndex > 0)
|
if (ItemView.SelectedIndex > 0)
|
||||||
{
|
{
|
||||||
ItemView.SelectedIndex--;
|
ItemView.SelectedIndex--;
|
||||||
@@ -278,6 +301,35 @@ public sealed partial class ListPage : Page,
|
|||||||
ItemView.SelectedIndex = ItemView.Items.Count - 1;
|
ItemView.SelectedIndex = ItemView.Items.Count - 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Receive(NavigateLeftCommand message)
|
||||||
|
{
|
||||||
|
// For grid views, use spatial navigation. For list views, just move up.
|
||||||
|
if (ViewModel?.IsGridView == true)
|
||||||
|
{
|
||||||
|
HandleGridArrowNavigation(VirtualKey.Left);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// In list view, left arrow doesn't navigate
|
||||||
|
// This maintains consistency with the SearchBar behavior
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Receive(NavigateRightCommand message)
|
||||||
|
{
|
||||||
|
// For grid views, use spatial navigation. For list views, just move down.
|
||||||
|
if (ViewModel?.IsGridView == true)
|
||||||
|
{
|
||||||
|
HandleGridArrowNavigation(VirtualKey.Right);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// In list view, right arrow doesn't navigate
|
||||||
|
// This maintains consistency with the SearchBar behavior
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void Receive(ActivateSelectedListItemMessage message)
|
public void Receive(ActivateSelectedListItemMessage message)
|
||||||
{
|
{
|
||||||
@@ -514,6 +566,130 @@ public sealed partial class ListPage : Page,
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Find a logical neighbor in the requested direction using containers' positions.
|
||||||
|
private void HandleGridArrowNavigation(VirtualKey key)
|
||||||
|
{
|
||||||
|
if (ItemView.Items.Count == 0)
|
||||||
|
{
|
||||||
|
// No items, goodbye.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentIndex = ItemView.SelectedIndex;
|
||||||
|
if (currentIndex < 0)
|
||||||
|
{
|
||||||
|
// -1 is a valid value (no item currently selected)
|
||||||
|
currentIndex = 0;
|
||||||
|
ItemView.SelectedIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Try to compute using container positions; if not available, fall back to simple +/-1.
|
||||||
|
var currentContainer = ItemView.ContainerFromIndex(currentIndex) as FrameworkElement;
|
||||||
|
if (currentContainer is not null && currentContainer.ActualWidth != 0 && currentContainer.ActualHeight != 0)
|
||||||
|
{
|
||||||
|
// Use center of current container as reference
|
||||||
|
var curPoint = currentContainer.TransformToVisual(ItemView).TransformPoint(new Point(0, 0));
|
||||||
|
var curCenterX = curPoint.X + (currentContainer.ActualWidth / 2.0);
|
||||||
|
var curCenterY = curPoint.Y + (currentContainer.ActualHeight / 2.0);
|
||||||
|
|
||||||
|
var bestScore = double.MaxValue;
|
||||||
|
var bestIndex = currentIndex;
|
||||||
|
|
||||||
|
for (var i = 0; i < ItemView.Items.Count; i++)
|
||||||
|
{
|
||||||
|
if (i == currentIndex)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ItemView.ContainerFromIndex(i) is FrameworkElement c && c.ActualWidth > 0 && c.ActualHeight > 0)
|
||||||
|
{
|
||||||
|
var p = c.TransformToVisual(ItemView).TransformPoint(new Point(0, 0));
|
||||||
|
var centerX = p.X + (c.ActualWidth / 2.0);
|
||||||
|
var centerY = p.Y + (c.ActualHeight / 2.0);
|
||||||
|
|
||||||
|
var dx = centerX - curCenterX;
|
||||||
|
var dy = centerY - curCenterY;
|
||||||
|
|
||||||
|
var candidate = false;
|
||||||
|
var score = double.MaxValue;
|
||||||
|
|
||||||
|
switch (key)
|
||||||
|
{
|
||||||
|
case VirtualKey.Left:
|
||||||
|
if (dx < 0)
|
||||||
|
{
|
||||||
|
candidate = true;
|
||||||
|
score = Math.Abs(dy) + (Math.Abs(dx) * 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case VirtualKey.Right:
|
||||||
|
if (dx > 0)
|
||||||
|
{
|
||||||
|
candidate = true;
|
||||||
|
score = Math.Abs(dy) + (Math.Abs(dx) * 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case VirtualKey.Up:
|
||||||
|
if (dy < 0)
|
||||||
|
{
|
||||||
|
candidate = true;
|
||||||
|
score = Math.Abs(dx) + (Math.Abs(dy) * 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case VirtualKey.Down:
|
||||||
|
if (dy > 0)
|
||||||
|
{
|
||||||
|
candidate = true;
|
||||||
|
score = Math.Abs(dx) + (Math.Abs(dy) * 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (candidate && score < bestScore)
|
||||||
|
{
|
||||||
|
bestScore = score;
|
||||||
|
bestIndex = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bestIndex != currentIndex)
|
||||||
|
{
|
||||||
|
ItemView.SelectedIndex = bestIndex;
|
||||||
|
ItemView.ScrollIntoView(ItemView.SelectedItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignore transform errors and fall back
|
||||||
|
}
|
||||||
|
|
||||||
|
// fallback linear behavior
|
||||||
|
var fallback = key switch
|
||||||
|
{
|
||||||
|
VirtualKey.Left => Math.Max(0, currentIndex - 1),
|
||||||
|
VirtualKey.Right => Math.Min(ItemView.Items.Count - 1, currentIndex + 1),
|
||||||
|
VirtualKey.Up => Math.Max(0, currentIndex - 1),
|
||||||
|
VirtualKey.Down => Math.Min(ItemView.Items.Count - 1, currentIndex + 1),
|
||||||
|
_ => currentIndex,
|
||||||
|
};
|
||||||
|
if (fallback != currentIndex)
|
||||||
|
{
|
||||||
|
ItemView.SelectedIndex = fallback;
|
||||||
|
ItemView.ScrollIntoView(ItemView.SelectedItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void Items_OnContextRequested(UIElement sender, ContextRequestedEventArgs e)
|
private void Items_OnContextRequested(UIElement sender, ContextRequestedEventArgs e)
|
||||||
{
|
{
|
||||||
var (item, element) = e.OriginalSource switch
|
var (item, element) = e.OriginalSource switch
|
||||||
@@ -564,9 +740,27 @@ public sealed partial class ListPage : Page,
|
|||||||
|
|
||||||
private void Items_PreviewKeyDown(object sender, KeyRoutedEventArgs e)
|
private void Items_PreviewKeyDown(object sender, KeyRoutedEventArgs e)
|
||||||
{
|
{
|
||||||
|
// Track keyboard as the last input source for activation logic.
|
||||||
if (e.Key is VirtualKey.Enter or VirtualKey.Space)
|
if (e.Key is VirtualKey.Enter or VirtualKey.Space)
|
||||||
{
|
{
|
||||||
_lastInputSource = InputSource.Keyboard;
|
_lastInputSource = InputSource.Keyboard;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle arrow navigation when we're showing a grid.
|
||||||
|
if (ViewModel?.IsGridView == true)
|
||||||
|
{
|
||||||
|
switch (e.Key)
|
||||||
|
{
|
||||||
|
case VirtualKey.Left:
|
||||||
|
case VirtualKey.Right:
|
||||||
|
case VirtualKey.Up:
|
||||||
|
case VirtualKey.Down:
|
||||||
|
_lastInputSource = InputSource.Keyboard;
|
||||||
|
HandleGridArrowNavigation(e.Key);
|
||||||
|
e.Handled = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user