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
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
65 changes: 37 additions & 28 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,23 +34,23 @@ 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.
Expand Down Expand Up @@ -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 @@

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 @@
{
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 Expand Up @@ -266,8 +274,8 @@
proxiedLogo = null;
}

seq.OnComplete(_ => Buttons.SetOsuLogo(null));

Check failure on line 277 in osu.Game/Screens/Menu/MainMenu.cs

View workflow job for this annotation

GitHub Actions / Code Quality

Possible 'null' assignment to non-nullable entity in osu.Game\Screens\Menu\MainMenu.cs on line 277
seq.OnAbort(_ => Buttons.SetOsuLogo(null));

Check failure on line 278 in osu.Game/Screens/Menu/MainMenu.cs

View workflow job for this annotation

GitHub Actions / Code Quality

Possible 'null' assignment to non-nullable entity in osu.Game\Screens\Menu\MainMenu.cs on line 278
}

protected override void LogoExiting(OsuLogo logo)
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