1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-28 12:23:21 +08:00

Merge pull request #5501 from nyquillerium/back-button-part-2

Fix BackButton handling input before all other UI elements
This commit is contained in:
Dan Balasescu 2019-09-25 23:36:25 +09:00 committed by GitHub
commit 3907d859f5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 279 additions and 54 deletions

View File

@ -0,0 +1,202 @@
// 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.
using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Screens;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Select;
using osuTK;
using osuTK.Graphics;
using osuTK.Input;
using IntroSequence = osu.Game.Configuration.IntroSequence;
namespace osu.Game.Tests.Visual.Menus
{
public class TestSceneScreenNavigation : ManualInputManagerTestScene
{
private const float click_padding = 25;
private GameHost host;
private TestOsuGame osuGame;
private Vector2 backButtonPosition => osuGame.ToScreenSpace(new Vector2(click_padding, osuGame.LayoutRectangle.Bottom - click_padding));
private Vector2 optionsButtonPosition => osuGame.ToScreenSpace(new Vector2(click_padding, click_padding));
[BackgroundDependencyLoader]
private void load(GameHost host)
{
this.host = host;
Child = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black,
};
}
[SetUpSteps]
public void SetUpSteps()
{
AddStep("Create new game instance", () =>
{
if (osuGame != null)
{
Remove(osuGame);
osuGame.Dispose();
}
osuGame = new TestOsuGame(LocalStorage, API);
osuGame.SetHost(host);
// todo: this can be removed once we can run audio trakcs without a device present
// see https://github.com/ppy/osu/issues/1302
osuGame.LocalConfig.Set(OsuSetting.IntroSequence, IntroSequence.Circles);
Add(osuGame);
});
AddUntilStep("Wait for load", () => osuGame.IsLoaded);
AddUntilStep("Wait for intro", () => osuGame.ScreenStack.CurrentScreen is IntroScreen);
confirmAtMainMenu();
}
[Test]
public void TestExitSongSelectWithEscape()
{
TestSongSelect songSelect = null;
pushAndConfirm(() => songSelect = new TestSongSelect());
AddStep("Show mods overlay", () => songSelect.ModSelectOverlay.Show());
AddAssert("Overlay was shown", () => songSelect.ModSelectOverlay.State.Value == Visibility.Visible);
AddStep("Press escape", () => pressAndRelease(Key.Escape));
AddAssert("Overlay was hidden", () => songSelect.ModSelectOverlay.State.Value == Visibility.Hidden);
exitViaEscapeAndConfirm();
}
[Test]
public void TestExitSongSelectWithClick()
{
TestSongSelect songSelect = null;
pushAndConfirm(() => songSelect = new TestSongSelect());
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", () => InputManager.HoveredDrawables.Any(d => d.Parent == osuGame.BackButton));
AddStep("Click back button", () => InputManager.Click(MouseButton.Left));
AddUntilStep("Overlay was hidden", () => songSelect.ModSelectOverlay.State.Value == Visibility.Hidden);
exitViaBackButtonAndConfirm();
}
[Test]
public void TestExitMultiWithEscape()
{
pushAndConfirm(() => new Screens.Multi.Multiplayer());
exitViaEscapeAndConfirm();
}
[Test]
public void TestExitMultiWithBackButton()
{
pushAndConfirm(() => new Screens.Multi.Multiplayer());
exitViaBackButtonAndConfirm();
}
[Test]
public void TestOpenOptionsAndExitWithEscape()
{
AddUntilStep("Wait for options to load", () => osuGame.Settings.IsLoaded);
AddStep("Enter menu", () => pressAndRelease(Key.Enter));
AddStep("Move mouse to options overlay", () => InputManager.MoveMouseTo(optionsButtonPosition));
AddStep("Click options overlay", () => InputManager.Click(MouseButton.Left));
AddAssert("Options overlay was opened", () => osuGame.Settings.State.Value == Visibility.Visible);
AddStep("Hide options overlay using escape", () => pressAndRelease(Key.Escape));
AddAssert("Options overlay was closed", () => osuGame.Settings.State.Value == Visibility.Hidden);
}
private void pushAndConfirm(Func<Screen> newScreen)
{
Screen screen = null;
AddStep("Push new screen", () => osuGame.ScreenStack.Push(screen = newScreen()));
AddUntilStep("Wait for new screen", () => osuGame.ScreenStack.CurrentScreen == screen && screen.IsLoaded);
}
private void exitViaEscapeAndConfirm()
{
AddStep("Press escape", () => pressAndRelease(Key.Escape));
confirmAtMainMenu();
}
private void exitViaBackButtonAndConfirm()
{
AddStep("Move mouse to backButton", () => InputManager.MoveMouseTo(backButtonPosition));
AddStep("Click back button", () => InputManager.Click(MouseButton.Left));
confirmAtMainMenu();
}
private void confirmAtMainMenu() => AddUntilStep("Wait for main menu", () => osuGame.ScreenStack.CurrentScreen is MainMenu menu && menu.IsLoaded);
private void pressAndRelease(Key key)
{
InputManager.PressKey(key);
InputManager.ReleaseKey(key);
}
private class TestOsuGame : OsuGame
{
public new ScreenStack ScreenStack => base.ScreenStack;
public new BackButton BackButton => base.BackButton;
public new SettingsPanel Settings => base.Settings;
public new OsuConfigManager LocalConfig => base.LocalConfig;
protected override Loader CreateLoader() => new TestLoader();
public TestOsuGame(Storage storage, IAPIProvider api)
{
Storage = storage;
API = api;
}
protected override void LoadComplete()
{
base.LoadComplete();
API.Login("Rhythm Champion", "osu!");
}
}
private class TestSongSelect : PlaySongSelect
{
public ModSelectOverlay ModSelectOverlay => ModSelect;
}
private class TestLoader : Loader
{
protected override ShaderPrecompiler CreateShaderPrecompiler() => new TestShaderPrecompiler();
private class TestShaderPrecompiler : ShaderPrecompiler
{
protected override bool AllLoaded => true;
}
}
}
}

