Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add second level menu for editors #25561

Merged
merged 9 commits into from
Nov 24, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public void TestErrorNotifications()
() => Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.MustBeInEditorToHandleLinks),
() => Is.EqualTo(1));

AddStep("enter song select", () => Game.ChildrenOfType<ButtonSystem>().Single().OnSolo.Invoke());
AddStep("enter song select", () => Game.ChildrenOfType<ButtonSystem>().Single().OnSolo?.Invoke());
AddUntilStep("entered song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect);

addStepClickLink("00:00:000 (1)", waitForSeek: false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ public void TestCreateNewDifficultyOnNonExistentBeatmap()
{
AddUntilStep("wait for dialog overlay", () => Game.ChildrenOfType<DialogOverlay>().SingleOrDefault() != null);

AddStep("open editor", () => Game.ChildrenOfType<ButtonSystem>().Single().OnEdit.Invoke());
AddStep("open editor", () => Game.ChildrenOfType<ButtonSystem>().Single().OnEditBeatmap?.Invoke());
AddUntilStep("wait for editor", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.IsLoaded);
AddStep("click on file", () =>
{
Expand Down
8 changes: 4 additions & 4 deletions osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ public void TestFromMainMenuDifferentRuleset([Values] ScorePresentType type)
[Test]
public void TestFromSongSelectWithFilter([Values] ScorePresentType type)
{
AddStep("enter song select", () => Game.ChildrenOfType<ButtonSystem>().Single().OnSolo.Invoke());
AddStep("enter song select", () => Game.ChildrenOfType<ButtonSystem>().Single().OnSolo?.Invoke());
AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.BeatmapSetsLoaded);

AddStep("filter to nothing", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).FilterControl.CurrentTextSearch.Value = "fdsajkl;fgewq");
Expand All @@ -110,7 +110,7 @@ public void TestFromSongSelectWithFilter([Values] ScorePresentType type)
[Test]
public void TestFromSongSelectWithConvertRulesetChange([Values] ScorePresentType type)
{
AddStep("enter song select", () => Game.ChildrenOfType<ButtonSystem>().Single().OnSolo.Invoke());
AddStep("enter song select", () => Game.ChildrenOfType<ButtonSystem>().Single().OnSolo?.Invoke());
AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.BeatmapSetsLoaded);

AddStep("set convert to false", () => Game.LocalConfig.SetValue(OsuSetting.ShowConvertedBeatmaps, false));
Expand All @@ -122,7 +122,7 @@ public void TestFromSongSelectWithConvertRulesetChange([Values] ScorePresentType
[Test]
public void TestFromSongSelect([Values] ScorePresentType type)
{
AddStep("enter song select", () => Game.ChildrenOfType<ButtonSystem>().Single().OnSolo.Invoke());
AddStep("enter song select", () => Game.ChildrenOfType<ButtonSystem>().Single().OnSolo?.Invoke());
AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.BeatmapSetsLoaded);

var firstImport = importScore(1);
Expand All @@ -135,7 +135,7 @@ public void TestFromSongSelect([Values] ScorePresentType type)
[Test]
public void TestFromSongSelectDifferentRuleset([Values] ScorePresentType type)
{
AddStep("enter song select", () => Game.ChildrenOfType<ButtonSystem>().Single().OnSolo.Invoke());
AddStep("enter song select", () => Game.ChildrenOfType<ButtonSystem>().Single().OnSolo?.Invoke());
AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.BeatmapSetsLoaded);

var firstImport = importScore(1);
Expand Down
31 changes: 18 additions & 13 deletions osu.Game.Tests/Visual/UserInterface/TestSceneButtonSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,15 @@ public void TestSmoothExit()
AddStep("Enter mode", performEnterMode);
}

[TestCase(Key.P, true)]
[TestCase(Key.M, true)]
[TestCase(Key.L, true)]
[TestCase(Key.E, false)]
[TestCase(Key.D, false)]
[TestCase(Key.Q, false)]
[TestCase(Key.O, false)]
public void TestShortcutKeys(Key key, bool entersPlay)
[TestCase(Key.P, Key.P)]
[TestCase(Key.M, Key.P)]
[TestCase(Key.L, Key.P)]
[TestCase(Key.B, Key.E)]
[TestCase(Key.S, Key.E)]
[TestCase(Key.D, null)]
[TestCase(Key.Q, null)]
[TestCase(Key.O, null)]
public void TestShortcutKeys(Key key, Key? subMenuEnterKey)
{
int activationCount = -1;
AddStep("set up action", () =>
Expand All @@ -96,8 +97,12 @@ public void TestShortcutKeys(Key key, bool entersPlay)
buttons.OnPlaylists = action;
break;

case Key.E:
buttons.OnEdit = action;
case Key.B:
buttons.OnEditBeatmap = action;
break;

case Key.S:
buttons.OnEditSkin = action;
break;

case Key.D:
Expand All @@ -117,10 +122,10 @@ public void TestShortcutKeys(Key key, bool entersPlay)
AddStep($"press {key}", () => InputManager.Key(key));
AddAssert("state is top level", () => buttons.State == ButtonSystemState.TopLevel);

if (entersPlay)
if (subMenuEnterKey != null)
{
AddStep("press P", () => InputManager.Key(Key.P));
AddAssert("state is play", () => buttons.State == ButtonSystemState.Play);
AddStep($"press {subMenuEnterKey}", () => InputManager.Key(subMenuEnterKey.Value));
AddAssert("state is not top menu", () => buttons.State != ButtonSystemState.TopLevel);
}

AddStep($"press {key}", () => InputManager.Key(key));
Expand Down
5 changes: 5 additions & 0 deletions osu.Game/Localisation/EditorStrings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ public static class EditorStrings
{
private const string prefix = @"osu.Game.Resources.Localisation.Editor";

/// <summary>
/// "Beatmap editor"
/// </summary>
public static LocalisableString BeatmapEditor => new TranslatableString(getKey(@"beatmap_editor"), @"Beatmap editor");

/// <summary>
/// "Waveform opacity"
/// </summary>
Expand Down
67 changes: 38 additions & 29 deletions osu.Game/Screens/Menu/ButtonSystem.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

#nullable disable

using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
Expand All @@ -36,29 +34,29 @@ namespace osu.Game.Screens.Menu
{
public partial class ButtonSystem : Container, IStateful<ButtonSystemState>, IKeyBindingHandler<GlobalAction>
{
public event Action<ButtonSystemState> StateChanged;
public const float BUTTON_WIDTH = 140f;
public const float WEDGE_WIDTH = 20;

private readonly IBindable<bool> isIdle = new BindableBool();
public event Action<ButtonSystemState>? StateChanged;

public Action OnEdit;
public Action OnExit;
public Action OnBeatmapListing;
public Action OnSolo;
public Action OnSettings;
public Action OnMultiplayer;
public Action OnPlaylists;
public Action? OnEditBeatmap;
public Action? OnEditSkin;
public Action? OnExit;
public Action? OnBeatmapListing;
public Action? OnSolo;
public Action? OnSettings;
public Action? OnMultiplayer;
public Action? OnPlaylists;

public const float BUTTON_WIDTH = 140f;
public const float WEDGE_WIDTH = 20;
private readonly IBindable<bool> isIdle = new BindableBool();

[CanBeNull]
private OsuLogo logo;
private OsuLogo? logo;

/// <summary>
/// Assign the <see cref="OsuLogo"/> that this ButtonSystem should manage the position of.
/// </summary>
/// <param name="logo">The instance of the logo to be assigned. If null, we are suspending from the screen that uses this ButtonSystem.</param>
public void SetOsuLogo(OsuLogo logo)
public void SetOsuLogo(OsuLogo? logo)
{
this.logo = logo;

Expand All @@ -84,9 +82,10 @@ public void SetOsuLogo(OsuLogo logo)

private readonly List<MainMenuButton> buttonsTopLevel = new List<MainMenuButton>();
private readonly List<MainMenuButton> buttonsPlay = new List<MainMenuButton>();
private readonly List<MainMenuButton> buttonsEdit = new List<MainMenuButton>();

private Sample sampleBackToLogo;
private Sample sampleLogoSwoosh;
private Sample? sampleBackToLogo;
private Sample? sampleLogoSwoosh;

private readonly LogoTrackingContainer logoTrackingContainer;

Expand All @@ -108,39 +107,45 @@ public ButtonSystem()
backButton = new MainMenuButton(ButtonSystemStrings.Back, @"back-to-top", OsuIcon.LeftCircle, new Color4(51, 58, 94, 255), () => State = ButtonSystemState.TopLevel,
-WEDGE_WIDTH)
{
VisibleState = ButtonSystemState.Play,
VisibleStateMin = ButtonSystemState.Play,
VisibleStateMax = ButtonSystemState.Edit,
},
logoTrackingContainer.LogoFacade.With(d => d.Scale = new Vector2(0.74f))
});

buttonArea.Flow.CentreTarget = logoTrackingContainer.LogoFacade;
}

[Resolved(CanBeNull = true)]
private OsuGame game { get; set; }
[Resolved]
private IAPIProvider api { get; set; } = null!;

[Resolved]
private IAPIProvider api { get; set; }
private OsuGame? game { get; set; }

[Resolved(CanBeNull = true)]
private LoginOverlay loginOverlay { get; set; }
[Resolved]
private LoginOverlay? loginOverlay { get; set; }

[BackgroundDependencyLoader(true)]
private void load(AudioManager audio, IdleTracker idleTracker, GameHost host)
[BackgroundDependencyLoader]
private void load(AudioManager audio, IdleTracker? idleTracker, GameHost host)
{
buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Solo, @"button-default-select", FontAwesome.Solid.User, new Color4(102, 68, 204, 255), () => OnSolo?.Invoke(), WEDGE_WIDTH, Key.P));
buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Multi, @"button-default-select", FontAwesome.Solid.Users, new Color4(94, 63, 186, 255), onMultiplayer, 0, Key.M));
buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Playlists, @"button-default-select", OsuIcon.Charts, new Color4(94, 63, 186, 255), onPlaylists, 0, Key.L));
buttonsPlay.ForEach(b => b.VisibleState = ButtonSystemState.Play);

buttonsEdit.Add(new MainMenuButton(EditorStrings.BeatmapEditor.ToLower(), @"button-default-select", HexaconsIcons.Beatmap, new Color4(238, 170, 0, 255), () => OnEditBeatmap?.Invoke(), WEDGE_WIDTH, Key.B));
buttonsEdit.Add(new MainMenuButton(SkinEditorStrings.SkinEditor.ToLower(), @"button-default-select", HexaconsIcons.Editor, new Color4(220, 160, 0, 255), () => OnEditSkin?.Invoke(), 0, Key.S));
buttonsEdit.ForEach(b => b.VisibleState = ButtonSystemState.Edit);

buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Play, @"button-play-select", OsuIcon.Logo, new Color4(102, 68, 204, 255), () => State = ButtonSystemState.Play, WEDGE_WIDTH, Key.P));
buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Edit, @"button-default-select", OsuIcon.EditCircle, new Color4(238, 170, 0, 255), () => OnEdit?.Invoke(), 0, Key.E));
buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Edit, @"button-default-select", OsuIcon.EditCircle, new Color4(238, 170, 0, 255), () => State = ButtonSystemState.Edit, 0, Key.E));
buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Browse, @"button-default-select", OsuIcon.ChevronDownCircle, new Color4(165, 204, 0, 255), () => OnBeatmapListing?.Invoke(), 0, Key.B, Key.D));

