diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectScreen.cs index 8eaa45696f..a02055d960 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectScreen.cs @@ -1,19 +1,32 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using System.Linq; using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; +using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.Mods; using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Mods; using osu.Game.Screens.OnlinePlay; +using osuTK.Input; namespace osu.Game.Tests.Visual.Multiplayer { public class TestSceneFreeModSelectScreen : MultiplayerTestScene { private FreeModSelectScreen freeModSelectScreen; + private readonly Bindable>> availableMods = new Bindable>>(); + + [BackgroundDependencyLoader] + private void load(OsuGameBase osuGameBase) + { + availableMods.BindTo(osuGameBase.AvailableMods); + } [Test] public void TestFreeModSelect() @@ -42,6 +55,26 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("customisation area not expanded", () => this.ChildrenOfType().Single().Height == 0); } + [Test] + public void TestSelectDeselectAll() + { + createFreeModSelect(); + + AddStep("click select all button", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType().First()); + InputManager.Click(MouseButton.Left); + }); + AddUntilStep("all mods selected", assertAllAvailableModsSelected); + + AddStep("click deselect all button", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType().Last()); + InputManager.Click(MouseButton.Left); + }); + AddUntilStep("all mods deselected", () => !freeModSelectScreen.SelectedMods.Value.Any()); + } + private void createFreeModSelect() { AddStep("create free mod select screen", () => Child = freeModSelectScreen = new FreeModSelectScreen @@ -52,5 +85,21 @@ namespace osu.Game.Tests.Visual.Multiplayer () => freeModSelectScreen.ChildrenOfType().Any() && freeModSelectScreen.ChildrenOfType().All(column => column.IsLoaded && column.ItemsLoaded)); } + + private bool assertAllAvailableModsSelected() + { + var allAvailableMods = availableMods.Value + .SelectMany(pair => pair.Value) + .Where(mod => mod.UserPlayable && mod.HasImplementation) + .ToList(); + + foreach (var availableMod in allAvailableMods) + { + if (freeModSelectScreen.SelectedMods.Value.All(selectedMod => selectedMod.GetType() != availableMod.GetType())) + return false; + } + + return true; + } } } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectScreen.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectScreen.cs index c92a738b56..9e0c8206b7 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectScreen.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectScreen.cs @@ -415,6 +415,23 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("unimplemented mod panel is filtered", () => getPanelForMod(typeof(TestUnimplementedMod)).Filtered.Value); } + [Test] + public void TestDeselectAllViaButton() + { + createScreen(); + changeRuleset(0); + + AddStep("select DT + HD", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime(), new OsuModHidden() }); + AddAssert("DT + HD selected", () => modSelectScreen.ChildrenOfType().Count(panel => panel.Active.Value) == 2); + + AddStep("click deselect all button", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType().Last()); + InputManager.Click(MouseButton.Left); + }); + AddUntilStep("all mods deselected", () => !SelectedMods.Value.Any()); + } + private void waitForColumnLoad() => AddUntilStep("all column content loaded", () => modSelectScreen.ChildrenOfType().Any() && modSelectScreen.ChildrenOfType().All(column => column.IsLoaded && column.ItemsLoaded)); diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs index f6fa591666..07871663a6 100644 --- a/osu.Game/Overlays/Mods/ModColumn.cs +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -275,30 +275,25 @@ namespace osu.Game.Overlays.Mods return; localAvailableMods = newMods; - Scheduler.AddOnce(loadPanels); + + if (!IsLoaded) + // if we're coming from BDL, perform the first load synchronously to make sure everything is in place as early as possible. + onPanelsLoaded(createPanels()); + else + asyncLoadPanels(); } private CancellationTokenSource? cancellationTokenSource; - private void loadPanels() + private void asyncLoadPanels() { cancellationTokenSource?.Cancel(); - var panels = localAvailableMods.Select(mod => CreateModPanel(mod).With(panel => panel.Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0))); + var panels = createPanels(); Task? loadTask; - latestLoadTask = loadTask = LoadComponentsAsync(panels, loaded => - { - panelFlow.ChildrenEnumerable = loaded; - - updateState(); - - foreach (var panel in panelFlow) - { - panel.Active.BindValueChanged(_ => panelStateChanged(panel)); - } - }, (cancellationTokenSource = new CancellationTokenSource()).Token); + latestLoadTask = loadTask = LoadComponentsAsync(panels, onPanelsLoaded, (cancellationTokenSource = new CancellationTokenSource()).Token); loadTask.ContinueWith(_ => { if (loadTask == latestLoadTask) @@ -306,6 +301,24 @@ namespace osu.Game.Overlays.Mods }); } + private IEnumerable createPanels() + { + var panels = localAvailableMods.Select(mod => CreateModPanel(mod).With(panel => panel.Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0))); + return panels; + } + + private void onPanelsLoaded(IEnumerable loaded) + { + panelFlow.ChildrenEnumerable = loaded; + + updateState(); + + foreach (var panel in panelFlow) + { + panel.Active.BindValueChanged(_ => panelStateChanged(panel)); + } + } + private void updateState() { foreach (var panel in panelFlow) @@ -386,7 +399,7 @@ namespace osu.Game.Overlays.Mods private readonly Queue pendingSelectionOperations = new Queue(); - protected bool SelectionAnimationRunning => pendingSelectionOperations.Count > 0; + internal bool SelectionAnimationRunning => pendingSelectionOperations.Count > 0; protected override void Update() { diff --git a/osu.Game/Overlays/Mods/ModSelectScreen.cs b/osu.Game/Overlays/Mods/ModSelectScreen.cs index 838c3597ae..e5b4927a6a 100644 --- a/osu.Game/Overlays/Mods/ModSelectScreen.cs +++ b/osu.Game/Overlays/Mods/ModSelectScreen.cs @@ -47,11 +47,6 @@ namespace osu.Game.Overlays.Mods } } - /// - /// Whether configurable s can be configured by the local user. - /// - protected virtual bool AllowCustomisation => true; - /// /// Whether the total score multiplier calculated from the current selected set of mods should be shown. /// @@ -59,12 +54,28 @@ namespace osu.Game.Overlays.Mods protected virtual ModColumn CreateModColumn(ModType modType, Key[]? toggleKeys = null) => new ModColumn(modType, false, toggleKeys); + protected virtual IEnumerable CreateFooterButtons() => new[] + { + customisationButton = new ShearedToggleButton(200) + { + Text = "Mod Customisation", + Active = { BindTarget = customisationVisible } + }, + new ShearedButton(200) + { + Text = "Deselect All", + Action = DeselectAll + } + }; + private readonly BindableBool customisationVisible = new BindableBool(); private DifficultyMultiplierDisplay? multiplierDisplay; private ModSettingsArea modSettingsArea = null!; private ColumnScrollContainer columnScroll = null!; private ColumnFlowContainer columnFlow = null!; + private ShearedToggleButton? customisationButton; + private FillFlowContainer footerButtonFlow = null!; [BackgroundDependencyLoader] private void load() @@ -95,6 +106,7 @@ namespace osu.Game.Overlays.Mods Padding = new MarginPadding { Top = (ShowTotalMultiplier ? DifficultyMultiplierDisplay.HEIGHT : 0) + PADDING, + Bottom = PADDING }, RelativeSizeAxes = Axes.Both, RelativePositionAxes = Axes.Both, @@ -145,17 +157,21 @@ namespace osu.Game.Overlays.Mods }); } - if (AllowCustomisation) + FooterContent.Child = footerButtonFlow = new FillFlowContainer { - Footer.Add(new ShearedToggleButton(200) + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Horizontal, + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Padding = new MarginPadding { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Margin = new MarginPadding { Vertical = PADDING, Left = 70 }, - Text = "Mod Customisation", - Active = { BindTarget = customisationVisible } - }); - } + Vertical = PADDING, + Horizontal = 70 + }, + Spacing = new Vector2(10), + ChildrenEnumerable = CreateFooterButtons() + }; } private ColumnDimContainer createModColumnContent(ModType modType, Key[]? toggleKeys = null) @@ -210,7 +226,7 @@ namespace osu.Game.Overlays.Mods private void updateCustomisation(ValueChangedEvent> valueChangedEvent) { - if (!AllowCustomisation) + if (customisationButton == null) return; bool anyCustomisableMod = false; @@ -244,6 +260,12 @@ namespace osu.Game.Overlays.Mods MainAreaContent.FadeColour(customisationVisible.Value ? Colour4.Gray : Colour4.White, transition_duration, Easing.InOutCubic); + foreach (var button in footerButtonFlow) + { + if (button != customisationButton) + button.Enabled.Value = !customisationVisible.Value; + } + float modAreaHeight = customisationVisible.Value ? ModSettingsArea.HEIGHT : 0; modSettingsArea.ResizeHeightTo(modAreaHeight, transition_duration, Easing.InOutCubic); @@ -319,6 +341,18 @@ namespace osu.Game.Overlays.Mods } } + protected void SelectAll() + { + foreach (var column in columnFlow.Columns) + column.SelectAll(); + } + + protected void DeselectAll() + { + foreach (var column in columnFlow.Columns) + column.DeselectAll(); + } + public override bool OnPressed(KeyBindingPressEvent e) { if (e.Action == GlobalAction.Back && customisationVisible.Value) @@ -427,6 +461,8 @@ namespace osu.Game.Overlays.Mods FinishTransforms(); } + protected override bool RequiresChildrenUpdate => base.RequiresChildrenUpdate || Column.SelectionAnimationRunning; + private void updateDim() { Colour4 targetColour; diff --git a/osu.Game/Screens/OnlinePlay/FreeModSelectScreen.cs b/osu.Game/Screens/OnlinePlay/FreeModSelectScreen.cs index 5a7a60b479..6298e1f8c0 100644 --- a/osu.Game/Screens/OnlinePlay/FreeModSelectScreen.cs +++ b/osu.Game/Screens/OnlinePlay/FreeModSelectScreen.cs @@ -2,6 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; +using osu.Framework.Graphics; +using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.Mods; using osu.Game.Rulesets.Mods; using osuTK.Input; @@ -10,7 +13,6 @@ namespace osu.Game.Screens.OnlinePlay { public class FreeModSelectScreen : ModSelectScreen { - protected override bool AllowCustomisation => false; protected override bool ShowTotalMultiplier => false; public new Func IsValidMod @@ -25,5 +27,23 @@ namespace osu.Game.Screens.OnlinePlay } protected override ModColumn CreateModColumn(ModType modType, Key[] toggleKeys = null) => new ModColumn(modType, true, toggleKeys); + + protected override IEnumerable CreateFooterButtons() => new[] + { + new ShearedButton(200) + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Text = "Select All", + Action = SelectAll + }, + new ShearedButton(200) + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Text = "Deselect All", + Action = DeselectAll + } + }; } }