diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs index 6e940f65b0..a8d991c3a7 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs @@ -12,11 +12,10 @@ using osu.Game.Overlays.Dialog; using osu.Game.Overlays.Mods; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; -using osu.Game.Screens.Footer; -using osu.Game.Screens.Play; using osu.Game.Screens.Select; using osuTK.Input; using FooterButtonMods = osu.Game.Screens.SelectV2.FooterButtonMods; +using FooterButtonOptions = osu.Game.Screens.SelectV2.FooterButtonOptions; namespace osu.Game.Tests.Visual.SongSelectV2 { @@ -382,17 +381,20 @@ namespace osu.Game.Tests.Visual.SongSelectV2 // } [Test] - public void TestFooterShowOptions() + public void TestFooterOptions() { LoadSongSelect(); - AddStep("enable options", () => - { - var optionsButton = this.ChildrenOfType().Last(); + ImportBeatmapForRuleset(0); - optionsButton.Enabled.Value = true; - optionsButton.TriggerClick(); - }); + // song select should automatically select the beatmap for us but this is not implemented yet. + // todo: remove when that's the case. + AddAssert("no beatmap selected", () => Beatmap.IsDefault); + AddStep("select beatmap", () => Beatmap.Value = Beatmaps.GetWorkingBeatmap(Beatmaps.GetAllUsableBeatmapSets().Single().Beatmaps.First())); + AddAssert("options enabled", () => this.ChildrenOfType().Single().Enabled.Value); + + AddStep("click", () => this.ChildrenOfType().Single().TriggerClick()); + AddUntilStep("popover displayed", () => this.ChildrenOfType().Any(p => p.IsPresent)); } [Test] @@ -400,7 +402,23 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { LoadSongSelect(); - AddToggleStep("set options enabled state", state => this.ChildrenOfType().Last().Enabled.Value = state); + ImportBeatmapForRuleset(0); + + // song select should automatically select the beatmap for us but this is not implemented yet. + // todo: remove when that's the case. + AddAssert("no beatmap selected", () => Beatmap.IsDefault); + AddStep("select beatmap", () => Beatmap.Value = Beatmaps.GetWorkingBeatmap(Beatmaps.GetAllUsableBeatmapSets().Single().Beatmaps.First())); + + AddAssert("options enabled", () => this.ChildrenOfType().Single().Enabled.Value); + AddStep("delete all beatmaps", () => Beatmaps.Delete()); + + // song select should automatically select the beatmap for us but this is not implemented yet. + // todo: remove when that's the case. + AddAssert("beatmap selected", () => !Beatmap.IsDefault); + AddStep("select no beatmap", () => Beatmap.SetDefault()); + + AddUntilStep("wait for no beatmap", () => Beatmap.IsDefault); + AddAssert("options disabled", () => !this.ChildrenOfType().Single().Enabled.Value); } #endregion diff --git a/osu.Game/Screens/SelectV2/FooterButtonOptions.cs b/osu.Game/Screens/SelectV2/FooterButtonOptions.cs index 41edaf2a02..c7800b44c3 100644 --- a/osu.Game/Screens/SelectV2/FooterButtonOptions.cs +++ b/osu.Game/Screens/SelectV2/FooterButtonOptions.cs @@ -2,9 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Sprites; +using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Input.Bindings; using osu.Game.Overlays; @@ -17,6 +19,12 @@ namespace osu.Game.Screens.SelectV2 [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; + [Resolved] + private IBindable beatmap { get; set; } = null!; + + [Resolved] + private ISongSelectBeatmapActions? beatmapActions { get; set; } + [BackgroundDependencyLoader] private void load(OsuColour colour) { @@ -28,6 +36,22 @@ namespace osu.Game.Screens.SelectV2 Action = this.ShowPopover; } - public Framework.Graphics.UserInterface.Popover GetPopover() => new Popover(this, colourProvider); + protected override void LoadComplete() + { + base.LoadComplete(); + beatmap.BindValueChanged(_ => beatmapChanged(), true); + } + + private void beatmapChanged() + { + this.HidePopover(); + Enabled.Value = !beatmap.IsDefault; + } + + public Framework.Graphics.UserInterface.Popover GetPopover() => new Popover(this, beatmap.Value) + { + ColourProvider = colourProvider, + BeatmapActions = beatmapActions + }; } } diff --git a/osu.Game/Screens/SelectV2/FooterButtonOptions_Popover.cs b/osu.Game/Screens/SelectV2/FooterButtonOptions_Popover.cs index 76b841ee99..ca43bc3fe5 100644 --- a/osu.Game/Screens/SelectV2/FooterButtonOptions_Popover.cs +++ b/osu.Game/Screens/SelectV2/FooterButtonOptions_Popover.cs @@ -12,7 +12,6 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Beatmaps; -using osu.Game.Collections; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; @@ -34,22 +33,21 @@ namespace osu.Game.Screens.SelectV2 private FillFlowContainer buttonFlow = null!; private readonly FooterButtonOptions footerButton; - [Cached] - private readonly OverlayColourProvider colourProvider; + private readonly WorkingBeatmap beatmap; - private WorkingBeatmap beatmapWhenOpening = null!; + // Can't use DI for these due to popover being initialised from a footer button which ends up being on the global + // PopoverContainer. + public ISongSelectBeatmapActions? BeatmapActions { get; init; } + public required OverlayColourProvider ColourProvider { get; init; } - [Resolved] - private IBindable beatmap { get; set; } = null!; - - public Popover(FooterButtonOptions footerButton, OverlayColourProvider colourProvider) + public Popover(FooterButtonOptions footerButton, WorkingBeatmap beatmap) { this.footerButton = footerButton; - this.colourProvider = colourProvider; + this.beatmap = beatmap; } [BackgroundDependencyLoader] - private void load(ManageCollectionsDialog? manageCollectionsDialog, OsuColour colours, BeatmapManager? beatmapManager) + private void load(OsuColour colours) { Content.Padding = new MarginPadding(5); @@ -60,23 +58,21 @@ namespace osu.Game.Screens.SelectV2 Spacing = new Vector2(3), }; - beatmapWhenOpening = beatmap.Value; - addHeader(CommonStrings.General); - addButton(SongSelectStrings.ManageCollections, FontAwesome.Solid.Book, () => manageCollectionsDialog?.Show()); + addButton(SongSelectStrings.ManageCollections, FontAwesome.Solid.Book, () => BeatmapActions?.ManageCollections()); - addHeader(SongSelectStrings.ForAllDifficulties, beatmapWhenOpening.BeatmapSetInfo.ToString()); - addButton(SongSelectStrings.DeleteBeatmap, FontAwesome.Solid.Trash, () => { }, colours.Red1); // songSelect?.DeleteBeatmap(beatmapWhenOpening.BeatmapSetInfo); + addHeader(SongSelectStrings.ForAllDifficulties, beatmap.BeatmapSetInfo.ToString()); + addButton(SongSelectStrings.DeleteBeatmap, FontAwesome.Solid.Trash, () => BeatmapActions?.Delete(beatmap.BeatmapSetInfo), colours.Red1); - addHeader(SongSelectStrings.ForSelectedDifficulty, beatmapWhenOpening.BeatmapInfo.DifficultyName); - // TODO: make work, and make show "unplayed" or "played" based on status. - addButton(SongSelectStrings.MarkAsPlayed, FontAwesome.Regular.TimesCircle, null); - addButton(SongSelectStrings.ClearAllLocalScores, FontAwesome.Solid.Eraser, () => { }, colours.Red1); // songSelect?.ClearScores(beatmapWhenOpening.BeatmapInfo); + addHeader(SongSelectStrings.ForSelectedDifficulty, beatmap.BeatmapInfo.DifficultyName); + // TODO: replace with "remove from played" button when beatmap is already played. + addButton(SongSelectStrings.MarkAsPlayed, FontAwesome.Regular.TimesCircle, () => BeatmapActions?.MarkPlayed(beatmap.BeatmapInfo)); + addButton(SongSelectStrings.ClearAllLocalScores, FontAwesome.Solid.Eraser, () => BeatmapActions?.ClearScores(beatmap.BeatmapInfo), colours.Red1); - // if (songSelect != null && songSelect.AllowEditing) - addButton(SongSelectStrings.EditBeatmap, FontAwesome.Solid.PencilAlt, () => { }); // songSelect.Edit(beatmapWhenOpening.BeatmapInfo); + if (BeatmapActions?.EditingAllowed == true) + addButton(SongSelectStrings.EditBeatmap, FontAwesome.Solid.PencilAlt, () => BeatmapActions.Edit(beatmap.BeatmapInfo)); - addButton(WebCommonStrings.ButtonsHide.ToSentence(), FontAwesome.Solid.Magic, () => beatmapManager?.Hide(beatmapWhenOpening.BeatmapInfo)); + addButton(WebCommonStrings.ButtonsHide.ToSentence(), FontAwesome.Solid.Magic, () => BeatmapActions?.Hide(beatmap.BeatmapInfo)); } protected override void LoadComplete() @@ -84,8 +80,12 @@ namespace osu.Game.Screens.SelectV2 base.LoadComplete(); ScheduleAfterChildren(() => GetContainingFocusManager()!.ChangeFocus(this)); + } - beatmap.BindValueChanged(_ => Hide()); + protected override void UpdateState(ValueChangedEvent state) + { + base.UpdateState(state); + footerButton.OverlayState.Value = state.NewValue; } private void addHeader(LocalisableString text, string? context = null) @@ -104,7 +104,7 @@ namespace osu.Game.Screens.SelectV2 textFlow.NewLine(); textFlow.AddText(context, t => { - t.Colour = colourProvider.Content2; + t.Colour = ColourProvider.Content2; t.Font = t.Font.With(size: 13); }); } @@ -118,6 +118,7 @@ namespace osu.Game.Screens.SelectV2 { Text = text, Icon = icon, + BackgroundColour = ColourProvider.Background3, TextColour = colour, Action = () => { @@ -129,44 +130,6 @@ namespace osu.Game.Screens.SelectV2 buttonFlow.Add(button); } - private partial class OptionButton : OsuButton - { - public IconUsage Icon { get; init; } - public Color4? TextColour { get; init; } - - public OptionButton() - { - Size = new Vector2(265, 50); - } - - [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) - { - BackgroundColour = colourProvider.Background3; - - SpriteText.Colour = TextColour ?? Color4.White; - Content.CornerRadius = 10; - - Add(new SpriteIcon - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Size = new Vector2(17), - X = 15, - Icon = Icon, - Colour = TextColour ?? Color4.White, - }); - } - - protected override SpriteText CreateText() => new OsuSpriteText - { - Depth = -1, - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - X = 40 - }; - } - protected override bool OnKeyDown(KeyDownEvent e) { // don't absorb control as ToolbarRulesetSelector uses control + number to navigate @@ -188,10 +151,40 @@ namespace osu.Game.Screens.SelectV2 return base.OnKeyDown(e); } - protected override void UpdateState(ValueChangedEvent state) + private partial class OptionButton : OsuButton { - base.UpdateState(state); - footerButton.OverlayState.Value = state.NewValue; + public IconUsage Icon { get; init; } + public Color4? TextColour { get; init; } + + public OptionButton() + { + Size = new Vector2(265, 50); + } + + [BackgroundDependencyLoader] + private void load() + { + SpriteText.Colour = TextColour ?? Color4.White; + Content.CornerRadius = 10; + + Add(new SpriteIcon + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(17), + X = 15, + Icon = Icon, + Colour = TextColour ?? Color4.White, + }); + } + + protected override SpriteText CreateText() => new OsuSpriteText + { + Depth = -1, + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + X = 40 + }; } } } diff --git a/osu.Game/Screens/SelectV2/ISongSelectBeatmapActions.cs b/osu.Game/Screens/SelectV2/ISongSelectBeatmapActions.cs new file mode 100644 index 0000000000..388967bc4f --- /dev/null +++ b/osu.Game/Screens/SelectV2/ISongSelectBeatmapActions.cs @@ -0,0 +1,48 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Beatmaps; + +namespace osu.Game.Screens.SelectV2 +{ + /// + /// Actions exposed by song select which are used by subcomponents to perform top-level operations. + /// + public interface ISongSelectBeatmapActions + { + /// + /// Requests the user for confirmation to delete the given beatmap set. + /// + void Delete(BeatmapSetInfo beatmapBeatmapSetInfo); + + /// + /// Requests the user for confirmation to clear all local scores in the given beatmap. + /// + void ClearScores(BeatmapInfo beatmap); + + /// + /// Opens beatmap editor with the given beatmap. + /// + void Edit(BeatmapInfo beatmap); + + /// + /// Whether calls to will succeed or not. + /// + bool EditingAllowed { get; } + + /// + /// Opens the manage collections dialog. + /// + void ManageCollections(); + + /// + /// Marks a beatmap manually as being played. + /// + void MarkPlayed(BeatmapInfo beatmap); + + /// + /// Hides a beatmap from user's vision. + /// + void Hide(BeatmapInfo beatmap); + } +} diff --git a/osu.Game/Screens/SelectV2/SoloSongSelect.cs b/osu.Game/Screens/SelectV2/SoloSongSelect.cs index ac64052395..7d62af8c9c 100644 --- a/osu.Game/Screens/SelectV2/SoloSongSelect.cs +++ b/osu.Game/Screens/SelectV2/SoloSongSelect.cs @@ -23,6 +23,8 @@ namespace osu.Game.Screens.SelectV2 [Resolved] private INotificationOverlay? notifications { get; set; } + public override bool EditingAllowed => true; + protected override bool OnStart() { if (playerLoader != null) return false; @@ -98,7 +100,7 @@ namespace osu.Game.Screens.SelectV2 playerLoader = null; } - private partial class PlayerLoader : Screens.Play.PlayerLoader + private partial class PlayerLoader : Play.PlayerLoader { public override bool ShowFooter => true; diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index b9898e841d..43fa394e39 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -15,11 +15,13 @@ using osu.Framework.Input.Events; using osu.Framework.Screens; using osu.Framework.Threading; using osu.Game.Beatmaps; +using osu.Game.Collections; using osu.Game.Graphics.Containers; using osu.Game.Input.Bindings; using osu.Game.Overlays; using osu.Game.Overlays.Mods; using osu.Game.Overlays.Volume; +using osu.Game.Screens.Edit; using osu.Game.Screens.Footer; using osu.Game.Screens.Menu; using osu.Game.Screens.Select; @@ -35,8 +37,8 @@ namespace osu.Game.Screens.SelectV2 /// This screen is intended to house all components introduced in the new song select design to add transitions and examine the overall look. /// This will be gradually built upon and ultimately replace once everything is in place. /// - [Cached(typeof(SongSelect))] - public abstract partial class SongSelect : OsuScreen, IKeyBindingHandler + [Cached(typeof(ISongSelectBeatmapActions))] + public abstract partial class SongSelect : OsuScreen, IKeyBindingHandler, ISongSelectBeatmapActions { private const float logo_scale = 0.4f; private const double fade_duration = 300; @@ -77,6 +79,12 @@ namespace osu.Game.Screens.SelectV2 [Resolved] private IDialogOverlay? dialogOverlay { get; set; } + [Resolved] + private BeatmapManager beatmaps { get; set; } = null!; + + [Resolved] + private ManageCollectionsDialog? collectionsDialog { get; set; } + [BackgroundDependencyLoader] private void load() { @@ -343,26 +351,6 @@ namespace osu.Game.Screens.SelectV2 #endregion - #region Beatmap management - - /// - /// Requests the user for confirmation to delete the given beatmap set. - /// - public void DeleteBeatmap(BeatmapSetInfo beatmapSet) - { - dialogOverlay?.Push(new BeatmapDeleteDialog(beatmapSet)); - } - - /// - /// Requests the user for confirmation to clear all local scores in the given beatmap. - /// - public void ClearScores(BeatmapInfo beatmap) - { - dialogOverlay?.Push(new BeatmapClearScoresDialog(beatmap)); - } - - #endregion - #region Hotkeys public virtual bool OnPressed(KeyBindingPressEvent e) @@ -395,7 +383,7 @@ namespace osu.Game.Screens.SelectV2 if (e.ShiftPressed) { if (!Beatmap.IsDefault) - DeleteBeatmap(Beatmap.Value.BeatmapSetInfo); + Delete(Beatmap.Value.BeatmapSetInfo); return true; } @@ -406,5 +394,30 @@ namespace osu.Game.Screens.SelectV2 } #endregion + + #region Beatmap management + + public virtual bool EditingAllowed => false; + + public void ManageCollections() => collectionsDialog?.Show(); + + public void MarkPlayed(BeatmapInfo beatmap) => beatmaps.MarkPlayed(beatmap); + + public void Hide(BeatmapInfo beatmap) => beatmaps.Hide(beatmap); + + public void Edit(BeatmapInfo beatmap) + { + if (!EditingAllowed) return; + + // Forced refetch is important here to guarantee correct invalidation across all difficulties. + Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap, true); + this.Push(new EditorLoader()); + } + + public void Delete(BeatmapSetInfo beatmapSet) => dialogOverlay?.Push(new BeatmapDeleteDialog(beatmapSet)); + + public void ClearScores(BeatmapInfo beatmap) => dialogOverlay?.Push(new BeatmapClearScoresDialog(beatmap)); + + #endregion } }