if (host.CanExit)
buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Exit, string.Empty, OsuIcon.CrossCircle, new Color4(238, 51, 153, 255), () => OnExit?.Invoke(), 0, Key.Q));

buttonArea.AddRange(buttonsPlay);
buttonArea.AddRange(buttonsEdit);
buttonArea.AddRange(buttonsTopLevel);

buttonArea.ForEach(b =>
Expand Down Expand Up @@ -270,6 +275,7 @@ private bool goBack()

return true;

case ButtonSystemState.Edit:
case ButtonSystemState.Play:
StopSamplePlayback();
backButton.TriggerClick();
Expand Down Expand Up @@ -328,6 +334,8 @@ public ButtonSystemState State

Logger.Log($"{nameof(ButtonSystem)}'s state changed from {lastState} to {state}");

buttonArea.FinishTransforms(true);

using (buttonArea.BeginDelayedSequence(lastState == ButtonSystemState.Initial ? 150 : 0))
{
buttonArea.ButtonSystemState = state;
Expand All @@ -340,7 +348,7 @@ public ButtonSystemState State
}
}

private ScheduledDelegate logoDelayedAction;
private ScheduledDelegate? logoDelayedAction;

private void updateLogoState(ButtonSystemState lastState = ButtonSystemState.Initial)
{
Expand Down Expand Up @@ -414,6 +422,7 @@ public enum ButtonSystemState
Initial,
TopLevel,
Play,
Edit,
EnteringMode,
}
}
10 changes: 9 additions & 1 deletion osu.Game/Screens/Menu/MainMenu.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
using osu.Game.IO;
using osu.Game.Online.API;
using osu.Game.Overlays;
using osu.Game.Overlays.SkinEditor;
using osu.Game.Rulesets;
using osu.Game.Screens.Backgrounds;
using osu.Game.Screens.Edit;
Expand Down Expand Up @@ -93,6 +94,9 @@ public partial class MainMenu : OsuScreen, IHandlePresentBeatmap, IKeyBindingHan

