diff --git a/osu.Android.props b/osu.Android.props
index ff04c7f120..0881861bdc 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -52,6 +52,6 @@
-
+
diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs
index ca75a816f1..9437023c70 100644
--- a/osu.Game.Rulesets.Catch/CatchRuleset.cs
+++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs
@@ -21,11 +21,13 @@ using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using System;
+using osu.Framework.Testing;
using osu.Game.Rulesets.Catch.Skinning;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Catch
{
+ [ExcludeFromDynamicCompile]
public class CatchRuleset : Ruleset, ILegacyRuleset
{
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => new DrawableCatchRuleset(this, beatmap, mods);
diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs
index a27485dd06..68dce8b139 100644
--- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs
+++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs
@@ -12,6 +12,7 @@ using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings;
+using osu.Framework.Testing;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mania.Replays;
using osu.Game.Rulesets.Replays.Types;
@@ -34,6 +35,7 @@ using osu.Game.Screens.Ranking.Statistics;
namespace osu.Game.Rulesets.Mania
{
+ [ExcludeFromDynamicCompile]
public class ManiaRuleset : Ruleset, ILegacyRuleset
{
///
diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs
index e488ba65c8..eaa5d8937a 100644
--- a/osu.Game.Rulesets.Osu/OsuRuleset.cs
+++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs
@@ -30,12 +30,14 @@ using osu.Game.Scoring;
using osu.Game.Skinning;
using System;
using System.Linq;
+using osu.Framework.Testing;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Statistics;
using osu.Game.Screens.Ranking.Statistics;
namespace osu.Game.Rulesets.Osu
{
+ [ExcludeFromDynamicCompile]
public class OsuRuleset : Ruleset, ILegacyRuleset
{
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => new DrawableOsuRuleset(this, beatmap, mods);
diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
index 156905fa9c..2011842591 100644
--- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
+++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
@@ -22,6 +22,7 @@ using osu.Game.Rulesets.Taiko.Scoring;
using osu.Game.Scoring;
using System;
using System.Linq;
+using osu.Framework.Testing;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Taiko.Edit;
using osu.Game.Rulesets.Taiko.Objects;
@@ -31,6 +32,7 @@ using osu.Game.Skinning;
namespace osu.Game.Rulesets.Taiko
{
+ [ExcludeFromDynamicCompile]
public class TaikoRuleset : Ruleset, ILegacyRuleset
{
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => new DrawableTaikoRuleset(this, beatmap, mods);
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs
index 5cf3a9d320..b1f6ee3e3a 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs
@@ -11,6 +11,7 @@ using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Multi.Lounge.Components;
using osuTK.Graphics;
+using osuTK.Input;
namespace osu.Game.Tests.Visual.Multiplayer
{
@@ -41,12 +42,42 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("first room removed", () => container.Rooms.All(r => r.Room.RoomID.Value != 0));
AddStep("select first room", () => container.Rooms.First().Action?.Invoke());
- AddAssert("first room selected", () => Room == RoomManager.Rooms.First());
+ AddAssert("first room selected", () => checkRoomSelected(RoomManager.Rooms.First()));
AddStep("join first room", () => container.Rooms.First().Action?.Invoke());
AddAssert("first room joined", () => RoomManager.Rooms.First().Status.Value is JoinedRoomStatus);
}
+ [Test]
+ public void TestKeyboardNavigation()
+ {
+ AddRooms(3);
+
+ AddAssert("no selection", () => checkRoomSelected(null));
+
+ press(Key.Down);
+ AddAssert("first room selected", () => checkRoomSelected(RoomManager.Rooms.First()));
+
+ press(Key.Up);
+ AddAssert("first room selected", () => checkRoomSelected(RoomManager.Rooms.First()));
+
+ press(Key.Down);
+ press(Key.Down);
+ AddAssert("last room selected", () => checkRoomSelected(RoomManager.Rooms.Last()));
+
+ press(Key.Enter);
+ AddAssert("last room joined", () => RoomManager.Rooms.Last().Status.Value is JoinedRoomStatus);
+ }
+
+ private void press(Key down)
+ {
+ AddStep($"press {down}", () =>
+ {
+ InputManager.PressKey(down);
+ InputManager.ReleaseKey(down);
+ });
+ }
+
[Test]
public void TestStringFiltering()
{
@@ -80,6 +111,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("3 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 3);
}
+ private bool checkRoomSelected(Room room) => Room == room;
+
private void joinRequested(Room room) => room.Status.Value = new JoinedRoomStatus();
private class JoinedRoomStatus : RoomStatus
diff --git a/osu.Game/Online/Multiplayer/Room.cs b/osu.Game/Online/Multiplayer/Room.cs
index d074ac9775..66d5d8b3e0 100644
--- a/osu.Game/Online/Multiplayer/Room.cs
+++ b/osu.Game/Online/Multiplayer/Room.cs
@@ -16,54 +16,54 @@ namespace osu.Game.Online.Multiplayer
{
[Cached]
[JsonProperty("id")]
- public Bindable RoomID { get; private set; } = new Bindable();
+ public readonly Bindable RoomID = new Bindable();
[Cached]
[JsonProperty("name")]
- public Bindable Name { get; private set; } = new Bindable();
+ public readonly Bindable Name = new Bindable();
[Cached]
[JsonProperty("host")]
- public Bindable Host { get; private set; } = new Bindable();
+ public readonly Bindable Host = new Bindable();
[Cached]
[JsonProperty("playlist")]
- public BindableList Playlist { get; private set; } = new BindableList();
+ public readonly BindableList Playlist = new BindableList();
[Cached]
[JsonProperty("channel_id")]
- public Bindable ChannelId { get; private set; } = new Bindable();
+ public readonly Bindable ChannelId = new Bindable();
[Cached]
[JsonIgnore]
- public Bindable Duration { get; private set; } = new Bindable(TimeSpan.FromMinutes(30));
+ public readonly Bindable Duration = new Bindable(TimeSpan.FromMinutes(30));
[Cached]
[JsonIgnore]
- public Bindable MaxAttempts { get; private set; } = new Bindable();
+ public readonly Bindable MaxAttempts = new Bindable();
[Cached]
[JsonIgnore]
- public Bindable Status { get; private set; } = new Bindable(new RoomStatusOpen());
+ public readonly Bindable Status = new Bindable(new RoomStatusOpen());
[Cached]
[JsonIgnore]
- public Bindable Availability { get; private set; } = new Bindable();
+ public readonly Bindable Availability = new Bindable();
[Cached]
[JsonIgnore]
- public Bindable Type { get; private set; } = new Bindable(new GameTypeTimeshift());
+ public readonly Bindable Type = new Bindable(new GameTypeTimeshift());
[Cached]
[JsonIgnore]
- public Bindable MaxParticipants { get; private set; } = new Bindable();
+ public readonly Bindable MaxParticipants = new Bindable();
[Cached]
[JsonProperty("recent_participants")]
- public BindableList RecentParticipants { get; private set; } = new BindableList();
+ public readonly BindableList RecentParticipants = new BindableList();
[Cached]
- public Bindable ParticipantCount { get; private set; } = new Bindable();
+ public readonly Bindable ParticipantCount = new Bindable();
// todo: TEMPORARY
[JsonProperty("participant_count")]
@@ -83,7 +83,7 @@ namespace osu.Game.Online.Multiplayer
// Only supports retrieval for now
[Cached]
[JsonProperty("ends_at")]
- public Bindable EndDate { get; private set; } = new Bindable();
+ public readonly Bindable EndDate = new Bindable();
// Todo: Find a better way to do this (https://github.com/ppy/osu-framework/issues/1930)
[JsonProperty("max_attempts", DefaultValueHandling = DefaultValueHandling.Ignore)]
@@ -97,7 +97,7 @@ namespace osu.Game.Online.Multiplayer
/// The position of this in the list. This is not read from or written to the API.
///
[JsonIgnore]
- public Bindable Position { get; private set; } = new Bindable(-1);
+ public readonly Bindable Position = new Bindable(-1);
public void CopyFrom(Room other)
{
@@ -130,7 +130,7 @@ namespace osu.Game.Online.Multiplayer
RecentParticipants.AddRange(other.RecentParticipants);
}
- Position = other.Position;
+ Position.Value = other.Position.Value;
}
public bool ShouldSerializeRoomID() => false;
diff --git a/osu.Game/Overlays/SearchableList/SearchableListFilterControl.cs b/osu.Game/Overlays/SearchableList/SearchableListFilterControl.cs
index d31470e685..de5e558943 100644
--- a/osu.Game/Overlays/SearchableList/SearchableListFilterControl.cs
+++ b/osu.Game/Overlays/SearchableList/SearchableListFilterControl.cs
@@ -136,8 +136,6 @@ namespace osu.Game.Overlays.SearchableList
private class FilterSearchTextBox : SearchTextBox
{
- protected override bool AllowCommit => true;
-
[BackgroundDependencyLoader]
private void load()
{
diff --git a/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs b/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs
index f14aa5fd8c..bf153b77df 100644
--- a/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs
+++ b/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs
@@ -9,13 +9,17 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Input.Bindings;
+using osu.Framework.Threading;
+using osu.Game.Extensions;
using osu.Game.Graphics.UserInterface;
+using osu.Game.Input.Bindings;
using osu.Game.Online.Multiplayer;
using osuTK;
namespace osu.Game.Screens.Multi.Lounge.Components
{
- public class RoomsContainer : CompositeDrawable
+ public class RoomsContainer : CompositeDrawable, IKeyBindingHandler
{
public Action JoinRequested;
@@ -88,8 +92,22 @@ namespace osu.Game.Screens.Multi.Lounge.Components
private void addRooms(IEnumerable rooms)
{
- foreach (var r in rooms)
- roomFlow.Add(new DrawableRoom(r) { Action = () => selectRoom(r) });
+ foreach (var room in rooms)
+ {
+ roomFlow.Add(new DrawableRoom(room)
+ {
+ Action = () =>
+ {
+ if (room == selectedRoom.Value)
+ {
+ joinSelected();
+ return;
+ }
+
+ selectRoom(room);
+ }
+ });
+ }
Filter(filter?.Value);
}
@@ -115,16 +133,100 @@ namespace osu.Game.Screens.Multi.Lounge.Components
private void selectRoom(Room room)
{
- var drawable = roomFlow.FirstOrDefault(r => r.Room == room);
-
- if (drawable != null && drawable.State == SelectionState.Selected)
- JoinRequested?.Invoke(room);
- else
- roomFlow.Children.ForEach(r => r.State = r.Room == room ? SelectionState.Selected : SelectionState.NotSelected);
-
+ roomFlow.Children.ForEach(r => r.State = r.Room == room ? SelectionState.Selected : SelectionState.NotSelected);
selectedRoom.Value = room;
}
+ private void joinSelected()
+ {
+ if (selectedRoom.Value == null) return;
+
+ JoinRequested?.Invoke(selectedRoom.Value);
+ }
+
+ #region Key selection logic (shared with BeatmapCarousel)
+
+ public bool OnPressed(GlobalAction action)
+ {
+ switch (action)
+ {
+ case GlobalAction.Select:
+ joinSelected();
+ return true;
+
+ case GlobalAction.SelectNext:
+ beginRepeatSelection(() => selectNext(1), action);
+ return true;
+
+ case GlobalAction.SelectPrevious:
+ beginRepeatSelection(() => selectNext(-1), action);
+ return true;
+ }
+
+ return false;
+ }
+
+ public void OnReleased(GlobalAction action)
+ {
+ switch (action)
+ {
+ case GlobalAction.SelectNext:
+ case GlobalAction.SelectPrevious:
+ endRepeatSelection(action);
+ break;
+ }
+ }
+
+ private ScheduledDelegate repeatDelegate;
+ private object lastRepeatSource;
+
+ ///
+ /// Begin repeating the specified selection action.
+ ///
+ /// The action to perform.
+ /// The source of the action. Used in conjunction with to only cancel the correct action (most recently pressed key).
+ private void beginRepeatSelection(Action action, object source)
+ {
+ endRepeatSelection();
+
+ lastRepeatSource = source;
+ repeatDelegate = this.BeginKeyRepeat(Scheduler, action);
+ }
+
+ private void endRepeatSelection(object source = null)
+ {
+ // only the most recent source should be able to cancel the current action.
+ if (source != null && !EqualityComparer