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) {