From 14bde85263a5c14b101d5e9d48c8972fc17cede5 Mon Sep 17 00:00:00 2001 From: Rudi Herouard Date: Tue, 10 Mar 2026 06:20:59 +0100 Subject: [PATCH] Fix song select navigation with home/end keys (#36879) It's a continuation of https://github.com/ppy/osu/pull/36293, but for the home and end keys. Now when using home or end keys, it selects respectively the first or last item in the carousel, instead of just scrolling. ## Before: https://github.com/user-attachments/assets/6ab08d2f-1da4-4740-9d9e-574d7a8a10c9 ## After: https://github.com/user-attachments/assets/30bab836-0006-4830-b4e9-2d85017a15e6 --- .../Carousel/Carousel.ScrollContainer.cs | 24 +++++++++++++++- osu.Game/Graphics/Carousel/Carousel.cs | 28 +++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/osu.Game/Graphics/Carousel/Carousel.ScrollContainer.cs b/osu.Game/Graphics/Carousel/Carousel.ScrollContainer.cs index 5f1488513a..625c246c4e 100644 --- a/osu.Game/Graphics/Carousel/Carousel.ScrollContainer.cs +++ b/osu.Game/Graphics/Carousel/Carousel.ScrollContainer.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; +using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Utils; @@ -31,10 +32,12 @@ namespace osu.Game.Graphics.Carousel /// Implementation of scroll container which handles very large vertical lists by internally using double precision /// for pre-display Y values. /// - protected partial class ScrollContainer : UserTrackingScrollContainer, IKeyBindingHandler + protected partial class ScrollContainer : UserTrackingScrollContainer, IKeyBindingHandler, IKeyBindingHandler { public Action? OnPageUp { get; init; } public Action? OnPageDown { get; init; } + public Action? OnListStart { get; init; } + public Action? OnListEnd { get; init; } public readonly Container Panels; @@ -146,6 +149,25 @@ namespace osu.Game.Graphics.Carousel return base.OnKeyDown(e); } + public new bool OnPressed(KeyBindingPressEvent e) + { + if (IsHandlingKeyboardScrolling) + { + switch (e.Action) + { + case PlatformAction.MoveBackwardLine: + OnListStart?.Invoke(); + return true; + + case PlatformAction.MoveForwardLine: + OnListEnd?.Invoke(); + return true; + } + } + + return base.OnPressed(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 13d3c22c19..5164068d53 100644 --- a/osu.Game/Graphics/Carousel/Carousel.cs +++ b/osu.Game/Graphics/Carousel/Carousel.cs @@ -319,6 +319,8 @@ namespace osu.Game.Graphics.Carousel RelativeSizeAxes = Axes.Both, OnPageUp = () => Scheduler.AddOnce(traverseFromKey, new TraversalOperation(TraversalType.Page, -1)), OnPageDown = () => Scheduler.AddOnce(traverseFromKey, new TraversalOperation(TraversalType.Page, 1)), + OnListStart = () => Scheduler.AddOnce(traverseFromKey, new TraversalOperation(TraversalType.Edge, -1)), + OnListEnd = () => Scheduler.AddOnce(traverseFromKey, new TraversalOperation(TraversalType.Edge, 1)), }; Items.BindCollectionChanged((_, args) => @@ -554,6 +556,10 @@ namespace osu.Game.Graphics.Carousel traverseKeyboardPage(traversal.Direction); break; + case TraversalType.Edge: + traverseKeyboardEdge(traversal.Direction); + break; + case TraversalType.Set: traverseSetSelection(traversal.Direction); break; @@ -572,6 +578,7 @@ namespace osu.Game.Graphics.Carousel Keyboard, Set, Page, + Edge, Group } @@ -682,6 +689,27 @@ namespace osu.Game.Graphics.Carousel } } + /// + /// Select the first or last item in the carousel. + /// + /// Positive for last item, negative for first item. + private void traverseKeyboardEdge(int direction) + { + if (carouselItems == null || carouselItems.Count == 0) + return; + + var item = direction > 0 + ? carouselItems.LastOrDefault(x => x.IsVisible) + : carouselItems.FirstOrDefault(x => x.IsVisible); + + if (item != null && !CheckModelEquality(item.Model, currentKeyboardSelection.Model)) + { + setKeyboardSelection(item.Model); + ScrollToSelection(); + playTraversalSound(); + } + } + /// /// Select the next valid group selection relative to a current selection. /// This is generally for keyboard based traversal.