diff --git a/osu-framework b/osu-framework index fac688633b..eb076a3301 160000 --- a/osu-framework +++ b/osu-framework @@ -1 +1 @@ -Subproject commit fac688633b8fcf34ae5d0514c26b03e217161eb4 +Subproject commit eb076a3301231eb73917073499051e49a9b12978 diff --git a/osu.Game.Tests/Visual/TestCaseQuitButton.cs b/osu.Game.Tests/Visual/TestCaseQuitButton.cs new file mode 100644 index 0000000000..f0f8d41074 --- /dev/null +++ b/osu.Game.Tests/Visual/TestCaseQuitButton.cs @@ -0,0 +1,57 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Game.Screens.Play.HUD; +using OpenTK; +using OpenTK.Input; + +namespace osu.Game.Tests.Visual +{ + [Description("'Hold to Quit' UI element")] + public class TestCaseQuitButton : ManualInputManagerTestCase + { + private bool exitAction; + + [BackgroundDependencyLoader] + private void load() + { + QuitButton quitButton; + + Add(quitButton = new QuitButton + { + Origin = Anchor.BottomRight, + Anchor = Anchor.BottomRight, + Action = () => exitAction = true + }); + + var text = quitButton.Children.OfType().First(); + + // initial display + AddUntilStep(() => text.IsPresent && !exitAction, "Text visible"); + AddUntilStep(() => !text.IsPresent && !exitAction, "Text is not visible"); + + AddStep("Trigger text fade in", () => InputManager.MoveMouseTo(quitButton)); + AddUntilStep(() => text.IsPresent && !exitAction, "Text visible"); + AddStep("Trigger text fade out", () => InputManager.MoveMouseTo(Vector2.One)); + AddUntilStep(() => !text.IsPresent && !exitAction, "Text is not visible"); + + AddStep("Trigger exit action", () => + { + exitAction = false; + InputManager.MoveMouseTo(quitButton); + InputManager.ButtonDown(MouseButton.Left); + }); + + AddStep("Early release", () => InputManager.ButtonUp(MouseButton.Left)); + AddAssert("action not triggered", () => !exitAction); + + AddStep("Trigger exit action", () => InputManager.ButtonDown(MouseButton.Left)); + AddUntilStep(() => exitAction, $"{nameof(quitButton.Action)} was triggered"); + } + } +} diff --git a/osu.Game/Graphics/Containers/HoldToConfirmContainer.cs b/osu.Game/Graphics/Containers/HoldToConfirmContainer.cs new file mode 100644 index 0000000000..adfc258f61 --- /dev/null +++ b/osu.Game/Graphics/Containers/HoldToConfirmContainer.cs @@ -0,0 +1,52 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using osu.Framework.Configuration; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; + +namespace osu.Game.Graphics.Containers +{ + public abstract class HoldToConfirmContainer : Container + { + public Action Action; + + private const int activate_delay = 400; + private const int fadeout_delay = 200; + + private bool fired; + private bool confirming; + + /// + /// Whether the overlay should be allowed to return from a fired state. + /// + protected virtual bool AllowMultipleFires => false; + + public Bindable Progress = new BindableDouble(); + + protected void BeginConfirm() + { + if (confirming || !AllowMultipleFires && fired) return; + + confirming = true; + + this.TransformBindableTo(Progress, 1, activate_delay * (1 - Progress.Value), Easing.Out).OnComplete(_ => Confirm()); + } + + protected virtual void Confirm() + { + Action?.Invoke(); + fired = true; + } + + protected void AbortConfirm() + { + if (!AllowMultipleFires && fired) return; + + confirming = false; + + this.TransformBindableTo(Progress, 0, fadeout_delay, Easing.Out); + } + } +} diff --git a/osu.Game/Overlays/HoldToConfirmOverlay.cs b/osu.Game/Overlays/HoldToConfirmOverlay.cs index a0e4bf1a39..7e2f6f5891 100644 --- a/osu.Game/Overlays/HoldToConfirmOverlay.cs +++ b/osu.Game/Overlays/HoldToConfirmOverlay.cs @@ -1,11 +1,10 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using System; using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics.Containers; using OpenTK.Graphics; namespace osu.Game.Overlays @@ -14,22 +13,10 @@ namespace osu.Game.Overlays /// An overlay which will display a black screen that dims over a period before confirming an exit action. /// Action is BYO (derived class will need to call and from a user event). /// - public abstract class HoldToConfirmOverlay : Container + public abstract class HoldToConfirmOverlay : HoldToConfirmContainer { - public Action Action; - private Box overlay; - private const int activate_delay = 400; - private const int fadeout_delay = 200; - - private bool fired; - - /// - /// Whether the overlay should be allowed to return from a fired state. - /// - protected virtual bool AllowMultipleFires => false; - [BackgroundDependencyLoader] private void load() { @@ -45,22 +32,8 @@ namespace osu.Game.Overlays RelativeSizeAxes = Axes.Both, } }; - } - protected void BeginConfirm() - { - if (!AllowMultipleFires && fired) return; - overlay.FadeIn(activate_delay * (1 - overlay.Alpha), Easing.Out).OnComplete(_ => - { - Action?.Invoke(); - fired = true; - }); - } - - protected void AbortConfirm() - { - if (!AllowMultipleFires && fired) return; - overlay.FadeOut(fadeout_delay, Easing.Out); + Progress.ValueChanged += v => overlay.Alpha = (float)v; } } } diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index d1d388ae1f..d7ec2032e2 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -154,6 +154,8 @@ namespace osu.Game.Screens.Menu case Key.Space: logo?.TriggerOnClick(state); return true; + case Key.Escape: + return goBack(); } return false; @@ -164,17 +166,22 @@ namespace osu.Game.Screens.Menu switch (action) { case GlobalAction.Back: - switch (State) - { - case MenuState.TopLevel: - State = MenuState.Initial; - return true; - case MenuState.Play: - backButton.TriggerOnClick(); - return true; - default: - return false; - } + return goBack(); + default: + return false; + } + } + + private bool goBack() + { + switch (State) + { + case MenuState.TopLevel: + State = MenuState.Initial; + return true; + case MenuState.Play: + backButton.TriggerOnClick(); + return true; default: return false; } diff --git a/osu.Game/Screens/Play/HUD/QuitButton.cs b/osu.Game/Screens/Play/HUD/QuitButton.cs new file mode 100644 index 0000000000..d0aa0dad92 --- /dev/null +++ b/osu.Game/Screens/Play/HUD/QuitButton.cs @@ -0,0 +1,198 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input; +using osu.Framework.MathUtils; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using OpenTK; + +namespace osu.Game.Screens.Play.HUD +{ + public class QuitButton : FillFlowContainer + { + public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => true; + + private readonly Button button; + + public Action Action + { + set => button.Action = value; + } + + private readonly OsuSpriteText text; + + public QuitButton() + { + Direction = FillDirection.Horizontal; + Spacing = new Vector2(20, 0); + Margin = new MarginPadding(10); + Children = new Drawable[] + { + text = new OsuSpriteText + { + Text = "hold for menu", + Font = @"Exo2.0-Bold", + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft + }, + button = new Button + { + HoverGained = () => text.FadeIn(500, Easing.OutQuint), + HoverLost = () => text.FadeOut(500, Easing.OutQuint) + } + }; + AutoSizeAxes = Axes.Both; + } + + protected override void LoadComplete() + { + text.FadeInFromZero(500, Easing.OutQuint).Delay(1500).FadeOut(500, Easing.OutQuint); + base.LoadComplete(); + } + + private float positionalAdjust; + + protected override bool OnMouseMove(InputState state) + { + positionalAdjust = Vector2.Distance(state.Mouse.NativeState.Position, button.ScreenSpaceDrawQuad.Centre) / 200; + return base.OnMouseMove(state); + } + + protected override void Update() + { + base.Update(); + + if (text.Alpha > 0 || button.Progress.Value > 0 || button.IsHovered) + Alpha = 1; + else + Alpha = Interpolation.ValueAt( + MathHelper.Clamp(Clock.ElapsedFrameTime, 0, 1000), + Alpha, MathHelper.Clamp(1 - positionalAdjust, 0.04f, 1), 0, 200, Easing.OutQuint); + } + + private class Button : HoldToConfirmContainer + { + private SpriteIcon icon; + private CircularProgress circularProgress; + private Circle overlayCircle; + + protected override bool AllowMultipleFires => true; + + public Action HoverGained; + public Action HoverLost; + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Size = new Vector2(60); + + Child = new CircularContainer + { + Masking = true, + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colours.Gray1, + Alpha = 0.5f, + }, + circularProgress = new CircularProgress + { + RelativeSizeAxes = Axes.Both, + InnerRadius = 1 + }, + overlayCircle = new Circle + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Colour = colours.Gray1, + Size = new Vector2(0.9f), + }, + icon = new SpriteIcon + { + Shadow = false, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(15), + Icon = FontAwesome.fa_close + }, + } + }; + + bind(); + } + + private void bind() + { + circularProgress.Current.BindTo(Progress); + Progress.ValueChanged += v => icon.Scale = new Vector2(1 + (float)v * 0.2f); + } + + private bool pendingAnimation; + + protected override void Confirm() + { + base.Confirm(); + + // temporarily unbind as to not look weird if releasing during confirm animation (can see the unwind of progress). + Progress.UnbindAll(); + + // avoid starting a new confirm call until we finish animating. + pendingAnimation = true; + + Progress.Value = 0; + + overlayCircle.ScaleTo(0, 100) + .Then().FadeOut().ScaleTo(1).FadeIn(500) + .OnComplete(a => + { + icon.ScaleTo(1, 100); + circularProgress.FadeOut(100).OnComplete(_ => + { + bind(); + + circularProgress.FadeIn(); + pendingAnimation = false; + }); + }); + } + + protected override bool OnHover(InputState state) + { + HoverGained?.Invoke(); + return true; + } + + protected override void OnHoverLost(InputState state) + { + HoverLost?.Invoke(); + base.OnHoverLost(state); + } + + protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) + { + if (!pendingAnimation && state.Mouse.Buttons.Count == 1) + BeginConfirm(); + return true; + } + + protected override bool OnMouseUp(InputState state, MouseUpEventArgs args) + { + if (state.Mouse.Buttons.Count == 0) + AbortConfirm(); + return true; + } + } + } +} diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 36d8bb75c0..f920b20649 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -34,6 +34,7 @@ namespace osu.Game.Screens.Play public readonly HealthDisplay HealthDisplay; public readonly SongProgress Progress; public readonly ModDisplay ModDisplay; + public readonly QuitButton HoldToQuit; public readonly PlayerSettingsOverlay PlayerSettingsOverlay; private Bindable showHud; @@ -51,14 +52,26 @@ namespace osu.Game.Screens.Play Children = new Drawable[] { - KeyCounter = CreateKeyCounter(), ComboCounter = CreateComboCounter(), ScoreCounter = CreateScoreCounter(), AccuracyCounter = CreateAccuracyCounter(), HealthDisplay = CreateHealthDisplay(), Progress = CreateProgress(), ModDisplay = CreateModsContainer(), - PlayerSettingsOverlay = CreatePlayerSettingsOverlay() + PlayerSettingsOverlay = CreatePlayerSettingsOverlay(), + new FillFlowContainer + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Position = -new Vector2(5, TwoLayerButton.SIZE_RETRACTED.Y), + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + KeyCounter = CreateKeyCounter(), + HoldToQuit = CreateQuitButton(), + } + } } }); @@ -187,7 +200,6 @@ namespace osu.Game.Screens.Play Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, Margin = new MarginPadding(10), - Y = -TwoLayerButton.SIZE_RETRACTED.Y, }; protected virtual ScoreCounter CreateScoreCounter() => new ScoreCounter(6) @@ -205,6 +217,12 @@ namespace osu.Game.Screens.Play RelativeSizeAxes = Axes.X, }; + protected virtual QuitButton CreateQuitButton() => new QuitButton + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + }; + protected virtual ModDisplay CreateModsContainer() => new ModDisplay { Anchor = Anchor.TopRight, diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index ec7a99145e..0150d76251 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -183,6 +183,7 @@ namespace osu.Game.Screens.Play ProcessCustomClock = false, Breaks = beatmap.Breaks }, + RulesetContainer.Cursor?.CreateProxy() ?? new Container(), hudOverlay = new HUDOverlay(scoreProcessor, RulesetContainer, working, offsetClock, adjustableClock) { Clock = Clock, // hud overlay doesn't want to use the audio clock directly @@ -190,7 +191,6 @@ namespace osu.Game.Screens.Play Anchor = Anchor.Centre, Origin = Anchor.Centre }, - RulesetContainer.Cursor?.CreateProxy() ?? new Container(), new SkipOverlay(firstObjectTime) { Clock = Clock, // skip button doesn't want to use the audio clock directly @@ -219,6 +219,8 @@ namespace osu.Game.Screens.Play } }; + hudOverlay.HoldToQuit.Action = Exit; + if (ShowStoryboard) initializeStoryboard(false);