private Sample reappearSampleSwoosh;

[Resolved(canBeNull: true)]
private SkinEditorOverlay skinEditor { get; set; }

[BackgroundDependencyLoader(true)]
private void load(BeatmapListingOverlay beatmapListing, SettingsOverlay settings, OsuConfigManager config, SessionStatics statics, AudioManager audio)
{
Expand Down Expand Up @@ -120,11 +124,15 @@ private void load(BeatmapListingOverlay beatmapListing, SettingsOverlay settings
{
Buttons = new ButtonSystem
{
OnEdit = delegate
OnEditBeatmap = () =>
{
Beatmap.SetDefault();
this.Push(new EditorLoader());
},
OnEditSkin = () =>
{
skinEditor?.Show();
},
OnSolo = loadSoloSongSelect,
OnMultiplayer = () => this.Push(new Multiplayer()),
OnPlaylists = () => this.Push(new Playlists()),
Expand Down
32 changes: 20 additions & 12 deletions osu.Game/Screens/Menu/MainMenuButton.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

#nullable disable

using System;
using System.Linq;
using osu.Framework;
Expand Down Expand Up @@ -33,7 +31,7 @@ namespace osu.Game.Screens.Menu
/// </summary>
public partial class MainMenuButton : BeatSyncedContainer, IStateful<ButtonState>
{
public event Action<ButtonState> StateChanged;
public event Action<ButtonState>? StateChanged;

public readonly Key[] TriggerKeys;

Expand All @@ -44,18 +42,28 @@ public partial class MainMenuButton : BeatSyncedContainer, IStateful<ButtonState
private readonly string sampleName;

/// <summary>
/// The menu state for which we are visible for.
/// The menu state for which we are visible for (assuming only one).
/// </summary>
public ButtonSystemState VisibleState = ButtonSystemState.TopLevel;
public ButtonSystemState VisibleState
{
set
{
VisibleStateMin = value;
VisibleStateMax = value;
}
}

public ButtonSystemState VisibleStateMin = ButtonSystemState.TopLevel;
public ButtonSystemState VisibleStateMax = ButtonSystemState.TopLevel;

private readonly Action clickAction;
private Sample sampleClick;
private Sample sampleHover;
private SampleChannel sampleChannel;
private readonly Action? clickAction;
private Sample? sampleClick;
private Sample? sampleHover;
private SampleChannel? sampleChannel;

public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => box.ReceivePositionalInputAt(screenSpacePos);

public MainMenuButton(LocalisableString text, string sampleName, IconUsage symbol, Color4 colour, Action clickAction = null, float extraWidth = 0, params Key[] triggerKeys)
public MainMenuButton(LocalisableString text, string sampleName, IconUsage symbol, Color4 colour, Action? clickAction = null, float extraWidth = 0, params Key[] triggerKeys)
{
this.sampleName = sampleName;
this.clickAction = clickAction;
Expand Down Expand Up @@ -315,9 +323,9 @@ public ButtonSystemState ButtonSystemState
break;

default:
if (value == VisibleState)
if (value <= VisibleStateMax && value >= VisibleStateMin)
State = ButtonState.Expanded;
else if (value < VisibleState)
else if (value < VisibleStateMin)
State = ButtonState.Contracted;
else
State = ButtonState.Exploded;
Expand Down