View File

@ -22,6 +22,7 @@ namespace osu.Game.Tests.Visual.UserInterface
public TestSceneBackButton()
{
BackButton button;
BackButton.Receptor receptor = new BackButton.Receptor();
Child = new Container
{
@ -31,12 +32,13 @@ namespace osu.Game.Tests.Visual.UserInterface
Masking = true,
Children = new Drawable[]
{
receptor,
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.SlateGray
},
button = new BackButton
button = new BackButton(receptor)
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,

View File

@ -10,14 +10,16 @@ using osu.Game.Input.Bindings;
namespace osu.Game.Graphics.UserInterface
{
public class BackButton : VisibilityContainer, IKeyBindingHandler<GlobalAction>
public class BackButton : VisibilityContainer
{
public Action Action;
private readonly TwoLayerButton button;
public BackButton()
public BackButton(Receptor receptor)
{
receptor.OnBackPressed = () => Action?.Invoke();
Size = TwoLayerButton.SIZE_EXTENDED;
Child = button = new TwoLayerButton
@ -37,19 +39,6 @@ namespace osu.Game.Graphics.UserInterface
button.HoverColour = colours.PinkDark;
}
public bool OnPressed(GlobalAction action)
{
if (action == GlobalAction.Back)
{
Action?.Invoke();
return true;
}
return false;
}
public bool OnReleased(GlobalAction action) => action == GlobalAction.Back;
protected override void PopIn()
{
button.MoveToX(0, 400, Easing.OutQuint);
@ -61,5 +50,24 @@ namespace osu.Game.Graphics.UserInterface
button.MoveToX(-TwoLayerButton.SIZE_EXTENDED.X / 2, 400, Easing.OutQuint);
button.FadeOut(400, Easing.OutQuint);
}
public class Receptor : Drawable, IKeyBindingHandler<GlobalAction>
{
public Action OnBackPressed;
public bool OnPressed(GlobalAction action)
{
switch (action)
{
case GlobalAction.Back:
OnBackPressed?.Invoke();
return true;
}
return false;
}
public bool OnReleased(GlobalAction action) => action == GlobalAction.Back;
}
}
}

View File

@ -64,7 +64,10 @@ namespace osu.Game.Online.API
public void Perform(IAPIProvider api)
{
if (!(api is APIAccess apiAccess))
throw new NotSupportedException($"A {nameof(APIAccess)} is required to perform requests.");
{
Fail(new NotSupportedException($"A {nameof(APIAccess)} is required to perform requests."));
return;
}
API = apiAccess;

View File

@ -81,10 +81,14 @@ namespace osu.Game
public readonly Bindable<OverlayActivation> OverlayActivationMode = new Bindable<OverlayActivation>();
private OsuScreenStack screenStack;
protected OsuScreenStack ScreenStack;
protected BackButton BackButton;
protected SettingsPanel Settings;
private VolumeOverlay volume;
private OsuLogo osuLogo;
private BackButton backButton;
private MainMenu menuScreen;
@ -96,8 +100,6 @@ namespace osu.Game
private readonly string[] args;
private SettingsPanel settings;
private readonly List<OverlayContainer> overlays = new List<OverlayContainer>();
private readonly List<OverlayContainer> toolbarElements = new List<OverlayContainer>();
@ -318,6 +320,8 @@ namespace osu.Game
}, $"watch {databasedScoreInfo}", bypassScreenAllowChecks: true);
}
protected virtual Loader CreateLoader() => new Loader();
#region Beatmap progression
private void beatmapChanged(ValueChangedEvent<WorkingBeatmap> beatmap)
@ -356,7 +360,7 @@ namespace osu.Game
performFromMainMenuTask?.Cancel();
// if the current screen does not allow screen changing, give the user an option to try again later.
if (!bypassScreenAllowChecks && (screenStack.CurrentScreen as IOsuScreen)?.AllowExternalScreenChange == false)
if (!bypassScreenAllowChecks && (ScreenStack.CurrentScreen as IOsuScreen)?.AllowExternalScreenChange == false)
{
notifications.Post(new SimpleNotification
{
@ -374,7 +378,7 @@ namespace osu.Game
CloseAllOverlays(false);
// we may already be at the target screen type.
if (targetScreen != null && screenStack.CurrentScreen?.GetType() == targetScreen)
if (targetScreen != null && ScreenStack.CurrentScreen?.GetType() == targetScreen)
{
action();
return;
@ -421,6 +425,7 @@ namespace osu.Game
ScoreManager.PresentImport = items => PresentScore(items.First());
Container logoContainer;
BackButton.Receptor receptor;
dependencies.CacheAs(idleTracker = new GameIdleTracker(6000));
@ -437,15 +442,16 @@ namespace osu.Game
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
screenStack = new OsuScreenStack { RelativeSizeAxes = Axes.Both },
backButton = new BackButton
receptor = new BackButton.Receptor(),
ScreenStack = new OsuScreenStack { RelativeSizeAxes = Axes.Both },
BackButton = new BackButton(receptor)
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Action = () =>
{
if ((screenStack.CurrentScreen as IOsuScreen)?.AllowBackButton == true)
screenStack.Exit();
if ((ScreenStack.CurrentScreen as IOsuScreen)?.AllowBackButton == true)
ScreenStack.Exit();
}
},
logoContainer = new Container { RelativeSizeAxes = Axes.Both },
@ -458,18 +464,15 @@ namespace osu.Game
idleTracker
});
screenStack.ScreenPushed += screenPushed;
screenStack.ScreenExited += screenExited;
ScreenStack.ScreenPushed += screenPushed;
ScreenStack.ScreenExited += screenExited;
loadComponentSingleFile(osuLogo, logo =>
{
logoContainer.Add(logo);
// Loader has to be created after the logo has finished loading as Loader performs logo transformations on entering.
screenStack.Push(new Loader
{
RelativeSizeAxes = Axes.Both
});
ScreenStack.Push(CreateLoader().With(l => l.RelativeSizeAxes = Axes.Both));
});
loadComponentSingleFile(Toolbar = new Toolbar
@ -504,7 +507,7 @@ namespace osu.Game
loadComponentSingleFile(social = new SocialOverlay(), overlayContent.Add, true);
loadComponentSingleFile(channelManager = new ChannelManager(), AddInternal, true);
loadComponentSingleFile(chatOverlay = new ChatOverlay(), overlayContent.Add, true);
loadComponentSingleFile(settings = new SettingsOverlay { GetToolbarHeight = () => ToolbarOffset }, leftFloatingOverlayContent.Add, true);
loadComponentSingleFile(Settings = new SettingsOverlay { GetToolbarHeight = () => ToolbarOffset }, leftFloatingOverlayContent.Add, true);
var changelogOverlay = loadComponentSingleFile(new ChangelogOverlay(), overlayContent.Add, true);
loadComponentSingleFile(userProfile = new UserProfileOverlay(), overlayContent.Add, true);
loadComponentSingleFile(beatmapSetOverlay = new BeatmapSetOverlay(), overlayContent.Add, true);
@ -535,7 +538,7 @@ namespace osu.Game
Add(externalLinkOpener = new ExternalLinkOpener());
var singleDisplaySideOverlays = new OverlayContainer[] { settings, notifications };
var singleDisplaySideOverlays = new OverlayContainer[] { Settings, notifications };
overlays.AddRange(singleDisplaySideOverlays);
foreach (var overlay in singleDisplaySideOverlays)
@ -588,7 +591,7 @@ namespace osu.Game
{
float offset = 0;
if (settings.State.Value == Visibility.Visible)
if (Settings.State.Value == Visibility.Visible)
offset += ToolbarButton.WIDTH / 2;
if (notifications.State.Value == Visibility.Visible)
offset -= ToolbarButton.WIDTH / 2;
@ -596,7 +599,7 @@ namespace osu.Game
screenContainer.MoveToX(offset, SettingsPanel.TRANSITION_LENGTH, Easing.OutQuint);
}
settings.State.ValueChanged += _ => updateScreenOffset();
Settings.State.ValueChanged += _ => updateScreenOffset();
notifications.State.ValueChanged += _ => updateScreenOffset();
}
@ -741,7 +744,7 @@ namespace osu.Game
return true;
case GlobalAction.ToggleSettings:
settings.ToggleVisibility();
Settings.ToggleVisibility();
return true;
case GlobalAction.ToggleDirect:
@ -788,13 +791,13 @@ namespace osu.Game
protected override bool OnExiting()
{
if (screenStack.CurrentScreen is Loader)
if (ScreenStack.CurrentScreen is Loader)
return false;
if (introScreen == null)
return true;
if (!introScreen.DidLoadMenu || !(screenStack.CurrentScreen is IntroScreen))
if (!introScreen.DidLoadMenu || !(ScreenStack.CurrentScreen is IntroScreen))
{
Scheduler.Add(introScreen.MakeCurrent);
return true;
@ -822,7 +825,7 @@ namespace osu.Game
screenContainer.Padding = new MarginPadding { Top = ToolbarOffset };
overlayContent.Padding = new MarginPadding { Top = ToolbarOffset };
MenuCursorContainer.CanShowCursor = (screenStack.CurrentScreen as IOsuScreen)?.CursorVisible ?? false;
MenuCursorContainer.CanShowCursor = (ScreenStack.CurrentScreen as IOsuScreen)?.CursorVisible ?? false;
}
protected virtual void ScreenChanged(IScreen current, IScreen newScreen)
@ -848,9 +851,9 @@ namespace osu.Game
Toolbar.Show();
if (newOsuScreen.AllowBackButton)
backButton.Show();
BackButton.Show();
else
backButton.Hide();
BackButton.Hide();
}
}

