mirror of
https://github.com/ppy/osu.git
synced 2025-02-24 13:22:55 +08:00
I had a bit of a struggle getting header traversal logic to work well. The constraints I had in place were a bit weird: - Group panels should toggle or potentially fall into the prev/next group - Set panels should just traverse around them The current method of using `CheckValidForGroupSelection` return type for traversal did not mesh with the above two cases. Just trust me on this one since it's quite hard to explain in words. After some re-thinking, I've gone with a simpler approach with one important change to UX: Now when group traversing with a beatmap set header currently keyboard focused, the first operation will be to reset keyboard selection to the selected beatmap, rather than traverse. I find this non-offensive – at most it means a user will need to press their group traversal key one extra time. I've also changed group headers to always toggle expansion when doing group traversal with them selected. To make all this work, the meaning of `Activation` has changed somewhat. It is now the primary path for carousel implementations to change selection of an item. It is what the `Drawable` panels call when they are clicked. Selection changes are not performed implicitly by `Carousel` – an implementation should decide when it actually wants to change the selection, usually in `HandleItemActivated`. Having less things mutating `CurrentSelection` is better in my eyes, as we see this variable as only being mutated internally when utmost required (ie the user has requested the change). With this change, `CurrentSelection` can no longer become of a non-`T` type (in the beatmap carousel implementation at least). This might pave a path forward for making `CurrentSelection` typed, but that comes with a few other concerns so I'll look at that as a follow-up.
222 lines
6.5 KiB
C#
222 lines
6.5 KiB
C#
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
// See the LICENCE file in the repository root for full licence text.
|
|
|
|
using NUnit.Framework;
|
|
using osu.Framework.Testing;
|
|
using osu.Game.Beatmaps;
|
|
using osu.Game.Screens.Select;
|
|
using osu.Game.Screens.Select.Filter;
|
|
using osuTK.Input;
|
|
|
|
namespace osu.Game.Tests.Visual.SongSelect
|
|
{
|
|
[TestFixture]
|
|
public partial class TestSceneBeatmapCarouselV2NoGrouping : BeatmapCarouselV2TestScene
|
|
{
|
|
[SetUpSteps]
|
|
public void SetUpSteps()
|
|
{
|
|
RemoveAllBeatmaps();
|
|
CreateCarousel();
|
|
SortBy(new FilterCriteria { Sort = SortMode.Title });
|
|
}
|
|
|
|
/// <summary>
|
|
/// Keyboard selection via up and down arrows doesn't actually change the selection until
|
|
/// the select key is pressed.
|
|
/// </summary>
|
|
[Test]
|
|
public void TestKeyboardSelectionKeyRepeat()
|
|
{
|
|
AddBeatmaps(10);
|
|
WaitForDrawablePanels();
|
|
CheckNoSelection();
|
|
|
|
Select();
|
|
CheckNoSelection();
|
|
|
|
AddStep("press down arrow", () => InputManager.PressKey(Key.Down));
|
|
checkSelectionIterating(false);
|
|
|
|
AddStep("press up arrow", () => InputManager.PressKey(Key.Up));
|
|
checkSelectionIterating(false);
|
|
|
|
AddStep("release down arrow", () => InputManager.ReleaseKey(Key.Down));
|
|
checkSelectionIterating(false);
|
|
|
|
AddStep("release up arrow", () => InputManager.ReleaseKey(Key.Up));
|
|
checkSelectionIterating(false);
|
|
|
|
Select();
|
|
CheckHasSelection();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Keyboard selection via left and right arrows moves between groups, updating the selection
|
|
/// immediately.
|
|
/// </summary>
|
|
[Test]
|
|
public void TestGroupSelectionKeyRepeat()
|
|
{
|
|
AddBeatmaps(10);
|
|
WaitForDrawablePanels();
|
|
CheckNoSelection();
|
|
|
|
AddStep("press right arrow", () => InputManager.PressKey(Key.Right));
|
|
checkSelectionIterating(true);
|
|
|
|
AddStep("press left arrow", () => InputManager.PressKey(Key.Left));
|
|
checkSelectionIterating(true);
|
|
|
|
AddStep("release right arrow", () => InputManager.ReleaseKey(Key.Right));
|
|
checkSelectionIterating(true);
|
|
|
|
AddStep("release left arrow", () => InputManager.ReleaseKey(Key.Left));
|
|
checkSelectionIterating(false);
|
|
}
|
|
|
|
[Test]
|
|
public void TestCarouselRemembersSelection()
|
|
{
|
|
AddBeatmaps(10);
|
|
WaitForDrawablePanels();
|
|
|
|
SelectNextGroup();
|
|
|
|
object? selection = null;
|
|
|
|
AddStep("store drawable selection", () => selection = GetSelectedPanel()?.Item?.Model);
|
|
|
|
CheckHasSelection();
|
|
AddAssert("drawable selection non-null", () => selection, () => Is.Not.Null);
|
|
AddAssert("drawable selection matches carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection));
|
|
|
|
RemoveAllBeatmaps();
|
|
AddUntilStep("no drawable selection", GetSelectedPanel, () => Is.Null);
|
|
|
|
AddBeatmaps(10);
|
|
WaitForDrawablePanels();
|
|
|
|
CheckHasSelection();
|
|
AddAssert("no drawable selection", GetSelectedPanel, () => Is.Null);
|
|
|
|
AddStep("add previous selection", () => BeatmapSets.Add(((BeatmapInfo)selection!).BeatmapSet!));
|
|
|
|
AddAssert("selection matches original carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection));
|
|
AddUntilStep("drawable selection restored", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection));
|
|
AddAssert("carousel item is visible", () => GetSelectedPanel()?.Item?.IsVisible, () => Is.True);
|
|
}
|
|
|
|
[Test]
|
|
public void TestTraversalBeyondStart()
|
|
{
|
|
const int total_set_count = 200;
|
|
|
|
AddBeatmaps(total_set_count);
|
|
WaitForDrawablePanels();
|
|
|
|
SelectNextGroup();
|
|
WaitForSelection(0, 0);
|
|
SelectPrevGroup();
|
|
WaitForSelection(total_set_count - 1, 0);
|
|
}
|
|
|
|
[Test]
|
|
public void TestTraversalBeyondEnd()
|
|
{
|
|
const int total_set_count = 200;
|
|
|
|
AddBeatmaps(total_set_count);
|
|
WaitForDrawablePanels();
|
|
|
|
SelectPrevGroup();
|
|
WaitForSelection(total_set_count - 1, 0);
|
|
SelectNextGroup();
|
|
WaitForSelection(0, 0);
|
|
}
|
|
|
|
[Test]
|
|
public void TestGroupSelectionOnHeader()
|
|
{
|
|
AddBeatmaps(10, 3);
|
|
WaitForDrawablePanels();
|
|
|
|
SelectNextGroup();
|
|
SelectNextGroup();
|
|
WaitForSelection(1, 0);
|
|
|
|
SelectPrevPanel();
|
|
SelectPrevGroup();
|
|
WaitForSelection(1, 0);
|
|
|
|
SelectPrevPanel();
|
|
SelectNextGroup();
|
|
WaitForSelection(1, 0);
|
|
}
|
|
|
|
[Test]
|
|
public void TestKeyboardSelection()
|
|
{
|
|
AddBeatmaps(10, 3);
|
|
WaitForDrawablePanels();
|
|
|
|
SelectNextPanel();
|
|
SelectNextPanel();
|
|
SelectNextPanel();
|
|
SelectNextPanel();
|
|
CheckNoSelection();
|
|
|
|
Select();
|
|
WaitForSelection(3, 0);
|
|
|
|
SelectNextPanel();
|
|
WaitForSelection(3, 0);
|
|
|
|
Select();
|
|
WaitForSelection(3, 1);
|
|
|
|
SelectNextPanel();
|
|
WaitForSelection(3, 1);
|
|
|
|
Select();
|
|
WaitForSelection(3, 2);
|
|
|
|
SelectNextPanel();
|
|
WaitForSelection(3, 2);
|
|
|
|
Select();
|
|
WaitForSelection(4, 0);
|
|
}
|
|
|
|
[Test]
|
|
public void TestEmptyTraversal()
|
|
{
|
|
SelectNextPanel();
|
|
CheckNoSelection();
|
|
|
|
SelectNextGroup();
|
|
CheckNoSelection();
|
|
|
|
SelectPrevPanel();
|
|
CheckNoSelection();
|
|
|
|
SelectPrevGroup();
|
|
CheckNoSelection();
|
|
}
|
|
|
|
private void checkSelectionIterating(bool isIterating)
|
|
{
|
|
object? selection = null;
|
|
|
|
for (int i = 0; i < 3; i++)
|
|
{
|
|
AddStep("store selection", () => selection = Carousel.CurrentSelection);
|
|
if (isIterating)
|
|
AddUntilStep("selection changed", () => Carousel.CurrentSelection != selection);
|
|
else
|
|
AddUntilStep("selection not changed", () => Carousel.CurrentSelection == selection);
|
|
}
|
|
}
|
|
}
|
|
}
|