diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModCover.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModCover.cs index 3ebfcedfd1..f51a7774a5 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModCover.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModCover.cs @@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Mania.Mods typeof(ManiaModFadeIn) }).ToArray(); - public override bool Ranked => false; + public override bool Ranked => true; public override bool ValidForFreestyleAsRequiredMod => false; diff --git a/osu.Game.Tests/Visual/Settings/TestSceneSettingsItemV2.cs b/osu.Game.Tests/Visual/Settings/TestSceneSettingsItemV2.cs new file mode 100644 index 0000000000..043eadf370 --- /dev/null +++ b/osu.Game.Tests/Visual/Settings/TestSceneSettingsItemV2.cs @@ -0,0 +1,271 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Cursor; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Localisation; +using osu.Game.Overlays; +using osu.Game.Overlays.Settings; +using osu.Game.Tests.Visual.UserInterface; +using osuTK; + +namespace osu.Game.Tests.Visual.Settings +{ + public partial class TestSceneSettingsItemV2 : ThemeComparisonTestScene + { + private readonly Bindable note = new Bindable(); + + [Cached] + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); + + private FormSliderBar sliderBar = null!; + private FormSliderBar classicSliderBar = null!; + + private SearchContainer searchContainer = null!; + + public TestSceneSettingsItemV2() + : base(false) + { + } + + protected override Drawable CreateContent() + { + return new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new BackgroundBox + { + RelativeSizeAxes = Axes.Both, + }, + new OsuContextMenuContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 400, + RelativeSizeAxes = Axes.Y, + Child = new PopoverContainer + { + RelativeSizeAxes = Axes.Both, + Child = new OsuScrollContainer + { + RelativeSizeAxes = Axes.Both, + ScrollbarVisible = false, + Child = searchContainer = new SearchContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Direction = FillDirection.Vertical, + Spacing = new Vector2(7), + Padding = new MarginPadding { Vertical = 10 }, + Children = new[] + { + new SettingsItemV2(new FormTextBox + { + Caption = "Artist", + HintText = "Poot artist here!", + PlaceholderText = "Here is an artist", + Current = { Value = string.Empty, Default = string.Empty } + }), + new SettingsItemV2(new FormTextBox + { + Caption = "Artist", + HintText = "Poot artist here!", + PlaceholderText = "Here is an artist", + Current = { Value = string.Empty, Default = string.Empty, Disabled = true } + }), + new SettingsItemV2(new FormNumberBox(allowDecimals: true) + { + Caption = "Number", + HintText = "Insert your favourite number", + PlaceholderText = "Mine is 42!", + Current = { Value = string.Empty, Default = string.Empty } + }), + new SettingsItemV2(new FormCheckBox + { + Caption = EditorSetupStrings.LetterboxDuringBreaks, + HintText = EditorSetupStrings.LetterboxDuringBreaksDescription, + }) + { + Note = { BindTarget = note }, + }, + new SettingsItemV2(new FormCheckBox + { + Caption = EditorSetupStrings.LetterboxDuringBreaks, + HintText = EditorSetupStrings.LetterboxDuringBreaksDescription, + Current = { Disabled = true }, + }), + new SettingsItemV2(new FormCheckBox + { + Caption = EditorSetupStrings.LetterboxDuringBreaks, + HintText = EditorSetupStrings.LetterboxDuringBreaksDescription, + Current = { Value = true, Disabled = true }, + }), + new SettingsItemV2(new FormEnumDropdown + { + Caption = EditorSetupStrings.EnableCountdown, + HintText = EditorSetupStrings.CountdownDescription, + }), + new SettingsItemV2(new FormEnumDropdown + { + Caption = EditorSetupStrings.EnableCountdown, + HintText = EditorSetupStrings.CountdownDescription, + Current = { Disabled = true }, + }), + new SettingsItemV2(new FormEnumDropdown + { + Caption = "Dropdown with many items", + HintText = EditorSetupStrings.CountdownDescription, + }) + { + Note = { BindTarget = note }, + }, + new SettingsItemV2(sliderBar = new FormSliderBar + { + Caption = "Slider", + Current = new BindableFloat + { + MinValue = 0, + MaxValue = 10, + Value = 5, + Precision = 0.1f, + }, + }), + new SettingsItemV2(new FormSliderBar + { + Caption = "Slider", + Current = new BindableFloat + { + MinValue = 0, + MaxValue = 10, + Value = 5, + Precision = 0.1f, + Disabled = true, + }, + TransferValueOnCommit = true, + }), + new SettingsItemV2(new FormSliderBar + { + Caption = "Slider without revert button", + Current = new BindableFloat + { + MinValue = 0, + MaxValue = 10, + Value = 5, + Precision = 0.1f, + }, + }) + { + ShowRevertToDefaultButton = false + }, + new SettingsItemV2(classicSliderBar = new FormSliderBar + { + Caption = "Slider with classic default", + Current = new BindableFloat + { + MinValue = 0, + MaxValue = 10, + Value = 5, + Precision = 0.1f, + }, + }) + { + ApplyClassicDefault = () => classicSliderBar.Current.Value = 2, + }, + }, + }, + }, + } + }, + }, + }; + } + + [Test] + public void TestDisplay() + { + AddStep("display", () => CreateThemedContent(OverlayColourScheme.Purple)); + } + + [Test] + public void TestNote() + { + AddStep("set informational note", () => note.Value = new SettingsNote.Data(LayoutSettingsStrings.OsuIsRunningExclusiveFullscreen.ToString(), SettingsNote.Type.Informational)); + AddStep("set warning note", + () => note.Value = new SettingsNote.Data( + "Using unlimited frame limiter can lead to stutters, bad performance and overheating. It will not improve perceived latency. “2x refresh rate” is recommended.", + SettingsNote.Type.Warning)); + AddStep("set critical note", + () => note.Value = new SettingsNote.Data( + "You have done something so horrible in the game settings to the point we have invented a new note type for this. Look at it, it's in red. It's worse than yellow.", + SettingsNote.Type.Critical)); + AddStep("clear note", () => note.Value = null); + } + + [Test] + public void TestClassicDefault() + { + AddStep("modify irrelevant setting", () => sliderBar.Current.Value = 4); + AddStep("apply classic defaults", () => this.ChildrenOfType().Where(i => i.HasClassicDefault).ForEach(s => s.ApplyClassicDefault())); + AddStep("apply regular defaults", () => this.ChildrenOfType().Where(i => i.HasClassicDefault).ForEach(s => s.ApplyDefault())); + AddStep("set classic filter", () => searchContainer.SearchTerm = SettingsItemV2.CLASSIC_DEFAULT_SEARCH_TERM); + AddStep("apply classic defaults", () => this.ChildrenOfType().Where(i => i.HasClassicDefault).ForEach(s => s.ApplyClassicDefault())); + AddStep("apply regular defaults", () => this.ChildrenOfType().Where(i => i.HasClassicDefault).ForEach(s => s.ApplyDefault())); + AddStep("set no filter", () => searchContainer.SearchTerm = string.Empty); + AddAssert("irrelevant setting left out", () => sliderBar.Current.Value, () => Is.EqualTo(4)); + } + + /// + /// Ensures that the reset to default button uses the correct implementation of IsDefault to determine whether it should be shown or not. + /// Values have been chosen so that after being set, Value != Default (but they are close enough that the difference is negligible compared to Precision). + /// + [TestCase(4.2f)] + [TestCase(9.9f)] + public void TestRestoreDefaultValueButtonPrecision(float initialValue) + { + BindableFloat current = null!; + SettingsRevertToDefaultButton revertToDefaultButton = null!; + + AddStep("set current bindable", () => sliderBar.Current = current = new BindableFloat(initialValue) + { + MinValue = 0, + MaxValue = 10, + Precision = 0.1f, + }); + + AddStep("retrieve restore default button", () => revertToDefaultButton = sliderBar.FindClosestParent().ChildrenOfType().Single()); + + AddAssert("restore button hidden", () => revertToDefaultButton.X == 0); + + AddStep("change value to next closest", () => sliderBar.Current.Value += current.Precision * 0.6f); + AddUntilStep("restore button shown", () => revertToDefaultButton.X > 0); + + AddStep("restore default", () => sliderBar.Current.SetDefault()); + AddUntilStep("restore button hidden", () => revertToDefaultButton.X == 0); + } + + private partial class BackgroundBox : Box + { + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + Colour = colourProvider.Background4; + } + } + } +} diff --git a/osu.Game/Graphics/OsuIcon.cs b/osu.Game/Graphics/OsuIcon.cs index 0cf2acadda..a02c611285 100644 --- a/osu.Game/Graphics/OsuIcon.cs +++ b/osu.Game/Graphics/OsuIcon.cs @@ -28,6 +28,7 @@ namespace osu.Game.Graphics public static IconUsage EditCircle => get(OsuIconMapping.EditCircle); public static IconUsage LeftCircle => get(OsuIconMapping.LeftCircle); public static IconUsage RightCircle => get(OsuIconMapping.RightCircle); + public static IconUsage Undo => get(OsuIconMapping.Undo); public static IconUsage Audio => get(OsuIconMapping.Audio); public static IconUsage Beatmap => get(OsuIconMapping.Beatmap); @@ -386,6 +387,9 @@ namespace osu.Game.Graphics [Description(@"twitter")] Twitter, + [Description(@"undo")] + Undo, + [Description(@"user-interface")] UserInterface, diff --git a/osu.Game/Graphics/UserInterfaceV2/FormCheckBox.cs b/osu.Game/Graphics/UserInterfaceV2/FormCheckBox.cs index d4cd86010f..586f6546ab 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormCheckBox.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormCheckBox.cs @@ -1,10 +1,13 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.Collections.Generic; 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.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; @@ -19,7 +22,7 @@ using osu.Game.Overlays; namespace osu.Game.Graphics.UserInterfaceV2 { - public partial class FormCheckBox : CompositeDrawable, IHasCurrentValue + public partial class FormCheckBox : CompositeDrawable, IHasCurrentValue, IFormControl { public Bindable Current { @@ -109,6 +112,8 @@ namespace osu.Game.Graphics.UserInterfaceV2 updateState(); playSamples(); background.FlashColour(ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark2), 800, Easing.OutQuint); + + ValueChanged?.Invoke(); }); current.BindDisabledChanged(_ => updateState(), true); } @@ -157,5 +162,15 @@ namespace osu.Game.Graphics.UserInterfaceV2 BorderColour = colourProvider.Light4; } } + + public IEnumerable FilterTerms => Caption.Yield(); + + public event Action? ValueChanged; + + public bool IsDefault => Current.IsDefault; + + public void SetDefault() => Current.SetDefault(); + + public bool IsDisabled => Current.Disabled; } } diff --git a/osu.Game/Graphics/UserInterfaceV2/FormDropdown.cs b/osu.Game/Graphics/UserInterfaceV2/FormDropdown.cs index d47b9ac73d..a12d166f56 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormDropdown.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormDropdown.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; @@ -17,7 +18,7 @@ using osuTK; namespace osu.Game.Graphics.UserInterfaceV2 { - public partial class FormDropdown : OsuDropdown + public partial class FormDropdown : OsuDropdown, IFormControl { /// /// Caption describing this slider bar, displayed on top of the controls. @@ -29,6 +30,12 @@ namespace osu.Game.Graphics.UserInterfaceV2 /// public LocalisableString HintText { get; init; } + /// + /// The maximum height of the dropdown's menu. + /// By default, this is set to 200px high. Set to to remove such limit. + /// + public float MaxHeight { get; set; } = 200; + private FormDropdownHeader header = null!; [BackgroundDependencyLoader] @@ -40,12 +47,40 @@ namespace osu.Game.Graphics.UserInterfaceV2 header.HintText = HintText; } + protected override void LoadComplete() + { + base.LoadComplete(); + Current.BindValueChanged(_ => ValueChanged?.Invoke()); + } + + public virtual IEnumerable FilterTerms + { + get + { + yield return Caption; + + foreach (var item in MenuItems) + yield return item.Text.Value; + } + } + + public event Action? ValueChanged; + + public bool IsDefault => Current.IsDefault; + + public void SetDefault() => Current.SetDefault(); + + public bool IsDisabled => Current.Disabled; + protected override DropdownHeader CreateHeader() => header = new FormDropdownHeader { Dropdown = this, }; - protected override DropdownMenu CreateMenu() => new FormDropdownMenu(); + protected override DropdownMenu CreateMenu() => new FormDropdownMenu + { + MaxHeight = MaxHeight, + }; private partial class FormDropdownHeader : DropdownHeader { diff --git a/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs b/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs index aaa66742b3..06f9db7846 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Globalization; using System.Numerics; using osu.Framework.Allocation; @@ -22,7 +23,7 @@ using osu.Game.Overlays; namespace osu.Game.Graphics.UserInterfaceV2 { - public partial class FormSliderBar : CompositeDrawable, IHasCurrentValue + public partial class FormSliderBar : CompositeDrawable, IHasCurrentValue, IFormControl where T : struct, INumber, IMinMaxValue { public Bindable Current @@ -194,7 +195,12 @@ namespace osu.Game.Graphics.UserInterfaceV2 slider.IsDragging.BindValueChanged(_ => updateState()); slider.Focused.BindValueChanged(_ => updateState()); - current.ValueChanged += e => currentNumberInstantaneous.Value = e.NewValue; + current.ValueChanged += e => + { + currentNumberInstantaneous.Value = e.NewValue; + ValueChanged?.Invoke(); + }; + current.MinValueChanged += v => currentNumberInstantaneous.MinValue = v; current.MaxValueChanged += v => currentNumberInstantaneous.MaxValue = v; current.PrecisionChanged += v => currentNumberInstantaneous.Precision = v; @@ -445,7 +451,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 } else { - leftBox.Colour = HasFocus || IsHovered || IsDragged ? colourProvider.Highlight1.Opacity(0.5f) : colourProvider.Dark2; + leftBox.Colour = HasFocus || IsHovered || IsDragged ? colourProvider.Highlight1.Opacity(0.5f) : colourProvider.Highlight1.Opacity(0.3f); nub.Colour = HasFocus || IsHovered || IsDragged ? colourProvider.Highlight1 : colourProvider.Light4; } } @@ -487,5 +493,15 @@ namespace osu.Game.Graphics.UserInterfaceV2 return true; } } + + public IEnumerable FilterTerms => new[] { Caption, HintText }; + + public event Action? ValueChanged; + + public bool IsDefault => Current.IsDefault; + + public void SetDefault() => Current.SetDefault(); + + public bool IsDisabled => Current.Disabled; } } diff --git a/osu.Game/Graphics/UserInterfaceV2/FormTextBox.cs b/osu.Game/Graphics/UserInterfaceV2/FormTextBox.cs index 973419310c..4981557e37 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormTextBox.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormTextBox.cs @@ -2,9 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; @@ -20,7 +22,7 @@ using osu.Game.Overlays; namespace osu.Game.Graphics.UserInterfaceV2 { - public partial class FormTextBox : CompositeDrawable, IHasCurrentValue + public partial class FormTextBox : CompositeDrawable, IHasCurrentValue, IFormControl { public Bindable Current { @@ -157,6 +159,8 @@ namespace osu.Game.Graphics.UserInterfaceV2 focusManager = GetContainingFocusManager()!; textBox.Focused.BindValueChanged(_ => updateState()); + + current.BindValueChanged(_ => ValueChanged?.Invoke()); current.BindDisabledChanged(_ => updateState(), true); } @@ -247,5 +251,15 @@ namespace osu.Game.Graphics.UserInterfaceV2 OnInputError?.Invoke(); } } + + public event Action? ValueChanged; + + public bool IsDefault => current.IsDefault; + + public void SetDefault() => current.SetDefault(); + + public bool IsDisabled => current.Disabled; + + public IEnumerable FilterTerms => Caption.Yield(); } } diff --git a/osu.Game/Graphics/UserInterfaceV2/IFormControl.cs b/osu.Game/Graphics/UserInterfaceV2/IFormControl.cs new file mode 100644 index 0000000000..97630a649a --- /dev/null +++ b/osu.Game/Graphics/UserInterfaceV2/IFormControl.cs @@ -0,0 +1,35 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; + +namespace osu.Game.Graphics.UserInterfaceV2 +{ + /// + /// Represents an interface for all form controls. + /// + public interface IFormControl : IDrawable, IHasFilterTerms + { + /// + /// Invoked when the value of the control has changed. + /// + event Action ValueChanged; + + /// + /// Whether the value of this control is in a default state. + /// + bool IsDefault { get; } + + /// + /// If enabled, resets the control to its default state. + /// + void SetDefault(); + + /// + /// Whether the control is currently disabled. + /// + bool IsDisabled { get; } + } +} diff --git a/osu.Game/Overlays/Settings/SettingsItemV2.cs b/osu.Game/Overlays/Settings/SettingsItemV2.cs new file mode 100644 index 0000000000..084aa1ad1f --- /dev/null +++ b/osu.Game/Overlays/Settings/SettingsItemV2.cs @@ -0,0 +1,178 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Localisation; +using osu.Game.Graphics.UserInterfaceV2; + +namespace osu.Game.Overlays.Settings +{ + public sealed partial class SettingsItemV2 : CompositeDrawable, ISettingsItem, IConditionalFilterable + { + private readonly IFormControl control; + private readonly SettingsRevertToDefaultButton revertButton; + + private readonly BindableBool controlDefault = new BindableBool(true); + private readonly BindableBool controlEnabled = new BindableBool(true); + + /// + /// Whether a revert button should be displayed when the control is modified away from default state. + /// + public bool ShowRevertToDefaultButton { get; init; } = true; + + /// + /// A note to display underneath the setting. + /// + public readonly Bindable Note = new Bindable(); + + public SettingsItemV2(IFormControl control) + { + this.control = control; + + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + InternalChild = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Left = SettingsPanel.CONTENT_MARGINS, Right = SettingsPanel.CONTENT_MARGINS_RIGHT }, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new[] + { + revertButton = new SettingsRevertToDefaultButton + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Action = ApplyDefault, + }, + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Child = (Drawable)control, + } + } + }, + new SettingsNote + { + RelativeSizeAxes = Axes.X, + Current = { BindTarget = Note }, + }, + }, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + controlDefault.Value = control.IsDefault; + controlEnabled.Value = !control.IsDisabled; + + controlDefault.BindValueChanged(_ => updateDefaultState()); + controlEnabled.BindValueChanged(_ => updateDefaultState(), true); + FinishTransforms(true); + } + + private void updateDefaultState() + { + bool showRevertButton = !controlDefault.Value && controlEnabled.Value && ShowRevertToDefaultButton; + + if (showRevertButton) + revertButton.Show(); + else + revertButton.Hide(); + } + + protected override void Update() + { + base.Update(); + controlDefault.Value = control.IsDefault; + controlEnabled.Value = !control.IsDisabled; + } + + #region ISettingsItem + + public bool HasClassicDefault => ApplyClassicDefault != null; + + /// + /// If set, this setting is considered as having a "classic" default value, + /// and this is the function for overwriting the control with that value. + /// + public Action? ApplyClassicDefault { get; set; } + + void ISettingsItem.ApplyClassicDefault() => ApplyClassicDefault?.Invoke(); + + public void ApplyDefault() + { + if (!control.IsDisabled) + control.SetDefault(); + } + + public event Action SettingChanged + { + add => control.ValueChanged += value; + remove => control.ValueChanged -= value; + } + + #endregion + + #region Filtering + + public const string CLASSIC_DEFAULT_SEARCH_TERM = @"has-classic-default"; + + public IEnumerable Keywords { get; init; } = Enumerable.Empty(); + + public IEnumerable FilterTerms + { + get + { + var filterTerms = new List(Keywords.Select(k => (LocalisableString)k)); + filterTerms.AddRange(control.FilterTerms); + + if (HasClassicDefault) + filterTerms.Add(CLASSIC_DEFAULT_SEARCH_TERM); + + return filterTerms; + } + } + + private bool matchingFilter = true; + + public bool MatchingFilter + { + get => matchingFilter; + set + { + bool wasPresent = IsPresent; + + matchingFilter = value; + + if (IsPresent != wasPresent) + Invalidate(Invalidation.Presence); + } + } + + public override bool IsPresent => base.IsPresent && MatchingFilter; + + public bool FilteringActive { get; set; } + + public BindableBool CanBeShown { get; } = new BindableBool(true); + + IBindable IConditionalFilterable.CanBeShown => CanBeShown; + + #endregion + } +} diff --git a/osu.Game/Overlays/Settings/SettingsNote.cs b/osu.Game/Overlays/Settings/SettingsNote.cs new file mode 100644 index 0000000000..3552e32d4e --- /dev/null +++ b/osu.Game/Overlays/Settings/SettingsNote.cs @@ -0,0 +1,118 @@ +// 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.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Localisation; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osuTK.Graphics; + +namespace osu.Game.Overlays.Settings +{ + public sealed partial class SettingsNote : CompositeDrawable + { + public readonly Bindable Current = new Bindable(); + + private Box background = null!; + private OsuTextFlowContainer text = null!; + + [Resolved] + private OsuColour colours { get; set; } = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load() + { + AutoSizeDuration = 300; + AutoSizeEasing = Easing.OutQuint; + + InternalChild = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Top = 5, Bottom = 5 }, + Child = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + CornerRadius = 5, + CornerExponent = 2.5f, + Masking = true, + Children = new Drawable[] + { + background = new Box + { + Colour = Color4.Black, + RelativeSizeAxes = Axes.Both, + }, + text = new OsuTextFlowContainer(s => s.Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold)) + { + Padding = new MarginPadding(8), + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + }, + } + }, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + Current.BindValueChanged(_ => updateDisplay(), true); + FinishTransforms(true); + } + + private void updateDisplay() + { + // Explicitly use ClearTransforms to clear any existing auto-size transform before modifying size / flag. + ClearTransforms(); + + if (Current.Value == null) + { + AutoSizeAxes = Axes.None; + this.ResizeHeightTo(0, 300, Easing.OutQuint); + this.FadeOut(250, Easing.OutQuint); + return; + } + + AutoSizeAxes = Axes.Y; + this.FadeIn(250, Easing.OutQuint); + + switch (Current.Value.Type) + { + case Type.Informational: + background.Colour = colourProvider.Dark2; + text.Colour = colourProvider.Content2; + break; + + case Type.Warning: + background.Colour = colours.Orange1; + text.Colour = colourProvider.Background5; + break; + + case Type.Critical: + background.Colour = colours.Red1; + text.Colour = colourProvider.Background5; + break; + } + + text.Text = Current.Value.Text; + } + + public record Data(LocalisableString Text, Type Type); + + public enum Type + { + Informational, + Warning, + Critical, + } + } +} diff --git a/osu.Game/Overlays/Settings/SettingsRevertToDefaultButton.cs b/osu.Game/Overlays/Settings/SettingsRevertToDefaultButton.cs new file mode 100644 index 0000000000..fd95277446 --- /dev/null +++ b/osu.Game/Overlays/Settings/SettingsRevertToDefaultButton.cs @@ -0,0 +1,99 @@ +// 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.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Events; +using osu.Framework.Localisation; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Localisation; +using osuTK; + +namespace osu.Game.Overlays.Settings +{ + public partial class SettingsRevertToDefaultButton : OsuClickableContainer + { + public const float WIDTH = 32; + + public float IconSize { get; init; } = 14; + + private Box background = null!; + private SpriteIcon spriteIcon = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + // this is done to ensure a click on this button doesn't trigger focus on a parent element which contains the button. + public override bool AcceptsFocus => true; + + public SettingsRevertToDefaultButton() + { + Size = new Vector2(WIDTH, 50); + } + + [BackgroundDependencyLoader] + private void load() + { + Masking = true; + CornerRadius = 5; + CornerExponent = 2.5f; + + Children = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background3, + }, + spriteIcon = new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = colourProvider.Light1, + Icon = OsuIcon.Undo, + Margin = new MarginPadding { Left = 12, Right = 5 }, + Size = new Vector2(IconSize), + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + Enabled.BindValueChanged(_ => updateDisplay(), true); + } + + public override LocalisableString TooltipText => CommonStrings.RevertToDefault; + + protected override bool OnHover(HoverEvent e) + { + updateDisplay(); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + updateDisplay(); + base.OnHoverLost(e); + } + + public override void Show() + { + this.FadeIn().MoveToX(WIDTH - 10, 200, Easing.OutElasticQuarter); + } + + public override void Hide() + { + this.MoveToX(0, 120, Easing.OutExpo).Then().FadeOut(); + } + + private void updateDisplay() + { + spriteIcon.FadeColour(IsHovered ? colourProvider.Content2 : colourProvider.Light1, 300, Easing.OutQuint); + background.FadeColour(IsHovered ? colourProvider.Background2 : colourProvider.Background3, 300, Easing.OutQuint); + } + } +} diff --git a/osu.Game/Overlays/SettingsPanel.cs b/osu.Game/Overlays/SettingsPanel.cs index 9b268c573f..8fd12e1e13 100644 --- a/osu.Game/Overlays/SettingsPanel.cs +++ b/osu.Game/Overlays/SettingsPanel.cs @@ -31,6 +31,9 @@ namespace osu.Game.Overlays { public const float CONTENT_MARGINS = 20; + // extra margin to give room to the revert-to-default button in settings controls. + public const float CONTENT_MARGINS_RIGHT = 30; + public const float TRANSITION_LENGTH = 600; private const float sidebar_width = SettingsSidebar.EXPANDED_WIDTH; diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index a2dabe7a9f..749afd8d5e 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ - +