diff --git a/osu.Game/Graphics/Carousel/Carousel.ScrollContainer.cs b/osu.Game/Graphics/Carousel/Carousel.ScrollContainer.cs index accd74aa4b..835a83998a 100644 --- a/osu.Game/Graphics/Carousel/Carousel.ScrollContainer.cs +++ b/osu.Game/Graphics/Carousel/Carousel.ScrollContainer.cs @@ -33,6 +33,9 @@ namespace osu.Game.Graphics.Carousel /// protected partial class ScrollContainer : UserTrackingScrollContainer, IKeyBindingHandler { + public Action? OnPageUp { get; init; } + public Action? OnPageDown { get; init; } + public readonly Container Panels; public void SetLayoutHeight(float height) => Panels.Height = height; @@ -127,6 +130,22 @@ namespace osu.Game.Graphics.Carousel protected override bool IsDragging => base.IsDragging || AbsoluteScrolling; + protected override bool OnKeyDown(KeyDownEvent e) + { + switch (e.Key) + { + case Key.PageUp: + OnPageUp?.Invoke(); + return true; + + case Key.PageDown: + OnPageDown?.Invoke(); + return true; + } + + return base.OnKeyDown(e); + } + public bool OnPressed(KeyBindingPressEvent e) { switch (e.Action) diff --git a/osu.Game/Graphics/Carousel/Carousel.cs b/osu.Game/Graphics/Carousel/Carousel.cs index 3b7e31b2ea..13d3c22c19 100644 --- a/osu.Game/Graphics/Carousel/Carousel.cs +++ b/osu.Game/Graphics/Carousel/Carousel.cs @@ -317,6 +317,8 @@ namespace osu.Game.Graphics.Carousel { Masking = false, RelativeSizeAxes = Axes.Both, + OnPageUp = () => Scheduler.AddOnce(traverseFromKey, new TraversalOperation(TraversalType.Page, -1)), + OnPageDown = () => Scheduler.AddOnce(traverseFromKey, new TraversalOperation(TraversalType.Page, 1)), }; Items.BindCollectionChanged((_, args) => @@ -538,26 +540,30 @@ namespace osu.Game.Graphics.Carousel } return false; + } - void traverseFromKey(TraversalOperation traversal) + private void traverseFromKey(TraversalOperation traversal) + { + switch (traversal.Type) { - switch (traversal.Type) - { - case TraversalType.Keyboard: - traverseKeyboardSelection(traversal.Direction); - break; + case TraversalType.Keyboard: + traverseKeyboardSelection(traversal.Direction); + break; - case TraversalType.Set: - traverseSetSelection(traversal.Direction); - break; + case TraversalType.Page: + traverseKeyboardPage(traversal.Direction); + break; - case TraversalType.Group: - traverseGroupSelection(traversal.Direction); - break; + case TraversalType.Set: + traverseSetSelection(traversal.Direction); + break; - default: - throw new ArgumentOutOfRangeException(); - } + case TraversalType.Group: + traverseGroupSelection(traversal.Direction); + break; + + default: + throw new ArgumentOutOfRangeException(); } } @@ -565,6 +571,7 @@ namespace osu.Game.Graphics.Carousel { Keyboard, Set, + Page, Group } @@ -622,6 +629,59 @@ namespace osu.Game.Graphics.Carousel } while (newIndex != originalIndex); } + /// + /// Performs a page-wise keyboard traversal in the carousel, moving the selection by approximately one "page" of items. + /// + /// Positive for downwards, negative for upwards. + private void traverseKeyboardPage(int direction) + { + if (carouselItems == null || carouselItems.Count == 0) + return; + + int startIndex = currentKeyboardSelection.Index ?? (direction > 0 ? carouselItems.Count - 1 : 0); + + // Compute the number of visible panels to treat as one page. + // Reduced by 50% to account for the search bar covering the top items. + int visiblePanelsCount = Math.Max(1, Scroll.Panels.Count / 2); + int visibleCount = 0; + int i = startIndex; + + while (i >= 0 && i < carouselItems.Count) + { + i += direction; + + if (i < 0 || i >= carouselItems.Count) + break; + + var item = carouselItems[i]; + + if (!item.IsVisible) + continue; + + visibleCount++; + + if (visibleCount >= visiblePanelsCount) + { + setKeyboardSelection(item.Model); + ScrollToSelection(); + playTraversalSound(); + return; + } + } + + // If we are at the beginning or end and there are not enough items left to scroll through a complete page, then we go to the last or first item. + var fallback = direction > 0 + ? carouselItems.LastOrDefault(x => x.IsVisible) + : carouselItems.FirstOrDefault(x => x.IsVisible); + + if (fallback != null && !CheckModelEquality(fallback.Model, currentKeyboardSelection.Model)) + { + setKeyboardSelection(fallback.Model); + ScrollToSelection(); + playTraversalSound(); + } + } + /// /// Select the next valid group selection relative to a current selection. /// This is generally for keyboard based traversal.