diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayMenuOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayMenuOverlay.cs index d69ac665cc..0aafbda951 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayMenuOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayMenuOverlay.cs @@ -87,7 +87,7 @@ namespace osu.Game.Tests.Visual.Gameplay showOverlay(); AddStep("Up arrow", () => InputManager.Key(Key.Up)); - AddAssert("Last button selected", () => pauseOverlay.Buttons.Last().Selected.Value); + AddAssert("Last button selected", () => pauseOverlay.Buttons.Last().Selected); } /// @@ -99,7 +99,7 @@ namespace osu.Game.Tests.Visual.Gameplay showOverlay(); AddStep("Down arrow", () => InputManager.Key(Key.Down)); - AddAssert("First button selected", () => getButton(0).Selected.Value); + AddAssert("First button selected", () => getButton(0).Selected); } /// @@ -111,11 +111,11 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("Show overlay", () => failOverlay.Show()); AddStep("Up arrow", () => InputManager.Key(Key.Up)); - AddAssert("Last button selected", () => failOverlay.Buttons.Last().Selected.Value); + AddAssert("Last button selected", () => failOverlay.Buttons.Last().Selected); AddStep("Up arrow", () => InputManager.Key(Key.Up)); - AddAssert("First button selected", () => failOverlay.Buttons.First().Selected.Value); + AddAssert("First button selected", () => failOverlay.Buttons.First().Selected); AddStep("Up arrow", () => InputManager.Key(Key.Up)); - AddAssert("Last button selected", () => failOverlay.Buttons.Last().Selected.Value); + AddAssert("Last button selected", () => failOverlay.Buttons.Last().Selected); } /// @@ -127,11 +127,11 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("Show overlay", () => failOverlay.Show()); AddStep("Down arrow", () => InputManager.Key(Key.Down)); - AddAssert("First button selected", () => failOverlay.Buttons.First().Selected.Value); + AddAssert("First button selected", () => failOverlay.Buttons.First().Selected); AddStep("Down arrow", () => InputManager.Key(Key.Down)); - AddAssert("Last button selected", () => failOverlay.Buttons.Last().Selected.Value); + AddAssert("Last button selected", () => failOverlay.Buttons.Last().Selected); AddStep("Down arrow", () => InputManager.Key(Key.Down)); - AddAssert("First button selected", () => failOverlay.Buttons.First().Selected.Value); + AddAssert("First button selected", () => failOverlay.Buttons.First().Selected); } /// @@ -145,7 +145,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("Hover first button", () => InputManager.MoveMouseTo(failOverlay.Buttons.First())); AddStep("Hide overlay", () => failOverlay.Hide()); - AddAssert("Overlay state is reset", () => !failOverlay.Buttons.Any(b => b.Selected.Value)); + AddAssert("Overlay state is reset", () => !failOverlay.Buttons.Any(b => b.Selected)); } /// @@ -162,11 +162,11 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("Hide overlay", () => pauseOverlay.Hide()); showOverlay(); - AddAssert("First button not selected", () => !getButton(0).Selected.Value); + AddAssert("First button not selected", () => !getButton(0).Selected); AddStep("Move slightly", () => InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + new Vector2(1))); - AddAssert("First button selected", () => getButton(0).Selected.Value); + AddAssert("First button selected", () => getButton(0).Selected); } /// @@ -179,8 +179,8 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("Down arrow", () => InputManager.Key(Key.Down)); AddStep("Hover second button", () => InputManager.MoveMouseTo(getButton(1))); - AddAssert("First button not selected", () => !getButton(0).Selected.Value); - AddAssert("Second button selected", () => getButton(1).Selected.Value); + AddAssert("First button not selected", () => !getButton(0).Selected); + AddAssert("Second button selected", () => getButton(1).Selected); } /// @@ -196,8 +196,8 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("Hover second button", () => InputManager.MoveMouseTo(getButton(1))); AddStep("Up arrow", () => InputManager.Key(Key.Up)); - AddAssert("Second button not selected", () => !getButton(1).Selected.Value); - AddAssert("First button selected", () => getButton(0).Selected.Value); + AddAssert("Second button not selected", () => !getButton(1).Selected); + AddAssert("First button selected", () => getButton(0).Selected); } /// @@ -211,7 +211,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("Hover second button", () => InputManager.MoveMouseTo(getButton(1))); AddStep("Unhover second button", () => InputManager.MoveMouseTo(Vector2.Zero)); AddStep("Down arrow", () => InputManager.Key(Key.Down)); - AddAssert("First button selected", () => getButton(0).Selected.Value); // Initial state condition + AddAssert("First button selected", () => getButton(0).Selected); // Initial state condition } /// @@ -282,7 +282,7 @@ namespace osu.Game.Tests.Visual.Gameplay showOverlay(); AddAssert("No button selected", - () => pauseOverlay.Buttons.All(button => !button.Selected.Value)); + () => pauseOverlay.Buttons.All(button => !button.Selected)); } private void showOverlay() => AddStep("Show overlay", () => pauseOverlay.Show()); diff --git a/osu.Game/Graphics/Containers/SelectionCycleFillFlowContainer.cs b/osu.Game/Graphics/Containers/SelectionCycleFillFlowContainer.cs new file mode 100644 index 0000000000..0e7b2bcc05 --- /dev/null +++ b/osu.Game/Graphics/Containers/SelectionCycleFillFlowContainer.cs @@ -0,0 +1,55 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics.UserInterface; + +namespace osu.Game.Graphics.Containers +{ + /// + /// A FillFlowContainer that provides functionality to cycle selection between children + /// The selection wraps around when overflowing past the first or last child. + /// + public class SelectionCycleFillFlowContainer : FillFlowContainer where T : Drawable, ISelectable + { + private int selectedIndex = -1; + + private void setSelected(int value) + { + if (selectedIndex == value) + return; + + // Deselect the previously-selected button + if (selectedIndex != -1) + this[selectedIndex].Selected = false; + + selectedIndex = value; + + // Select the newly-selected button + if (selectedIndex != -1) + this[selectedIndex].Selected = true; + } + + public void SelectNext() + { + if (selectedIndex == -1 || selectedIndex == Count - 1) + setSelected(0); + else + setSelected(selectedIndex + 1); + } + + public void SelectPrevious() + { + if (selectedIndex == -1 || selectedIndex == 0) + setSelected(Count - 1); + else + setSelected(selectedIndex - 1); + } + + public void Deselect() => setSelected(-1); + public void Select(T item) => setSelected(IndexOf(item)); + + public T Selected => (selectedIndex >= 0 && selectedIndex < Count) ? this[selectedIndex] : null; + } +} diff --git a/osu.Game/Graphics/UserInterface/DialogButton.cs b/osu.Game/Graphics/UserInterface/DialogButton.cs index 1047aa4255..718a5171c2 100644 --- a/osu.Game/Graphics/UserInterface/DialogButton.cs +++ b/osu.Game/Graphics/UserInterface/DialogButton.cs @@ -2,24 +2,24 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Bindables; -using osuTK; -using osuTK.Graphics; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.Containers; -using osu.Game.Graphics.Backgrounds; -using osu.Game.Graphics.Sprites; -using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Graphics.Effects; -using osu.Game.Graphics.Containers; using osu.Framework.Input.Events; using osu.Framework.Localisation; +using osu.Game.Graphics.Backgrounds; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osuTK; +using osuTK.Graphics; namespace osu.Game.Graphics.UserInterface { - public class DialogButton : OsuClickableContainer + public class DialogButton : OsuClickableContainer, ISelectable { private const float idle_width = 0.8f; private const float hover_width = 0.9f; @@ -27,7 +27,13 @@ namespace osu.Game.Graphics.UserInterface private const float hover_duration = 500; private const float click_duration = 200; - public readonly BindableBool Selected = new BindableBool(); + public readonly BindableBool SelectedBindable = new BindableBool(); + + public bool Selected + { + get => SelectedBindable.Value; + set => SelectedBindable.Value = value; + } private readonly Container backgroundContainer; private readonly Container colourContainer; @@ -153,7 +159,7 @@ namespace osu.Game.Graphics.UserInterface updateGlow(); - Selected.ValueChanged += selectionChanged; + SelectedBindable.ValueChanged += selectionChanged; } private Color4 buttonColour; @@ -221,7 +227,7 @@ namespace osu.Game.Graphics.UserInterface .OnComplete(_ => { clickAnimating = false; - Selected.TriggerChange(); + SelectedBindable.TriggerChange(); }); return base.OnClick(e); @@ -235,7 +241,7 @@ namespace osu.Game.Graphics.UserInterface protected override void OnMouseUp(MouseUpEvent e) { - if (Selected.Value) + if (Selected) colourContainer.ResizeWidthTo(hover_width, click_duration, Easing.In); base.OnMouseUp(e); } @@ -243,7 +249,7 @@ namespace osu.Game.Graphics.UserInterface protected override bool OnHover(HoverEvent e) { base.OnHover(e); - Selected.Value = true; + Selected = true; return true; } @@ -251,7 +257,7 @@ namespace osu.Game.Graphics.UserInterface protected override void OnHoverLost(HoverLostEvent e) { base.OnHoverLost(e); - Selected.Value = false; + Selected = false; } private void selectionChanged(ValueChangedEvent args) diff --git a/osu.Game/Graphics/UserInterface/ISelectable.cs b/osu.Game/Graphics/UserInterface/ISelectable.cs new file mode 100644 index 0000000000..49c3d725c8 --- /dev/null +++ b/osu.Game/Graphics/UserInterface/ISelectable.cs @@ -0,0 +1,10 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Graphics.UserInterface +{ + public interface ISelectable + { + bool Selected { get; set; } + } +} diff --git a/osu.Game/Overlays/Volume/VolumeMeter.cs b/osu.Game/Overlays/Volume/VolumeMeter.cs index 9210001a3a..3e9849a077 100644 --- a/osu.Game/Overlays/Volume/VolumeMeter.cs +++ b/osu.Game/Overlays/Volume/VolumeMeter.cs @@ -19,13 +19,14 @@ using osu.Framework.Threading; using osu.Framework.Utils; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osuTK; using osuTK.Graphics; namespace osu.Game.Overlays.Volume { - public class VolumeMeter : Container, IKeyBindingHandler + public class VolumeMeter : Container, IKeyBindingHandler, ISelectable { private CircularProgress volumeCircle; private CircularProgress volumeCircleGlow; @@ -43,7 +44,13 @@ namespace osu.Game.Overlays.Volume private Sample sample; private double sampleLastPlaybackTime; - public Action RequestFocus; + public BindableBool SelectedBindable = new BindableBool(); + + public bool Selected + { + get => SelectedBindable.Value; + set => SelectedBindable.Value = value; + } public VolumeMeter(string name, float circleSize, Color4 meterColour) { @@ -212,6 +219,8 @@ namespace osu.Game.Overlays.Volume Bindable.BindValueChanged(volume => { this.TransformTo(nameof(DisplayVolume), volume.NewValue, 400, Easing.OutQuint); }, true); bgProgress.Current.Value = 0.75f; + + SelectedBindable.ValueChanged += selectionChanged; } private int? displayVolumeInt; @@ -330,21 +339,9 @@ namespace osu.Game.Overlays.Volume private const float transition_length = 500; - public void Focus() - { - this.ScaleTo(1.04f, transition_length, Easing.OutExpo); - focusGlowContainer.FadeIn(transition_length, Easing.OutExpo); - } - - public void Unfocus() - { - this.ScaleTo(1f, transition_length, Easing.OutExpo); - focusGlowContainer.FadeOut(transition_length, Easing.OutExpo); - } - protected override bool OnHover(HoverEvent e) { - RequestFocus?.Invoke(this); + Selected = true; return false; } @@ -352,6 +349,20 @@ namespace osu.Game.Overlays.Volume { } + private void selectionChanged(ValueChangedEvent selected) + { + if (selected.NewValue) + { + this.ScaleTo(1.04f, transition_length, Easing.OutExpo); + focusGlowContainer.FadeIn(transition_length, Easing.OutExpo); + } + else + { + this.ScaleTo(1f, transition_length, Easing.OutExpo); + focusGlowContainer.FadeOut(transition_length, Easing.OutExpo); + } + } + public bool OnPressed(GlobalAction action) { if (!IsHovered) @@ -360,12 +371,12 @@ namespace osu.Game.Overlays.Volume switch (action) { case GlobalAction.SelectPrevious: - RequestFocus?.Invoke(this); + Selected = true; adjust(1, false); return true; case GlobalAction.SelectNext: - RequestFocus?.Invoke(this); + Selected = true; adjust(-1, false); return true; } diff --git a/osu.Game/Overlays/VolumeOverlay.cs b/osu.Game/Overlays/VolumeOverlay.cs index dca39774f2..0c56e48cd4 100644 --- a/osu.Game/Overlays/VolumeOverlay.cs +++ b/osu.Game/Overlays/VolumeOverlay.cs @@ -12,6 +12,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Framework.Threading; using osu.Game.Graphics; +using osu.Game.Graphics.Containers; using osu.Game.Input.Bindings; using osu.Game.Overlays.Volume; using osuTK; @@ -32,7 +33,7 @@ namespace osu.Game.Overlays public Bindable IsMuted { get; } = new Bindable(); - private FillFlowContainer volumeMeters; + private SelectionCycleFillFlowContainer volumeMeters; [BackgroundDependencyLoader] private void load(AudioManager audio, OsuColour colours) @@ -55,7 +56,7 @@ namespace osu.Game.Overlays Margin = new MarginPadding(10), Current = { BindTarget = IsMuted } }, - volumeMeters = new FillFlowContainer + volumeMeters = new SelectionCycleFillFlowContainer { Direction = FillDirection.Vertical, AutoSizeAxes = Axes.Both, @@ -65,18 +66,9 @@ namespace osu.Game.Overlays Margin = new MarginPadding { Left = offset }, Children = new[] { - volumeMeterEffect = new VolumeMeter("EFFECTS", 125, colours.BlueDarker) - { - RequestFocus = v => focusedMeter.Value = v - }, - volumeMeterMaster = new VolumeMeter("MASTER", 150, colours.PinkDarker) - { - RequestFocus = v => focusedMeter.Value = v - }, - volumeMeterMusic = new VolumeMeter("MUSIC", 125, colours.BlueDarker) - { - RequestFocus = v => focusedMeter.Value = v - }, + volumeMeterEffect = new VolumeMeter("EFFECTS", 125, colours.BlueDarker), + volumeMeterMaster = new VolumeMeter("MASTER", 150, colours.PinkDarker), + volumeMeterMusic = new VolumeMeter("MUSIC", 125, colours.BlueDarker), } } }); @@ -92,23 +84,18 @@ namespace osu.Game.Overlays else audio.RemoveAdjustment(AdjustableProperty.Volume, muteAdjustment); }); - - focusedMeter.BindValueChanged(meter => - { - meter.OldValue?.Unfocus(); - meter.NewValue?.Focus(); - }); } - private readonly Bindable focusedMeter = new Bindable(); - protected override void LoadComplete() { base.LoadComplete(); - volumeMeterMaster.Bindable.ValueChanged += _ => Show(); - volumeMeterEffect.Bindable.ValueChanged += _ => Show(); - volumeMeterMusic.Bindable.ValueChanged += _ => Show(); + foreach (var volumeMeter in volumeMeters) + { + volumeMeter.Bindable.ValueChanged += _ => Show(); + volumeMeter.SelectedBindable.ValueChanged += selected => volumeMeterSelectionChanged(volumeMeter, selected.NewValue); + } + muteButton.Current.ValueChanged += _ => Show(); } @@ -122,28 +109,26 @@ namespace osu.Game.Overlays if (State.Value == Visibility.Hidden) Show(); else - focusedMeter.Value.Decrease(amount, isPrecise); + volumeMeters.Selected?.Decrease(amount, isPrecise); return true; case GlobalAction.IncreaseVolume: if (State.Value == Visibility.Hidden) Show(); else - focusedMeter.Value.Increase(amount, isPrecise); + volumeMeters.Selected?.Increase(amount, isPrecise); return true; case GlobalAction.NextVolumeMeter: - if (State.Value == Visibility.Hidden) - Show(); - else - focusShift(1); + if (State.Value == Visibility.Visible) + volumeMeters.SelectNext(); + Show(); return true; case GlobalAction.PreviousVolumeMeter: - if (State.Value == Visibility.Hidden) - Show(); - else - focusShift(-1); + if (State.Value == Visibility.Visible) + volumeMeters.SelectPrevious(); + Show(); return true; case GlobalAction.ToggleMute: @@ -155,15 +140,12 @@ namespace osu.Game.Overlays return false; } - private void focusShift(int direction = 1) + private void volumeMeterSelectionChanged(VolumeMeter meter, bool isSelected) { - Show(); - - var newIndex = volumeMeters.IndexOf(focusedMeter.Value) + direction; - if (newIndex < 0) - newIndex += volumeMeters.Count; - - focusedMeter.Value = volumeMeters.Children[newIndex % volumeMeters.Count]; + if (!isSelected) + volumeMeters.Deselect(); + else + volumeMeters.Select(meter); } private ScheduledDelegate popOutDelegate; @@ -172,7 +154,7 @@ namespace osu.Game.Overlays { // Focus on the master meter as a default if previously hidden if (State.Value == Visibility.Hidden) - focusedMeter.Value = volumeMeterMaster; + volumeMeters.Select(volumeMeterMaster); if (State.Value == Visibility.Visible) schedulePopOut(); diff --git a/osu.Game/Screens/Play/GameplayMenuOverlay.cs b/osu.Game/Screens/Play/GameplayMenuOverlay.cs index 4a28da0dde..876f33d814 100644 --- a/osu.Game/Screens/Play/GameplayMenuOverlay.cs +++ b/osu.Game/Screens/Play/GameplayMenuOverlay.cs @@ -2,23 +2,24 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; +using System.Linq; +using Humanizer; +using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Graphics.Sprites; -using osuTK; -using osuTK.Graphics; -using osu.Game.Graphics; -using osu.Framework.Allocation; -using osu.Game.Graphics.UserInterface; +using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; -using System.Collections.Generic; -using System.Linq; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; -using Humanizer; -using osu.Framework.Graphics.Effects; +using osuTK; +using osuTK.Graphics; namespace osu.Game.Screens.Play { @@ -41,18 +42,18 @@ namespace osu.Game.Screens.Play /// /// Action that is invoked when is triggered. /// - protected virtual Action BackAction => () => InternalButtons.Children.LastOrDefault()?.Click(); + protected virtual Action BackAction => () => InternalButtons.Selected?.Click(); /// /// Action that is invoked when is triggered. /// - protected virtual Action SelectAction => () => InternalButtons.Children.FirstOrDefault(f => f.Selected.Value)?.Click(); + protected virtual Action SelectAction => () => InternalButtons.Selected?.Click(); public abstract string Header { get; } public abstract string Description { get; } - protected ButtonContainer InternalButtons; + protected SelectionCycleFillFlowContainer InternalButtons; public IReadOnlyList Buttons => InternalButtons; private FillFlowContainer retryCounterContainer; @@ -116,7 +117,7 @@ namespace osu.Game.Screens.Play } } }, - InternalButtons = new ButtonContainer + InternalButtons = new SelectionCycleFillFlowContainer { Origin = Anchor.TopCentre, Anchor = Anchor.TopCentre, @@ -183,7 +184,7 @@ namespace osu.Game.Screens.Play } }; - button.Selected.ValueChanged += selected => buttonSelectionChanged(button, selected.NewValue); + button.SelectedBindable.ValueChanged += selected => buttonSelectionChanged(button, selected.NewValue); InternalButtons.Add(button); } @@ -255,46 +256,6 @@ namespace osu.Game.Screens.Play }; } - protected class ButtonContainer : FillFlowContainer - { - private int selectedIndex = -1; - - private void setSelected(int value) - { - if (selectedIndex == value) - return; - - // Deselect the previously-selected button - if (selectedIndex != -1) - this[selectedIndex].Selected.Value = false; - - selectedIndex = value; - - // Select the newly-selected button - if (selectedIndex != -1) - this[selectedIndex].Selected.Value = true; - } - - public void SelectNext() - { - if (selectedIndex == -1 || selectedIndex == Count - 1) - setSelected(0); - else - setSelected(selectedIndex + 1); - } - - public void SelectPrevious() - { - if (selectedIndex == -1 || selectedIndex == 0) - setSelected(Count - 1); - else - setSelected(selectedIndex - 1); - } - - public void Deselect() => setSelected(-1); - public void Select(DialogButton button) => setSelected(IndexOf(button)); - } - private class Button : DialogButton { // required to ensure keyboard navigation always starts from an extremity (unless the cursor is moved) @@ -302,7 +263,7 @@ namespace osu.Game.Screens.Play protected override bool OnMouseMove(MouseMoveEvent e) { - Selected.Value = true; + Selected = true; return base.OnMouseMove(e); } }