View File

@ -65,7 +65,7 @@ namespace osu.Game
protected RulesetConfigCache RulesetConfigCache;
protected APIAccess API;
protected IAPIProvider API;
protected MenuCursorContainer MenuCursorContainer;
@ -73,6 +73,8 @@ namespace osu.Game
protected override Container<Drawable> Content => content;
protected Storage Storage { get; set; }
private Bindable<WorkingBeatmap> beatmap; // cached via load() method
[Cached]
@ -123,7 +125,7 @@ namespace osu.Game
{
Resources.AddStore(new DllResourceStore(@"osu.Game.Resources.dll"));
dependencies.Cache(contextFactory = new DatabaseContextFactory(Host.Storage));
dependencies.Cache(contextFactory = new DatabaseContextFactory(Storage));
var largeStore = new LargeTextureStore(Host.CreateTextureLoaderStore(new NamespacedResourceStore<byte[]>(Resources, @"Textures")));
largeStore.AddStore(Host.CreateTextureLoaderStore(new OnlineStore()));
@ -158,21 +160,21 @@ namespace osu.Game
runMigrations();
dependencies.Cache(SkinManager = new SkinManager(Host.Storage, contextFactory, Host, Audio, new NamespacedResourceStore<byte[]>(Resources, "Skins/Legacy")));
dependencies.Cache(SkinManager = new SkinManager(Storage, contextFactory, Host, Audio, new NamespacedResourceStore<byte[]>(Resources, "Skins/Legacy")));
dependencies.CacheAs<ISkinSource>(SkinManager);
API = new APIAccess(LocalConfig);
if (API == null) API = new APIAccess(LocalConfig);
dependencies.CacheAs<IAPIProvider>(API);
dependencies.CacheAs(API);
var defaultBeatmap = new DummyWorkingBeatmap(Audio, Textures);
dependencies.Cache(RulesetStore = new RulesetStore(contextFactory));
dependencies.Cache(FileStore = new FileStore(contextFactory, Host.Storage));
dependencies.Cache(FileStore = new FileStore(contextFactory, Storage));
// ordering is important here to ensure foreign keys rules are not broken in ModelStore.Cleanup()
dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Host.Storage, API, contextFactory, Host));
dependencies.Cache(BeatmapManager = new BeatmapManager(Host.Storage, contextFactory, RulesetStore, API, Audio, Host, defaultBeatmap));
dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, API, contextFactory, Host));
dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, contextFactory, RulesetStore, API, Audio, Host, defaultBeatmap));
// this should likely be moved to ArchiveModelManager when another case appers where it is necessary
// to have inter-dependent model managers. this could be obtained with an IHasForeign<T> interface to
@ -206,7 +208,8 @@ namespace osu.Game
FileStore.Cleanup();
AddInternal(API);
if (API is APIAccess apiAcces)
AddInternal(apiAcces);
AddInternal(RulesetConfigCache);
GlobalActionContainer globalBinding;
@ -266,9 +269,13 @@ namespace osu.Game
public override void SetHost(GameHost host)
{
if (LocalConfig == null)
LocalConfig = new OsuConfigManager(host.Storage);
base.SetHost(host);
if (Storage == null)
Storage = host.Storage;
if (LocalConfig == null)
LocalConfig = new OsuConfigManager(Storage);
}
private readonly List<ICanAcceptFiles> fileImporters = new List<ICanAcceptFiles>();