diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs
index 76353323d6..e2b4b2870f 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs
@@ -56,6 +56,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("click", () => InputManager.Click(MouseButton.Left));
AddAssert("no item selected", () => playlist.SelectedItem.Value == null);
+
+ AddStep("press down", () => InputManager.Key(Key.Down));
+ AddAssert("no item selected", () => playlist.SelectedItem.Value == null);
}
[Test]
@@ -73,6 +76,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("click", () => InputManager.Click(MouseButton.Left));
AddAssert("no item selected", () => playlist.SelectedItem.Value == null);
+
+ AddStep("press down", () => InputManager.Key(Key.Down));
+ AddAssert("no item selected", () => playlist.SelectedItem.Value == null);
}
[Test]
@@ -91,6 +97,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("click", () => InputManager.Click(MouseButton.Left));
AddAssert("no item selected", () => playlist.SelectedItem.Value == null);
+
+ AddStep("press down", () => InputManager.Key(Key.Down));
+ AddAssert("no item selected", () => playlist.SelectedItem.Value == null);
}
[Test]
@@ -147,6 +156,40 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("item 1 is selected", () => playlist.SelectedItem.Value == playlist.Items[1]);
}
+ [Test]
+ public void TestKeyboardSelection()
+ {
+ createPlaylist(p => p.AllowSelection = true);
+
+ AddStep("press down", () => InputManager.Key(Key.Down));
+ AddAssert("item 0 is selected", () => playlist.SelectedItem.Value == playlist.Items[0]);
+
+ AddStep("press down", () => InputManager.Key(Key.Down));
+ AddAssert("item 1 is selected", () => playlist.SelectedItem.Value == playlist.Items[1]);
+
+ AddStep("press up", () => InputManager.Key(Key.Up));
+ AddAssert("item 0 is selected", () => playlist.SelectedItem.Value == playlist.Items[0]);
+
+ AddUntilStep("navigate to last item via keyboard", () =>
+ {
+ InputManager.Key(Key.Down);
+ return playlist.SelectedItem.Value == playlist.Items.Last();
+ });
+ AddAssert("last item is selected", () => playlist.SelectedItem.Value == playlist.Items.Last());
+ AddUntilStep("last item is scrolled into view", () =>
+ {
+ var drawableItem = playlist.ItemMap[playlist.Items.Last()];
+ return playlist.ScreenSpaceDrawQuad.Contains(drawableItem.ScreenSpaceDrawQuad.TopLeft)
+ && playlist.ScreenSpaceDrawQuad.Contains(drawableItem.ScreenSpaceDrawQuad.BottomRight);
+ });
+
+ AddStep("press down", () => InputManager.Key(Key.Down));
+ AddAssert("last item is selected", () => playlist.SelectedItem.Value == playlist.Items.Last());
+
+ AddStep("press up", () => InputManager.Key(Key.Up));
+ AddAssert("second last item is selected", () => playlist.SelectedItem.Value == playlist.Items.Reverse().ElementAt(1));
+ }
+
[Test]
public void TestDownloadButtonHiddenWhenBeatmapExists()
{
diff --git a/osu.Game/Online/Chat/StandAloneChatDisplay.cs b/osu.Game/Online/Chat/StandAloneChatDisplay.cs
index 6a7da52416..df210fcaf8 100644
--- a/osu.Game/Online/Chat/StandAloneChatDisplay.cs
+++ b/osu.Game/Online/Chat/StandAloneChatDisplay.cs
@@ -14,6 +14,7 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Chat;
using osu.Game.Resources.Localisation.Web;
using osuTK.Graphics;
+using osuTK.Input;
namespace osu.Game.Online.Chat
{
@@ -119,6 +120,20 @@ namespace osu.Game.Online.Chat
public class ChatTextBox : FocusedTextBox
{
+ protected override bool OnKeyDown(KeyDownEvent e)
+ {
+ // Chat text boxes are generally used in places where they retain focus, but shouldn't block interaction with other
+ // elements on the same screen.
+ switch (e.Key)
+ {
+ case Key.Up:
+ case Key.Down:
+ return false;
+ }
+
+ return base.OnKeyDown(e);
+ }
+
protected override void LoadComplete()
{
base.LoadComplete();
diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs
index 57bb4253cb..2a72fc6eb1 100644
--- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs
+++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs
@@ -6,7 +6,10 @@ using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Input.Bindings;
+using osu.Framework.Input.Events;
using osu.Game.Graphics.Containers;
+using osu.Game.Input.Bindings;
using osu.Game.Online.Rooms;
using osuTK;
@@ -15,7 +18,7 @@ namespace osu.Game.Screens.OnlinePlay
///
/// A scrollable list which displays the s in a .
///
- public class DrawableRoomPlaylist : OsuRearrangeableListContainer
+ public class DrawableRoomPlaylist : OsuRearrangeableListContainer, IKeyBindingHandler
{
///
/// The currently-selected item. Selection is visually represented with a border.
@@ -169,5 +172,78 @@ namespace osu.Game.Screens.OnlinePlay
});
protected virtual DrawableRoomPlaylistItem CreateDrawablePlaylistItem(PlaylistItem item) => new DrawableRoomPlaylistItem(item);
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ // schedules added as the properties may change value while the drawable items haven't been created yet.
+ SelectedItem.BindValueChanged(_ => Scheduler.AddOnce(scrollToSelection));
+ Items.BindCollectionChanged((_, __) => Scheduler.AddOnce(scrollToSelection), true);
+ }
+
+ private void scrollToSelection()
+ {
+ // SelectedItem and ItemMap/drawable items are managed separately,
+ // so if the item can't be unmapped to a drawable, don't try to scroll to it.
+ // best effort is made to not drop any updates, by subscribing to both sources.
+ if (SelectedItem.Value == null || !ItemMap.TryGetValue(SelectedItem.Value, out var drawableItem))
+ return;
+
+ // ScrollIntoView does not handle non-loaded items appropriately, delay scroll until the item finishes loading.
+ // see: https://github.com/ppy/osu-framework/issues/5158
+ if (!drawableItem.IsLoaded)
+ drawableItem.OnLoadComplete += _ => ScrollContainer.ScrollIntoView(drawableItem);
+ else
+ ScrollContainer.ScrollIntoView(drawableItem);
+ }
+
+ #region Key selection logic (shared with BeatmapCarousel and RoomsContainer)
+
+ public bool OnPressed(KeyBindingPressEvent e)
+ {
+ switch (e.Action)
+ {
+ case GlobalAction.SelectNext:
+ selectNext(1);
+ return true;
+
+ case GlobalAction.SelectPrevious:
+ selectNext(-1);
+ return true;
+ }
+
+ return false;
+ }
+
+ public void OnReleased(KeyBindingReleaseEvent e)
+ {
+ }
+
+ private void selectNext(int direction)
+ {
+ if (!AllowSelection)
+ return;
+
+ var visibleItems = ListContainer.AsEnumerable().Where(r => r.IsPresent);
+
+ PlaylistItem item;
+
+ if (SelectedItem.Value == null)
+ item = visibleItems.FirstOrDefault()?.Model;
+ else
+ {
+ if (direction < 0)
+ visibleItems = visibleItems.Reverse();
+
+ item = visibleItems.SkipWhile(r => r.Model != SelectedItem.Value).Skip(1).FirstOrDefault()?.Model;
+ }
+
+ // we already have a valid selection only change selection if we still have a room to switch to.
+ if (item != null)
+ SelectedItem.Value = item;
+ }
+
+ #endregion
}
}
diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs
index 175cd2c44e..74e4225f11 100644
--- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs
+++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs
@@ -139,7 +139,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
return base.OnClick(e);
}
- #region Key selection logic (shared with BeatmapCarousel)
+ #region Key selection logic (shared with BeatmapCarousel and DrawableRoomPlaylist)
public bool OnPressed(KeyBindingPressEvent e)
{