diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs index f7140537ee..ef115ab66b 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Linq; using System.Threading; using NUnit.Framework; using osu.Framework.Allocation; @@ -283,7 +282,7 @@ namespace osu.Game.Tests.Visual.Background AddUntilStep("Song select has selection", () => songSelect.Carousel?.SelectedBeatmapInfo != null); AddStep("Set default user settings", () => { - SelectedMods.Value = SelectedMods.Value.Concat(new[] { new OsuModNoFail() }).ToArray(); + SelectedMods.Value = new[] { new OsuModNoFail() }; songSelect.DimLevel.Value = 0.7f; songSelect.BlurLevel.Value = 0.4f; }); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 8e45d99eae..97cb08e3c1 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -627,7 +627,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("invoke on back button", () => multiplayerComponents.OnBackButton()); - AddAssert("mod overlay is hidden", () => this.ChildrenOfType().Single().State.Value == Visibility.Hidden); + AddAssert("mod overlay is hidden", () => this.ChildrenOfType().Single().State.Value == Visibility.Hidden); AddAssert("dialog overlay is hidden", () => DialogOverlay.State.Value == Visibility.Hidden); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs index 714951cc42..061fe5715b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs @@ -132,7 +132,11 @@ namespace osu.Game.Tests.Visual.Multiplayer private void assertHasFreeModButton(Type type, bool hasButton = true) { AddAssert($"{type.ReadableName()} {(hasButton ? "displayed" : "not displayed")} in freemod overlay", - () => songSelect.ChildrenOfType().Single().ChildrenOfType().All(b => b.Mod.GetType() != type)); + () => this.ChildrenOfType() + .Single() + .ChildrenOfType() + .Where(panel => !panel.Filtered.Value) + .All(b => b.Mod.GetType() != type)); } private class TestMultiplayerMatchSongSelect : MultiplayerMatchSongSelect diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index 057032c413..2abde82e92 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -168,8 +168,13 @@ namespace osu.Game.Tests.Visual.Multiplayer ClickButtonWhenEnabled(); + AddUntilStep("mod select contents loaded", + () => this.ChildrenOfType().Any() && this.ChildrenOfType().All(col => col.IsLoaded && col.ItemsLoaded)); AddUntilStep("mod select contains only double time mod", - () => this.ChildrenOfType().SingleOrDefault()?.ChildrenOfType().SingleOrDefault()?.Mod is OsuModDoubleTime); + () => this.ChildrenOfType() + .SingleOrDefault()? + .ChildrenOfType() + .SingleOrDefault(panel => !panel.Filtered.Value)?.Mod is OsuModDoubleTime); } } } diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index a1f41d4caf..f8eee7be56 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -253,12 +253,12 @@ namespace osu.Game.Tests.Visual.Navigation PushAndConfirm(() => songSelect = new TestPlaySongSelect()); AddStep("Show mods overlay", () => songSelect.ModSelectOverlay.Show()); AddAssert("Overlay was shown", () => songSelect.ModSelectOverlay.State.Value == Visibility.Visible); - AddStep("Move mouse to backButton", () => InputManager.MoveMouseTo(backButtonPosition)); - // BackButton handles hover using its child button, so this checks whether or not any of BackButton's children are hovered. - AddUntilStep("Back button is hovered", () => Game.ChildrenOfType().First().Children.Any(c => c.IsHovered)); + AddStep("Move mouse to dimmed area", () => InputManager.MoveMouseTo(new Vector2( + songSelect.ScreenSpaceDrawQuad.TopLeft.X + 1, + songSelect.ScreenSpaceDrawQuad.TopLeft.Y + songSelect.ScreenSpaceDrawQuad.Height / 2))); + AddStep("Click left mouse button", () => InputManager.Click(MouseButton.Left)); - AddStep("Click back button", () => InputManager.Click(MouseButton.Left)); AddUntilStep("Overlay was hidden", () => songSelect.ModSelectOverlay.State.Value == Visibility.Hidden); exitViaBackButtonAndConfirm(); } @@ -551,7 +551,7 @@ namespace osu.Game.Tests.Visual.Navigation public class TestPlaySongSelect : PlaySongSelect { - public ModSelectOverlay ModSelectOverlay => ModSelect; + public ModSelectScreen ModSelectOverlay => ModSelect; public BeatmapOptionsOverlay BeatmapOptionsOverlay => BeatmapOptions; diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index d27f16a624..083e24be7b 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -8,6 +8,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Bindables; +using osu.Framework.Graphics.Containers; using osu.Framework.Platform; using osu.Framework.Screens; using osu.Framework.Testing; @@ -18,6 +19,7 @@ using osu.Game.Extensions; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; +using osu.Game.Overlays.Mods; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; @@ -918,6 +920,19 @@ namespace osu.Game.Tests.Visual.SongSelect AddAssert("check ruleset is correct for score", () => Ruleset.Value.OnlineID == 0); } + [Test] + public void TestModOverlayToggling() + { + changeRuleset(0); + createSongSelect(); + + AddStep("toggle mod overlay on", () => InputManager.Key(Key.F1)); + AddUntilStep("mod overlay shown", () => songSelect.ModSelect.State.Value == Visibility.Visible); + + AddStep("toggle mod overlay off", () => InputManager.Key(Key.F1)); + AddUntilStep("mod overlay hidden", () => songSelect.ModSelect.State.Value == Visibility.Hidden); + } + private void waitForInitialSelection() { AddUntilStep("wait for initial selection", () => !Beatmap.IsDefault); @@ -993,6 +1008,7 @@ namespace osu.Game.Tests.Visual.SongSelect public WorkingBeatmap CurrentBeatmap => Beatmap.Value; public IWorkingBeatmap CurrentBeatmapDetailsBeatmap => BeatmapDetails.Beatmap; public new BeatmapCarousel Carousel => base.Carousel; + public new ModSelectScreen ModSelect => base.ModSelect; public new void PresentScore(ScoreInfo score) => base.PresentScore(score); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneShearedOverlayContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneShearedOverlayContainer.cs index 5a9cafde27..4dd64c6536 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneShearedOverlayContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneShearedOverlayContainer.cs @@ -66,7 +66,10 @@ namespace osu.Game.Tests.Visual.UserInterface public class TestShearedOverlayContainer : ShearedOverlayContainer { - protected override OverlayColourScheme ColourScheme => OverlayColourScheme.Green; + public TestShearedOverlayContainer() + : base(OverlayColourScheme.Green) + { + } [BackgroundDependencyLoader] private void load() diff --git a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs index 68351acd7e..512602d120 100644 --- a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs +++ b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs @@ -34,7 +34,7 @@ namespace osu.Game.Graphics.Containers protected virtual bool DimMainContent => true; [Resolved(CanBeNull = true)] - private OsuGame game { get; set; } + private IOverlayManager overlayManager { get; set; } [Resolved] private PreviewTrackManager previewTrackManager { get; set; } @@ -50,8 +50,8 @@ namespace osu.Game.Graphics.Containers protected override void LoadComplete() { - if (game != null) - OverlayActivationMode.BindTo(game.OverlayActivationMode); + if (overlayManager != null) + OverlayActivationMode.BindTo(overlayManager.OverlayActivationMode); OverlayActivationMode.BindValueChanged(mode => { @@ -127,14 +127,14 @@ namespace osu.Game.Graphics.Containers if (didChange) samplePopIn?.Play(); - if (BlockScreenWideMouse && DimMainContent) game?.AddBlockingOverlay(this); + if (BlockScreenWideMouse && DimMainContent) overlayManager?.ShowBlockingOverlay(this); break; case Visibility.Hidden: if (didChange) samplePopOut?.Play(); - if (BlockScreenWideMouse) game?.RemoveBlockingOverlay(this); + if (BlockScreenWideMouse) overlayManager?.HideBlockingOverlay(this); break; } @@ -150,7 +150,7 @@ namespace osu.Game.Graphics.Containers protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - game?.RemoveBlockingOverlay(this); + overlayManager?.HideBlockingOverlay(this); } } } diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 69adca4190..c2a96a9082 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -63,7 +63,7 @@ namespace osu.Game /// The full osu! experience. Builds on top of to add menus and binding logic /// for initial components that are generally retrieved via DI. /// - public class OsuGame : OsuGameBase, IKeyBindingHandler, ILocalUserPlayInfo, IPerformFromScreenRunner + public class OsuGame : OsuGameBase, IKeyBindingHandler, ILocalUserPlayInfo, IPerformFromScreenRunner, IOverlayManager { /// /// The amount of global offset to apply when a left/right anchored overlay is displayed (ie. settings or notifications). @@ -171,6 +171,7 @@ namespace osu.Game private readonly string[] args; private readonly List focusedOverlays = new List(); + private readonly List externalOverlays = new List(); private readonly List visibleBlockingOverlays = new List(); @@ -183,22 +184,50 @@ namespace osu.Game SentryLogger = new SentryLogger(this); } + #region IOverlayManager + + IBindable IOverlayManager.OverlayActivationMode => OverlayActivationMode; + private void updateBlockingOverlayFade() => ScreenContainer.FadeColour(visibleBlockingOverlays.Any() ? OsuColour.Gray(0.5f) : Color4.White, 500, Easing.OutQuint); - public void AddBlockingOverlay(OverlayContainer overlay) + IDisposable IOverlayManager.RegisterBlockingOverlay(OverlayContainer overlayContainer) + { + if (overlayContainer.Parent != null) + throw new ArgumentException($@"Overlays registered via {nameof(IOverlayManager.RegisterBlockingOverlay)} should not be added to the scene graph."); + + if (externalOverlays.Contains(overlayContainer)) + throw new ArgumentException($@"{overlayContainer} has already been registered via {nameof(IOverlayManager.RegisterBlockingOverlay)} once."); + + externalOverlays.Add(overlayContainer); + overlayContent.Add(overlayContainer); + return new InvokeOnDisposal(() => unregisterBlockingOverlay(overlayContainer)); + } + + void IOverlayManager.ShowBlockingOverlay(OverlayContainer overlay) { if (!visibleBlockingOverlays.Contains(overlay)) visibleBlockingOverlays.Add(overlay); updateBlockingOverlayFade(); } - public void RemoveBlockingOverlay(OverlayContainer overlay) => Schedule(() => + void IOverlayManager.HideBlockingOverlay(OverlayContainer overlay) => Schedule(() => { visibleBlockingOverlays.Remove(overlay); updateBlockingOverlayFade(); }); + /// + /// Unregisters a blocking that was not created by itself. + /// + private void unregisterBlockingOverlay(OverlayContainer overlayContainer) + { + externalOverlays.Remove(overlayContainer); + overlayContainer.Expire(); + } + + #endregion + /// /// Close all game-wide overlays. /// diff --git a/osu.Game/Overlays/FirstRunSetupOverlay.cs b/osu.Game/Overlays/FirstRunSetupOverlay.cs index 75778e6c4d..607bef76dd 100644 --- a/osu.Game/Overlays/FirstRunSetupOverlay.cs +++ b/osu.Game/Overlays/FirstRunSetupOverlay.cs @@ -31,8 +31,6 @@ namespace osu.Game.Overlays [Cached] public class FirstRunSetupOverlay : ShearedOverlayContainer { - protected override OverlayColourScheme ColourScheme => OverlayColourScheme.Purple; - [Resolved] private IPerformFromScreenRunner performer { get; set; } = null!; @@ -70,6 +68,11 @@ namespace osu.Game.Overlays private Container content = null!; + public FirstRunSetupOverlay() + : base(OverlayColourScheme.Purple) + { + } + [BackgroundDependencyLoader] private void load(OsuColour colours) { diff --git a/osu.Game/Overlays/IOverlayManager.cs b/osu.Game/Overlays/IOverlayManager.cs new file mode 100644 index 0000000000..940ee2d8db --- /dev/null +++ b/osu.Game/Overlays/IOverlayManager.cs @@ -0,0 +1,44 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics.Containers; +using osu.Game.Screens.Select; + +namespace osu.Game.Overlays +{ + [Cached] + internal interface IOverlayManager + { + /// + /// Whether overlays should be able to be opened game-wide. Value is sourced from the current active screen. + /// + IBindable OverlayActivationMode { get; } + + /// + /// Registers a blocking that was not created by itself for later use. + /// + /// + /// The goal of this method is to allow child screens, like to register their own full-screen blocking overlays + /// with background dim. + /// In those cases, for the dim to work correctly, the overlays need to be added at a game level directly, rather as children of the screens. + /// + /// + /// An that should be disposed of when the should be unregistered. + /// Disposing of this will automatically expire the . + /// + IDisposable RegisterBlockingOverlay(OverlayContainer overlayContainer); + + /// + /// Should be called when has been shown and should begin blocking background input. + /// + void ShowBlockingOverlay(OverlayContainer overlay); + + /// + /// Should be called when a blocking has been hidden and should stop blocking background input. + /// + void HideBlockingOverlay(OverlayContainer overlay); + } +} diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs index a1ad435733..f0741cdc40 100644 --- a/osu.Game/Overlays/Mods/ModColumn.cs +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -41,7 +41,7 @@ namespace osu.Game.Overlays.Mods private Func? filter; /// - /// Function determining whether each mod in the column should be displayed. + /// A function determining whether each mod in the column should be displayed. /// A return value of means that the mod is not filtered and therefore its corresponding panel should be displayed. /// A return value of means that the mod is filtered out and therefore its corresponding panel should be hidden. /// @@ -250,9 +250,8 @@ namespace osu.Game.Overlays.Mods private void load(OsuGameBase game, OverlayColourProvider colourProvider, OsuColour colours) { availableMods.BindTo(game.AvailableMods); - // this `BindValueChanged` callback is intentionally here, to ensure that local available mods are constructed as early as possible. - // this is needed to make sure no external changes to mods are dropped while mod panels are asynchronously loading. - availableMods.BindValueChanged(_ => updateLocalAvailableMods(), true); + updateLocalAvailableMods(asyncLoadContent: false); + availableMods.BindValueChanged(_ => updateLocalAvailableMods(asyncLoadContent: true)); headerBackground.Colour = accentColour = colours.ForModType(ModType); @@ -279,7 +278,7 @@ namespace osu.Game.Overlays.Mods toggleAllCheckbox.LabelText = toggleAllCheckbox.Current.Value ? CommonStrings.DeselectAll : CommonStrings.SelectAll; } - private void updateLocalAvailableMods() + private void updateLocalAvailableMods(bool asyncLoadContent) { var newMods = ModUtils.FlattenMods(availableMods.Value.GetValueOrDefault(ModType) ?? Array.Empty()) .Select(m => m.DeepClone()) @@ -290,11 +289,10 @@ namespace osu.Game.Overlays.Mods localAvailableMods = newMods; - 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 + if (asyncLoadContent) asyncLoadPanels(); + else + onPanelsLoaded(createPanels()); } private CancellationTokenSource? cancellationTokenSource; diff --git a/osu.Game/Overlays/Mods/ModSelectScreen.cs b/osu.Game/Overlays/Mods/ModSelectScreen.cs index 8ec2260370..0a1b4e857e 100644 --- a/osu.Game/Overlays/Mods/ModSelectScreen.cs +++ b/osu.Game/Overlays/Mods/ModSelectScreen.cs @@ -20,10 +20,10 @@ using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; +using osu.Game.Localisation; using osu.Game.Rulesets.Mods; using osuTK; using osuTK.Input; -using osu.Game.Localisation; namespace osu.Game.Overlays.Mods { @@ -31,13 +31,16 @@ namespace osu.Game.Overlays.Mods { protected const int BUTTON_WIDTH = 200; - protected override OverlayColourScheme ColourScheme => OverlayColourScheme.Green; - [Cached] public Bindable> SelectedMods { get; private set; } = new Bindable>(Array.Empty()); private Func isValidMod = m => true; + /// + /// A function determining whether each mod in the column should be displayed. + /// A return value of means that the mod is not filtered and therefore its corresponding panel should be displayed. + /// A return value of means that the mod is filtered out and therefore its corresponding panel should be hidden. + /// public Func IsValidMod { get => isValidMod; @@ -57,31 +60,27 @@ 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(BUTTON_WIDTH) - { - Text = ModSelectScreenStrings.ModCustomisation, - Active = { BindTarget = customisationVisible } - }, - new ShearedButton(BUTTON_WIDTH) - { - Text = CommonStrings.DeselectAll, - Action = DeselectAll - } - }; + protected virtual IReadOnlyList ComputeNewModsFromSelection(IReadOnlyList oldSelection, IReadOnlyList newSelection) => newSelection; + + protected virtual IEnumerable CreateFooterButtons() => createDefaultFooterButtons(); private readonly BindableBool customisationVisible = new BindableBool(); - private DifficultyMultiplierDisplay? multiplierDisplay; private ModSettingsArea modSettingsArea = null!; private ColumnScrollContainer columnScroll = null!; private ColumnFlowContainer columnFlow = null!; - private FillFlowContainer footerButtonFlow = null!; private ShearedButton backButton = null!; + + private DifficultyMultiplierDisplay? multiplierDisplay; + private ShearedToggleButton? customisationButton; + protected ModSelectScreen(OverlayColourScheme colourScheme = OverlayColourScheme.Green) + : base(colourScheme) + { + } + [BackgroundDependencyLoader] private void load(OsuColour colours) { @@ -185,14 +184,6 @@ namespace osu.Game.Overlays.Mods }; } - private ColumnDimContainer createModColumnContent(ModType modType, Key[]? toggleKeys = null) - => new ColumnDimContainer(CreateModColumn(modType, toggleKeys)) - { - AutoSizeAxes = Axes.X, - RelativeSizeAxes = Axes.Y, - RequestScroll = column => columnScroll.ScrollIntoView(column, extraScroll: 140) - }; - protected override void LoadComplete() { base.LoadComplete(); @@ -216,6 +207,47 @@ namespace osu.Game.Overlays.Mods updateAvailableMods(); } + /// + /// Select all visible mods in all columns. + /// + protected void SelectAll() + { + foreach (var column in columnFlow.Columns) + column.SelectAll(); + } + + /// + /// Deselect all visible mods in all columns. + /// + protected void DeselectAll() + { + foreach (var column in columnFlow.Columns) + column.DeselectAll(); + } + + private ColumnDimContainer createModColumnContent(ModType modType, Key[]? toggleKeys = null) + => new ColumnDimContainer(CreateModColumn(modType, toggleKeys)) + { + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + RequestScroll = column => columnScroll.ScrollIntoView(column, extraScroll: 140) + }; + + private ShearedButton[] createDefaultFooterButtons() + => new[] + { + customisationButton = new ShearedToggleButton(BUTTON_WIDTH) + { + Text = ModSelectScreenStrings.ModCustomisation, + Active = { BindTarget = customisationVisible } + }, + new ShearedButton(BUTTON_WIDTH) + { + Text = CommonStrings.DeselectAll, + Action = DeselectAll + } + }; + private void updateMultiplier() { if (multiplierDisplay == null) @@ -306,7 +338,7 @@ namespace osu.Game.Overlays.Mods SelectedMods.Value = ComputeNewModsFromSelection(SelectedMods.Value, candidateSelection); } - protected virtual IReadOnlyList ComputeNewModsFromSelection(IReadOnlyList oldSelection, IReadOnlyList newSelection) => newSelection; + #region Transition handling protected override void PopIn() { @@ -352,20 +384,17 @@ namespace osu.Game.Overlays.Mods } } - protected void SelectAll() - { - foreach (var column in columnFlow.Columns) - column.SelectAll(); - } + #endregion - protected void DeselectAll() - { - foreach (var column in columnFlow.Columns) - column.DeselectAll(); - } + #region Input handling public override bool OnPressed(KeyBindingPressEvent e) { + if (e.Repeat) + return false; + + // This is handled locally here because this overlay is being registered at the game level + // and therefore takes away keyboard focus from the screen stack. if (e.Action == GlobalAction.Back) { if (customisationVisible.Value) @@ -375,9 +404,27 @@ namespace osu.Game.Overlays.Mods return true; } - return base.OnPressed(e); + switch (e.Action) + { + case GlobalAction.ToggleModSelection: + case GlobalAction.Select: + { + if (customisationVisible.Value) + customisationVisible.Value = false; + Hide(); + return true; + } + + default: + return base.OnPressed(e); + } } + #endregion + + /// + /// Manages horizontal scrolling of mod columns, along with the "active" states of each column based on visibility. + /// internal class ColumnScrollContainer : OsuScrollContainer { public ColumnScrollContainer() @@ -416,6 +463,9 @@ namespace osu.Game.Overlays.Mods } } + /// + /// Manages padding and layout of mod columns. + /// internal class ColumnFlowContainer : FillFlowContainer { public IEnumerable Columns => Children.Select(dimWrapper => dimWrapper.Column); @@ -452,11 +502,21 @@ namespace osu.Game.Overlays.Mods } } + /// + /// Encapsulates a column and provides dim and input blocking based on an externally managed "active" state. + /// internal class ColumnDimContainer : Container { public ModColumn Column { get; } + /// + /// Tracks whether this column is in an interactive state. Generally only the case when the column is on-screen. + /// public readonly Bindable Active = new BindableBool(); + + /// + /// Invoked when the column is clicked while not active, requesting a scroll to be performed to bring it on-screen. + /// public Action? RequestScroll { get; set; } [Resolved] @@ -511,6 +571,9 @@ namespace osu.Game.Overlays.Mods } } + /// + /// A container which blocks and handles input, managing the "return from customisation" state change. + /// private class ClickToReturnContainer : Container { public BindableBool HandleMouse { get; } = new BindableBool(); diff --git a/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs b/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs index eca192c8e5..92e88bfaaf 100644 --- a/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs +++ b/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs @@ -51,17 +51,15 @@ namespace osu.Game.Overlays.Mods /// protected Container FooterContent { get; private set; } - protected abstract OverlayColourScheme ColourScheme { get; } - protected override bool StartHidden => true; protected override bool BlockNonPositionalInput => true; - protected ShearedOverlayContainer() + protected ShearedOverlayContainer(OverlayColourScheme colourScheme) { RelativeSizeAxes = Axes.Both; - ColourProvider = new OverlayColourProvider(ColourScheme); + ColourProvider = new OverlayColourProvider(colourScheme); } [BackgroundDependencyLoader] diff --git a/osu.Game/Overlays/Mods/UserModSelectScreen.cs b/osu.Game/Overlays/Mods/UserModSelectScreen.cs index ca33d35605..a018797cba 100644 --- a/osu.Game/Overlays/Mods/UserModSelectScreen.cs +++ b/osu.Game/Overlays/Mods/UserModSelectScreen.cs @@ -12,6 +12,11 @@ namespace osu.Game.Overlays.Mods { public class UserModSelectScreen : ModSelectScreen { + public UserModSelectScreen(OverlayColourScheme colourScheme = OverlayColourScheme.Green) + : base(colourScheme) + { + } + protected override ModColumn CreateModColumn(ModType modType, Key[] toggleKeys = null) => new UserModColumn(modType, false, toggleKeys); protected override IReadOnlyList ComputeNewModsFromSelection(IReadOnlyList oldSelection, IReadOnlyList newSelection) diff --git a/osu.Game/Screens/OnlinePlay/FreeModSelectScreen.cs b/osu.Game/Screens/OnlinePlay/FreeModSelectScreen.cs index 6d241af043..e92de5e083 100644 --- a/osu.Game/Screens/OnlinePlay/FreeModSelectScreen.cs +++ b/osu.Game/Screens/OnlinePlay/FreeModSelectScreen.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Game.Overlays; using System.Collections.Generic; using osu.Framework.Graphics; using osu.Game.Graphics.UserInterface; @@ -23,6 +24,7 @@ namespace osu.Game.Screens.OnlinePlay } public FreeModSelectScreen() + : base(OverlayColourScheme.Plum) { IsValidMod = _ => true; } diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index cc1f842f8c..ec4e329f7a 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; @@ -57,6 +58,9 @@ namespace osu.Game.Screens.OnlinePlay.Match protected readonly IBindable RoomId = new Bindable(); + [Resolved(CanBeNull = true)] + private IOverlayManager overlayManager { get; set; } + [Resolved] private MusicController music { get; set; } @@ -77,7 +81,11 @@ namespace osu.Game.Screens.OnlinePlay.Match public readonly Room Room; private readonly bool allowEdit; - private ModSelectOverlay userModsSelectOverlay; + private ModSelectScreen userModsSelectOverlay; + + [CanBeNull] + private IDisposable userModsSelectOverlayRegistration; + private RoomSettingsOverlay settingsOverlay; private Drawable mainContent; @@ -180,11 +188,6 @@ namespace osu.Game.Screens.OnlinePlay.Match Origin = Anchor.BottomLeft, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Child = userModsSelectOverlay = new UserModSelectOverlay - { - SelectedMods = { BindTarget = UserMods }, - IsValidMod = _ => false - } }, } } @@ -227,6 +230,12 @@ namespace osu.Game.Screens.OnlinePlay.Match } } }; + + LoadComponent(userModsSelectOverlay = new UserModSelectScreen(OverlayColourScheme.Plum) + { + SelectedMods = { BindTarget = UserMods }, + IsValidMod = _ => false + }); } protected override void LoadComplete() @@ -254,6 +263,8 @@ namespace osu.Game.Screens.OnlinePlay.Match beatmapAvailabilityTracker.SelectedItem.BindTo(SelectedItem); beatmapAvailabilityTracker.Availability.BindValueChanged(_ => updateWorkingBeatmap()); + + userModsSelectOverlayRegistration = overlayManager?.RegisterBlockingOverlay(userModsSelectOverlay); } protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) @@ -298,7 +309,7 @@ namespace osu.Game.Screens.OnlinePlay.Match public override void OnSuspending(ScreenTransitionEvent e) { - endHandlingTrack(); + onLeaving(); base.OnSuspending(e); } @@ -316,7 +327,7 @@ namespace osu.Game.Screens.OnlinePlay.Match RoomManager?.PartRoom(); Mods.Value = Array.Empty(); - endHandlingTrack(); + onLeaving(); return base.OnExiting(e); } @@ -412,6 +423,12 @@ namespace osu.Game.Screens.OnlinePlay.Match Beatmap.BindValueChanged(applyLoopingToTrack, true); } + private void onLeaving() + { + userModsSelectOverlay.Hide(); + endHandlingTrack(); + } + private void endHandlingTrack() { Beatmap.ValueChanged -= applyLoopingToTrack; @@ -459,5 +476,12 @@ namespace osu.Game.Screens.OnlinePlay.Match public class UserModSelectButton : PurpleTriangleButton { } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + userModsSelectOverlayRegistration?.Dispose(); + } } } diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs index 6a559dbb2c..c4503773ad 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs @@ -14,6 +14,7 @@ using osu.Framework.Screens; using osu.Game.Beatmaps; using osu.Game.Online.API; using osu.Game.Online.Rooms; +using osu.Game.Overlays; using osu.Game.Overlays.Mods; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; @@ -45,7 +46,6 @@ namespace osu.Game.Screens.OnlinePlay protected readonly Bindable> FreeMods = new Bindable>(Array.Empty()); - private readonly FreeModSelectOverlay freeModSelectOverlay; private readonly Room room; private WorkingBeatmap initialBeatmap; @@ -53,13 +53,16 @@ namespace osu.Game.Screens.OnlinePlay private IReadOnlyList initialMods; private bool itemSelected; + private readonly FreeModSelectScreen freeModSelectOverlay; + private IDisposable freeModSelectOverlayRegistration; + protected OnlinePlaySongSelect(Room room) { this.room = room; Padding = new MarginPadding { Horizontal = HORIZONTAL_OVERFLOW_PADDING }; - freeModSelectOverlay = new FreeModSelectOverlay + freeModSelectOverlay = new FreeModSelectScreen { SelectedMods = { BindTarget = FreeMods }, IsValidMod = IsValidFreeMod, @@ -75,7 +78,7 @@ namespace osu.Game.Screens.OnlinePlay initialRuleset = Ruleset.Value; initialMods = Mods.Value.ToList(); - FooterPanels.Add(freeModSelectOverlay); + LoadComponent(freeModSelectOverlay); } protected override void LoadComplete() @@ -94,6 +97,8 @@ namespace osu.Game.Screens.OnlinePlay Mods.BindValueChanged(onModsChanged); Ruleset.BindValueChanged(onRulesetChanged); + + freeModSelectOverlayRegistration = OverlayManager?.RegisterBlockingOverlay(freeModSelectOverlay); } private void onModsChanged(ValueChangedEvent> mods) @@ -150,10 +155,12 @@ namespace osu.Game.Screens.OnlinePlay Mods.Value = initialMods; } + freeModSelectOverlay.Hide(); + return base.OnExiting(e); } - protected override ModSelectOverlay CreateModSelectOverlay() => new UserModSelectOverlay + protected override ModSelectScreen CreateModSelectOverlay() => new UserModSelectScreen(OverlayColourScheme.Plum) { IsValidMod = IsValidMod }; @@ -182,5 +189,12 @@ namespace osu.Game.Screens.OnlinePlay private bool checkCompatibleFreeMod(Mod mod) => Mods.Value.All(m => m.Acronym != mod.Acronym) // Mod must not be contained in the required mods. && ModUtils.CheckCompatibleSet(Mods.Value.Append(mod).ToArray()); // Mod must be compatible with all the required mods. + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + freeModSelectOverlayRegistration?.Dispose(); + } } } diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 2a1ed2a7a8..eb5e996972 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -35,6 +35,7 @@ using osu.Framework.Input.Bindings; using osu.Game.Collections; using osu.Game.Graphics.UserInterface; using System.Diagnostics; +using JetBrains.Annotations; using osu.Game.Screens.Play; using osu.Game.Database; using osu.Game.Skinning; @@ -101,7 +102,7 @@ namespace osu.Game.Screens.Select [Resolved(CanBeNull = true)] private LegacyImportManager legacyImportManager { get; set; } - protected ModSelectOverlay ModSelect { get; private set; } + protected ModSelectScreen ModSelect { get; private set; } protected Sample SampleConfirm { get; private set; } @@ -116,9 +117,15 @@ namespace osu.Game.Screens.Select private double audioFeedbackLastPlaybackTime; + [CanBeNull] + private IDisposable modSelectOverlayRegistration; + [Resolved] private MusicController music { get; set; } + [Resolved(CanBeNull = true)] + internal IOverlayManager OverlayManager { get; private set; } + [BackgroundDependencyLoader(true)] private void load(AudioManager audio, IDialogOverlay dialog, OsuColour colours, ManageCollectionsDialog manageCollectionsDialog, DifficultyRecommender recommender) { @@ -252,38 +259,25 @@ namespace osu.Game.Screens.Select { AddRangeInternal(new Drawable[] { - new GridContainer // used for max height implementation + FooterPanels = new Container { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, RelativeSizeAxes = Axes.Both, - RowDimensions = new[] + Padding = new MarginPadding { Bottom = Footer.HEIGHT }, + Children = new Drawable[] { - new Dimension(), - new Dimension(GridSizeMode.Relative, 1f, maxSize: ModSelectOverlay.HEIGHT + Footer.HEIGHT), - }, - Content = new[] - { - null, - new Drawable[] - { - FooterPanels = new Container - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Bottom = Footer.HEIGHT }, - Children = new Drawable[] - { - BeatmapOptions = new BeatmapOptionsOverlay(), - ModSelect = CreateModSelectOverlay() - } - } - } + BeatmapOptions = new BeatmapOptionsOverlay(), } }, - Footer = new Footer() + Footer = new Footer(), }); } + // preload the mod select overlay for later use in `LoadComplete()`. + // therein it will be registered at the `OsuGame` level to properly function as a blocking overlay. + LoadComponent(ModSelect = CreateModSelectOverlay()); + if (Footer != null) { foreach (var (button, overlay) in CreateFooterButtons()) @@ -317,6 +311,13 @@ namespace osu.Game.Screens.Select } } + protected override void LoadComplete() + { + base.LoadComplete(); + + modSelectOverlayRegistration = OverlayManager?.RegisterBlockingOverlay(ModSelect); + } + /// /// Creates the buttons to be displayed in the footer. /// @@ -332,7 +333,7 @@ namespace osu.Game.Screens.Select (new FooterButtonOptions(), BeatmapOptions) }; - protected virtual ModSelectOverlay CreateModSelectOverlay() => new UserModSelectOverlay(); + protected virtual ModSelectScreen CreateModSelectOverlay() => new UserModSelectScreen(); protected virtual void ApplyFilterToCarousel(FilterCriteria criteria) { @@ -658,6 +659,7 @@ namespace osu.Game.Screens.Select return true; beatmapInfoWedge.Hide(); + ModSelect.Hide(); this.FadeOut(100); @@ -716,6 +718,8 @@ namespace osu.Game.Screens.Select if (music != null) music.TrackChanged -= ensureTrackLooping; + + modSelectOverlayRegistration?.Dispose(); } /// diff --git a/osu.Game/Tests/Visual/ScreenTestScene.cs b/osu.Game/Tests/Visual/ScreenTestScene.cs index e9069d8c9c..0fa2f3e786 100644 --- a/osu.Game/Tests/Visual/ScreenTestScene.cs +++ b/osu.Game/Tests/Visual/ScreenTestScene.cs @@ -1,12 +1,15 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Development; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Logging; using osu.Framework.Testing; +using osu.Game.Graphics; using osu.Game.Overlays; using osu.Game.Screens; @@ -15,11 +18,12 @@ namespace osu.Game.Tests.Visual /// /// A test case which can be used to test a screen (that relies on OnEntering being called to execute startup instructions). /// - public abstract class ScreenTestScene : OsuManualInputManagerTestScene + public abstract class ScreenTestScene : OsuManualInputManagerTestScene, IOverlayManager { protected readonly OsuScreenStack Stack; private readonly Container content; + private readonly Container overlayContent; protected override Container Content => content; @@ -36,7 +40,11 @@ namespace osu.Game.Tests.Visual RelativeSizeAxes = Axes.Both }, content = new Container { RelativeSizeAxes = Axes.Both }, - DialogOverlay = new DialogOverlay() + overlayContent = new Container + { + RelativeSizeAxes = Axes.Both, + Child = DialogOverlay = new DialogOverlay() + } }); Stack.ScreenPushed += (lastScreen, newScreen) => Logger.Log($"{nameof(ScreenTestScene)} screen changed → {newScreen}"); @@ -65,5 +73,26 @@ namespace osu.Game.Tests.Visual return false; }); } + + #region IOverlayManager + + IBindable IOverlayManager.OverlayActivationMode { get; } = new Bindable(OverlayActivation.All); + + // in the blocking methods below it is important to be careful about threading (e.g. use `Expire()` rather than `Remove()`, and schedule transforms), + // because in the worst case the clean-up methods could be called from async disposal. + + IDisposable IOverlayManager.RegisterBlockingOverlay(OverlayContainer overlayContainer) + { + overlayContent.Add(overlayContainer); + return new InvokeOnDisposal(() => overlayContainer.Expire()); + } + + void IOverlayManager.ShowBlockingOverlay(OverlayContainer overlay) + => Schedule(() => Stack.FadeColour(OsuColour.Gray(0.5f), 500, Easing.OutQuint)); + + void IOverlayManager.HideBlockingOverlay(OverlayContainer overlay) + => Schedule(() => Stack.FadeColour(Colour4.White, 500, Easing.OutQuint)); + + #endregion } }