From 0e6fd456371141eb35e4915c28b1340c6b702e2c Mon Sep 17 00:00:00 2001 From: LA <1245661240@qq.com> Date: Sat, 17 Jan 2026 21:21:02 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=8C=E6=AD=A5=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Security/ElevatedPrivilegesChecker.cs | 3 +- .../Objects/Drawables/DrawableNote.cs | 1 - .../Edit/PolygonGenerationPopover.cs | 30 +-- .../Edit/PreciseMovementPopover.cs | 18 +- .../Edit/PreciseRotationPopover.cs | 9 +- .../Edit/PreciseScalePopover.cs | 9 +- .../Objects/Drawables/DrawableHitCircle.cs | 3 - .../UI/DrumSamplePlayer.cs | 27 --- .../UserInterface/TestSceneFormButton.cs | 139 ++++++++++++++ .../UserInterface/TestSceneFormSliderBar.cs | 9 +- .../UserInterface/TestSceneOnScreenDisplay.cs | 5 +- .../TestSceneSliderWithTextBoxInput.cs | 131 ------------- osu.Game/Beatmaps/BeatmapManager.cs | 2 +- osu.Game/Beatmaps/WorkingBeatmapCache.cs | 6 +- osu.Game/Configuration/BackgroundSource.cs | 4 +- osu.Game/Database/LegacyBeatmapExporter.cs | 5 +- osu.Game/Database/LegacyExporter.cs | 5 +- osu.Game/Graphics/ScreenshotManager.cs | 3 +- .../Graphics/UserInterfaceV2/FormButton.cs | 175 ++++++++++++++---- .../Graphics/UserInterfaceV2/FormDropdown.cs | 25 ++- .../Graphics/UserInterfaceV2/FormSliderBar.cs | 38 +++- osu.Game/Localisation/NotificationsStrings.cs | 59 +++++- .../MultiplayerClientExtensions.cs | 17 +- .../Multiplayer/ServerShutdownNotification.cs | 5 +- osu.Game/Online/OnlineStatusNotifier.cs | 62 ++++--- .../Overlays/Music/MusicKeyBindingHandler.cs | 5 +- .../Overlays/OSD/CopiedToClipboardToast.cs | 2 +- osu.Game/Overlays/OSD/SpeedChangeToast.cs | 11 +- osu.Game/Overlays/OSD/Toast.cs | 24 ++- osu.Game/Overlays/OSD/TrackedSettingToast.cs | 6 +- .../Sections/General/QuickActionSettings.cs | 4 +- osu.Game/Overlays/SkinEditor/SkinEditor.cs | 3 +- .../Edit/ComposerDistanceSnapProvider.cs | 4 +- .../Objects/Drawables/DrawableHitObject.cs | 3 - .../Screens/Edit/Components/FormSampleSet.cs | 19 +- osu.Game/Screens/Edit/Editor.cs | 3 +- osu.Game/Screens/Edit/EditorBeatmap.cs | 13 +- osu.Game/Screens/Edit/EditorBeatmapSkin.cs | 59 ++++-- .../Edit/Setup/FormSampleSetChooser.cs | 144 ++++++++++++++ .../Screens/Edit/Setup/ResourcesSection.cs | 24 +++ osu.Game/Screens/Edit/Timing/EffectSection.cs | 15 +- .../IndeterminateSliderWithTextBoxInput.cs | 2 +- .../AddPlaylistToCollectionButton.cs | 9 +- .../Screens/Select/ModSpeedHotkeyHandler.cs | 8 +- .../BeatmapTitleWedge.DifficultyDisplay.cs | 9 +- osu.Game/Skinning/LegacyBeatmapSkin.cs | 2 + osu.Game/Skinning/RealmBackedResourceStore.cs | 8 +- osu.Game/Skinning/Skin.cs | 4 + 48 files changed, 811 insertions(+), 360 deletions(-) create mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneFormButton.cs delete mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneSliderWithTextBoxInput.cs create mode 100644 osu.Game/Screens/Edit/Setup/FormSampleSetChooser.cs diff --git a/osu.Desktop/Security/ElevatedPrivilegesChecker.cs b/osu.Desktop/Security/ElevatedPrivilegesChecker.cs index 4b6ebc9b56..1ac0a8153a 100644 --- a/osu.Desktop/Security/ElevatedPrivilegesChecker.cs +++ b/osu.Desktop/Security/ElevatedPrivilegesChecker.cs @@ -7,6 +7,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; +using osu.Game.Localisation; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; @@ -32,7 +33,7 @@ namespace osu.Desktop.Security { public ElevatedPrivilegesNotification() { - Text = $"Running osu! as {(RuntimeInfo.IsUnix ? "root" : "administrator")} does not improve performance, may break integrations and poses a security risk. Please run the game as a normal user."; + Text = NotificationsStrings.ElevatedPrivileges(RuntimeInfo.IsUnix ? "root" : "Administrator"); } [BackgroundDependencyLoader] diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs index 5f8627e956..f6b92ab405 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs @@ -11,7 +11,6 @@ using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Beatmaps; using osu.Game.Graphics; -using osu.Game.LAsEzExtensions.Audio; using osu.Game.Rulesets.Mania.Configuration; using osu.Game.Rulesets.Mania.Skinning.Default; using osu.Game.Rulesets.Scoring; diff --git a/osu.Game.Rulesets.Osu/Edit/PolygonGenerationPopover.cs b/osu.Game.Rulesets.Osu/Edit/PolygonGenerationPopover.cs index 046f57c0a5..fe5e0581ec 100644 --- a/osu.Game.Rulesets.Osu/Edit/PolygonGenerationPopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PolygonGenerationPopover.cs @@ -25,10 +25,10 @@ namespace osu.Game.Rulesets.Osu.Edit { public partial class PolygonGenerationPopover : OsuPopover { - private SliderWithTextBoxInput distanceSnapInput = null!; - private SliderWithTextBoxInput offsetAngleInput = null!; - private SliderWithTextBoxInput repeatCountInput = null!; - private SliderWithTextBoxInput pointInput = null!; + private FormSliderBar distanceSnapInput { get; set; } = null!; + private FormSliderBar offsetAngleInput { get; set; } = null!; + private FormSliderBar repeatCountInput { get; set; } = null!; + private FormSliderBar pointInput { get; set; } = null!; private RoundedButton commitButton = null!; private readonly List insertedCircles = new List(); @@ -64,11 +64,12 @@ namespace osu.Game.Rulesets.Osu.Edit { Width = 220, AutoSizeAxes = Axes.Y, - Spacing = new Vector2(20), + Spacing = new Vector2(5), Children = new Drawable[] { - distanceSnapInput = new SliderWithTextBoxInput("Distance snap:") + distanceSnapInput = new FormSliderBar { + Caption = "Distance snap", Current = new BindableNumber(1) { MinValue = 0.1, @@ -76,37 +77,40 @@ namespace osu.Game.Rulesets.Osu.Edit Precision = 0.1, Value = ((OsuHitObjectComposer)composer).DistanceSnapProvider.DistanceSpacingMultiplier.Value, }, - Instantaneous = true + TabbableContentContainer = this }, - offsetAngleInput = new SliderWithTextBoxInput("Offset angle:") + offsetAngleInput = new FormSliderBar { + Caption = "Offset angle", Current = new BindableNumber { MinValue = 0, MaxValue = 180, Precision = 1 }, - Instantaneous = true + TabbableContentContainer = this }, - repeatCountInput = new SliderWithTextBoxInput("Repeats:") + repeatCountInput = new FormSliderBar { + Caption = "Repeats", Current = new BindableNumber(1) { MinValue = 1, MaxValue = 10, Precision = 1 }, - Instantaneous = true + TabbableContentContainer = this }, - pointInput = new SliderWithTextBoxInput("Vertices:") + pointInput = new FormSliderBar { + Caption = "Vertices", Current = new BindableNumber(3) { MinValue = 3, MaxValue = 32, Precision = 1, }, - Instantaneous = true + TabbableContentContainer = this }, commitButton = new RoundedButton { diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs index f3739ab445..caac51632b 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs @@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.Edit private BindableNumber xBindable = null!; private BindableNumber yBindable = null!; - private SliderWithTextBoxInput xInput = null!; + private FormSliderBar xInput { get; set; } = null!; private OsuCheckbox relativeCheckbox = null!; public PreciseMovementPopover() @@ -52,31 +52,31 @@ namespace osu.Game.Rulesets.Osu.Edit { Width = 220, AutoSizeAxes = Axes.Y, - Spacing = new Vector2(20), + Spacing = new Vector2(5), Children = new Drawable[] { - xInput = new SliderWithTextBoxInput("X:") + xInput = new FormSliderBar { + Caption = "X", Current = xBindable = new BindableNumber { Precision = 1, }, - Instantaneous = true, - TabbableContentContainer = this, + TabbableContentContainer = this }, - new SliderWithTextBoxInput("Y:") + new FormSliderBar { + Caption = "Y", Current = yBindable = new BindableNumber { Precision = 1, }, - Instantaneous = true, - TabbableContentContainer = this, + TabbableContentContainer = this }, relativeCheckbox = new OsuCheckbox(false) { RelativeSizeAxes = Axes.X, - LabelText = "Relative movement", + LabelText = "Relative movement" } } }; diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs index e2cde1a325..959963ed33 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Osu.Edit private readonly Bindable rotationInfo = new Bindable(new PreciseRotationInfo(0, EditorOrigin.GridCentre)); - private SliderWithTextBoxInput angleInput = null!; + private FormSliderBar angleInput { get; set; } = null!; private EditorRadioButtonCollection rotationOrigin = null!; private RadioButton gridCentreButton = null!; @@ -54,11 +54,12 @@ namespace osu.Game.Rulesets.Osu.Edit { Width = 220, AutoSizeAxes = Axes.Y, - Spacing = new Vector2(20), + Spacing = new Vector2(5), Children = new Drawable[] { - angleInput = new SliderWithTextBoxInput("Angle (degrees):") + angleInput = new FormSliderBar { + Caption = "Angle (degrees)", Current = new BindableNumber { MinValue = -360, @@ -66,7 +67,7 @@ namespace osu.Game.Rulesets.Osu.Edit Precision = 1 }, KeyboardStep = 1f, - Instantaneous = true + TabbableContentContainer = this }, rotationOrigin = new EditorRadioButtonCollection { diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs index ca4a99b9cd..88bf92ecb5 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs @@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Osu.Edit private readonly Bindable scaleInfo = new Bindable(new PreciseScaleInfo(1, EditorOrigin.GridCentre, true, true)); - private SliderWithTextBoxInput scaleInput = null!; + private FormSliderBar scaleInput { get; set; } = null!; private BindableNumber scaleInputBindable = null!; private EditorRadioButtonCollection scaleOrigin = null!; @@ -66,11 +66,12 @@ namespace osu.Game.Rulesets.Osu.Edit { Width = 220, AutoSizeAxes = Axes.Y, - Spacing = new Vector2(20), + Spacing = new Vector2(5), Children = new Drawable[] { - scaleInput = new SliderWithTextBoxInput("Scale:") + scaleInput = new FormSliderBar { + Caption = "Scale", Current = scaleInputBindable = new BindableNumber { MinValue = 0.05f, @@ -80,7 +81,7 @@ namespace osu.Game.Rulesets.Osu.Edit Default = 1, }, KeyboardStep = 0.01f, - Instantaneous = true + TabbableContentContainer = this }, scaleOrigin = new EditorRadioButtonCollection { diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index 9b25c11dd6..101c34b725 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -287,9 +287,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (IsHovered) { - // 记录延迟追踪按键输入 - osu.Game.LAsEzExtensions.Audio.InputAudioLatencyTracker.Instance?.RecordKeyPress(e.Action == OsuAction.LeftButton ? osuTK.Input.Key.Z : osuTK.Input.Key.X); - Hit(); HitAction ??= e.Action; return true; diff --git a/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs b/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs index 642fc06f78..57067ac666 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs @@ -10,8 +10,6 @@ using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects.Drawables; using osu.Game.Rulesets.UI; using osu.Game.Screens.Play; -using osu.Game.LAsEzExtensions.Audio; -using osuTK.Input; namespace osu.Game.Rulesets.Taiko.UI { @@ -96,31 +94,6 @@ namespace osu.Game.Rulesets.Taiko.UI } } - // Record input for latency tracking (map TaikoAction to a representative Key) - try - { - Key mapped = Key.Unknown; - switch (e.Action) - { - case TaikoAction.LeftCentre: - mapped = Key.D; - break; - case TaikoAction.RightCentre: - mapped = Key.J; - break; - case TaikoAction.LeftRim: - mapped = Key.F; - break; - case TaikoAction.RightRim: - mapped = Key.K; - break; - } - - if (mapped != Key.Unknown) - InputAudioLatencyTracker.Instance?.RecordKeyPress(mapped); - } - catch { } - Play(triggerSource, hitType, strong); lastHitTime = Time.Current; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFormButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFormButton.cs new file mode 100644 index 0000000000..a22607e781 --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFormButton.cs @@ -0,0 +1,139 @@ +// 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.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Cursor; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Overlays; +using osuTK; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public partial class TestSceneFormButton : ThemeComparisonTestScene + { + public TestSceneFormButton() + : base(false) + { + } + + protected override Drawable CreateContent() => new OsuContextMenuContainer + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new BackgroundBox + { + RelativeSizeAxes = Axes.Both, + }, + new PopoverContainer + { + RelativeSizeAxes = Axes.Both, + Child = new OsuScrollContainer + { + RelativeSizeAxes = Axes.Both, + Child = new FillFlowContainer + { + AutoSizeAxes = Axes.Y, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Width = 400, + Direction = FillDirection.Vertical, + Spacing = new Vector2(5), + Padding = new MarginPadding(10), + Children = new Drawable[] + { + new FormButton + { + Caption = "Button with default style", + Action = () => { }, + }, + new FormButton + { + Caption = "Button with default style", + Enabled = { Value = false }, + }, + new FormButton + { + Caption = "Button with custom style", + BackgroundColour = new OsuColour().DangerousButtonColour, + ButtonIcon = FontAwesome.Solid.Hamburger, + Action = () => { }, + }, + new FormButton + { + Caption = "Button with custom style", + BackgroundColour = new OsuColour().DangerousButtonColour, + ButtonIcon = FontAwesome.Solid.Hamburger, + Enabled = { Value = false }, + }, + new FormButton + { + Caption = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua", + BackgroundColour = new OsuColour().Blue3, + ButtonIcon = FontAwesome.Solid.Book, + Action = () => { }, + }, + new FormButton + { + Caption = "Button with text inside", + ButtonText = "Text in button", + Action = () => { }, + }, + new FormButton + { + Caption = "Button with text inside", + ButtonText = "Text in button", + Enabled = { Value = false }, + }, + new FormButton + { + Caption = "Button with text inside", + ButtonText = "Text in button", + BackgroundColour = new OsuColour().DangerousButtonColour, + Action = () => { }, + }, + new FormButton + { + Caption = "Button with text inside", + ButtonText = "Text in button", + BackgroundColour = new OsuColour().DangerousButtonColour, + Enabled = { Value = false }, + }, + new FormButton + { + Caption = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor", + ButtonText = "Text in button", + BackgroundColour = new OsuColour().Blue3, + Action = () => { }, + }, + new FormButton + { + Caption = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor", + ButtonText = "Text in button", + BackgroundColour = new OsuColour().Blue3, + Enabled = { Value = false }, + }, + }, + }, + }, + } + } + }; + + private partial class BackgroundBox : Box + { + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + Colour = colourProvider.Background4; + } + } + } +} diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFormSliderBar.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFormSliderBar.cs index a962cd4a6f..c7b9975301 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFormSliderBar.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFormSliderBar.cs @@ -64,9 +64,11 @@ namespace osu.Game.Tests.Visual.UserInterface }); } - [Test] - public void TestNubDoubleClickRevertToDefault() + [TestCase(false)] + [TestCase(true)] + public void TestNubDoubleClickRevertToDefault(bool transferValueOnCommit) { + OsuSpriteText text; FormSliderBar slider = null!; AddStep("create content", () => @@ -81,9 +83,11 @@ namespace osu.Game.Tests.Visual.UserInterface Spacing = new Vector2(10), Children = new Drawable[] { + text = new OsuSpriteText(), slider = new FormSliderBar { Caption = "Slider", + TransferValueOnCommit = transferValueOnCommit, Current = new BindableFloat { MinValue = 0, @@ -94,6 +98,7 @@ namespace osu.Game.Tests.Visual.UserInterface }, } }; + slider.Current.BindValueChanged(_ => text.Text = $"Current value is: {slider.Current.Value}", true); }); AddStep("set slider to 1", () => slider.Current.Value = 1); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOnScreenDisplay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOnScreenDisplay.cs index 4bd3a883f1..34795f3b1f 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOnScreenDisplay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOnScreenDisplay.cs @@ -96,7 +96,7 @@ namespace osu.Game.Tests.Visual.UserInterface private partial class EmptyToast : Toast { public EmptyToast() - : base("", "", "") + : base("", "") { } } @@ -104,8 +104,9 @@ namespace osu.Game.Tests.Visual.UserInterface private partial class LengthyToast : Toast { public LengthyToast() - : base("Toast with a very very very long text", "A very very very very very very long text also", "A very very very very very long shortcut") + : base("Toast with a very very very long text", "A very very very very very very long text also") { + ExtraText = "A very very very very very long shortcut"; } } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneSliderWithTextBoxInput.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneSliderWithTextBoxInput.cs deleted file mode 100644 index 06b9623508..0000000000 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneSliderWithTextBoxInput.cs +++ /dev/null @@ -1,131 +0,0 @@ -// 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.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Input; -using osu.Framework.Testing; -using osu.Game.Graphics.UserInterface; -using osu.Game.Graphics.UserInterfaceV2; -using osuTK.Input; - -namespace osu.Game.Tests.Visual.UserInterface -{ - public partial class TestSceneSliderWithTextBoxInput : OsuManualInputManagerTestScene - { - private SliderWithTextBoxInput sliderWithTextBoxInput = null!; - - private OsuSliderBar slider => sliderWithTextBoxInput.ChildrenOfType>().Single(); - private Nub nub => sliderWithTextBoxInput.ChildrenOfType().Single(); - private OsuTextBox textBox => sliderWithTextBoxInput.ChildrenOfType().Single(); - - [SetUpSteps] - public void SetUpSteps() - { - AddStep("create slider", () => Child = sliderWithTextBoxInput = new SliderWithTextBoxInput("Test Slider") - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Width = 0.5f, - Current = new BindableFloat - { - MinValue = -5, - MaxValue = 5, - Precision = 0.2f - } - }); - } - - [Test] - public void TestNonInstantaneousMode() - { - AddStep("set instantaneous to false", () => sliderWithTextBoxInput.Instantaneous = false); - - AddStep("focus textbox", () => ((IFocusManager)InputManager).ChangeFocus(textBox)); - AddStep("change text", () => textBox.Text = "3"); - AddAssert("slider not moved", () => slider.Current.Value, () => Is.Zero); - AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.Zero); - - AddStep("commit text", () => InputManager.Key(Key.Enter)); - AddAssert("slider moved", () => slider.Current.Value, () => Is.EqualTo(3)); - AddAssert("current changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(3)); - - AddStep("move mouse to nub", () => InputManager.MoveMouseTo(nub)); - AddStep("hold left mouse", () => InputManager.PressButton(MouseButton.Left)); - AddStep("move mouse to minimum", () => InputManager.MoveMouseTo(sliderWithTextBoxInput.ScreenSpaceDrawQuad.BottomLeft)); - AddAssert("textbox not changed", () => textBox.Current.Value, () => Is.EqualTo("3")); - AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(3)); - - AddStep("release left mouse", () => InputManager.ReleaseButton(MouseButton.Left)); - AddAssert("textbox changed", () => textBox.Current.Value, () => Is.EqualTo("-5")); - AddAssert("current changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5)); - - AddStep("focus textbox", () => ((IFocusManager)InputManager).ChangeFocus(textBox)); - AddStep("set text to invalid", () => textBox.Text = "garbage"); - AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5)); - AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5)); - - AddStep("commit text", () => InputManager.Key(Key.Enter)); - AddAssert("text restored", () => textBox.Text, () => Is.EqualTo("-5")); - AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5)); - AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5)); - - AddStep("focus textbox", () => ((IFocusManager)InputManager).ChangeFocus(textBox)); - AddStep("set text to invalid", () => textBox.Text = "garbage"); - AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5)); - AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5)); - - AddStep("lose focus", () => ((IFocusManager)InputManager).ChangeFocus(null)); - AddAssert("text restored", () => textBox.Text, () => Is.EqualTo("-5")); - AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5)); - AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5)); - } - - [Test] - public void TestInstantaneousMode() - { - AddStep("set instantaneous to true", () => sliderWithTextBoxInput.Instantaneous = true); - - AddStep("focus textbox", () => ((IFocusManager)InputManager).ChangeFocus(textBox)); - AddStep("change text", () => textBox.Text = "3"); - AddAssert("slider moved", () => slider.Current.Value, () => Is.EqualTo(3)); - AddAssert("current changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(3)); - - AddStep("commit text", () => InputManager.Key(Key.Enter)); - AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(3)); - AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(3)); - - AddStep("move mouse to nub", () => InputManager.MoveMouseTo(nub)); - AddStep("hold left mouse", () => InputManager.PressButton(MouseButton.Left)); - AddStep("move mouse to minimum", () => InputManager.MoveMouseTo(sliderWithTextBoxInput.ScreenSpaceDrawQuad.BottomLeft)); - AddAssert("textbox changed", () => textBox.Current.Value, () => Is.EqualTo("-5")); - AddAssert("current changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5)); - - AddStep("release left mouse", () => InputManager.ReleaseButton(MouseButton.Left)); - AddAssert("textbox not changed", () => textBox.Current.Value, () => Is.EqualTo("-5")); - AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5)); - - AddStep("focus textbox", () => ((IFocusManager)InputManager).ChangeFocus(textBox)); - AddStep("set text to invalid", () => textBox.Text = "garbage"); - AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5)); - AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5)); - - AddStep("commit text", () => InputManager.Key(Key.Enter)); - AddAssert("text restored", () => textBox.Text, () => Is.EqualTo("-5")); - AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5)); - AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5)); - - AddStep("focus textbox", () => ((IFocusManager)InputManager).ChangeFocus(textBox)); - AddStep("set text to invalid", () => textBox.Text = "garbage"); - AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5)); - AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5)); - - AddStep("lose focus", () => ((IFocusManager)InputManager).ChangeFocus(null)); - AddAssert("text restored", () => textBox.Text, () => Is.EqualTo("-5")); - AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5)); - AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5)); - } - } -} diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 7860dba075..8446f43c53 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -95,7 +95,7 @@ namespace osu.Game.Beatmaps protected virtual WorkingBeatmapCache CreateWorkingBeatmapCache(AudioManager audioManager, IResourceStore resources, IResourceStore storage, WorkingBeatmap? defaultBeatmap, GameHost? host) { - return new WorkingBeatmapCache(BeatmapTrackStore, audioManager, resources, storage, defaultBeatmap, host); + return new WorkingBeatmapCache(BeatmapTrackStore, audioManager, resources, storage, defaultBeatmap, host, Realm); } protected virtual BeatmapImporter CreateBeatmapImporter(Storage storage, RealmAccess realm) => new BeatmapImporter(storage, realm); diff --git a/osu.Game/Beatmaps/WorkingBeatmapCache.cs b/osu.Game/Beatmaps/WorkingBeatmapCache.cs index 9957935977..0bb61798dc 100644 --- a/osu.Game/Beatmaps/WorkingBeatmapCache.cs +++ b/osu.Game/Beatmaps/WorkingBeatmapCache.cs @@ -47,12 +47,13 @@ namespace osu.Game.Beatmaps private readonly LargeTextureStore beatmapPanelTextureStore; private readonly ITrackStore trackStore; private readonly IResourceStore files; + private readonly RealmAccess realm; [CanBeNull] private readonly GameHost host; public WorkingBeatmapCache(ITrackStore trackStore, AudioManager audioManager, IResourceStore resources, IResourceStore files, WorkingBeatmap defaultBeatmap = null, - GameHost host = null) + GameHost host = null, RealmAccess realm = null) { DefaultBeatmap = defaultBeatmap; @@ -63,6 +64,7 @@ namespace osu.Game.Beatmaps largeTextureStore = new LargeTextureStore(host?.Renderer ?? new DummyRenderer(), host?.CreateTextureLoaderStore(files)); beatmapPanelTextureStore = new LargeTextureStore(host?.Renderer ?? new DummyRenderer(), new BeatmapPanelBackgroundTextureLoaderStore(host?.CreateTextureLoaderStore(files))); this.trackStore = trackStore; + this.realm = realm; } public void Invalidate(BeatmapSetInfo info) @@ -118,7 +120,7 @@ namespace osu.Game.Beatmaps ITrackStore IBeatmapResourceProvider.Tracks => trackStore; IRenderer IStorageResourceProvider.Renderer => host?.Renderer ?? new DummyRenderer(); AudioManager IStorageResourceProvider.AudioManager => audioManager; - RealmAccess IStorageResourceProvider.RealmAccess => null!; + RealmAccess IStorageResourceProvider.RealmAccess => realm; IResourceStore IStorageResourceProvider.Files => files; IResourceStore IStorageResourceProvider.Resources => resources; IResourceStore IStorageResourceProvider.CreateTextureLoaderStore(IResourceStore underlyingStore) => host?.CreateTextureLoaderStore(underlyingStore); diff --git a/osu.Game/Configuration/BackgroundSource.cs b/osu.Game/Configuration/BackgroundSource.cs index 8b10ddff07..a8f6b142d4 100644 --- a/osu.Game/Configuration/BackgroundSource.cs +++ b/osu.Game/Configuration/BackgroundSource.cs @@ -18,10 +18,10 @@ namespace osu.Game.Configuration [LocalisableDescription(typeof(UserInterfaceStrings), nameof(UserInterfaceStrings.BeatmapWithStoryboard))] BeatmapWithStoryboard, - [Description("'EzResource/Webm/*.webm' from local folders")] + [Description("'EzResource/Webm/*' from osu data folder ")] WebmSource, - [Description("'EzResource/BG/*.png' from local folders")] + [Description("'EzResource/BG/*' from local folders")] Slides, } } diff --git a/osu.Game/Database/LegacyBeatmapExporter.cs b/osu.Game/Database/LegacyBeatmapExporter.cs index 7587c2332f..0ddce16355 100644 --- a/osu.Game/Database/LegacyBeatmapExporter.cs +++ b/osu.Game/Database/LegacyBeatmapExporter.cs @@ -13,6 +13,7 @@ using osu.Game.Beatmaps.Formats; using osu.Game.Beatmaps.Timing; using osu.Game.Extensions; using osu.Game.IO; +using osu.Game.Localisation; using osu.Game.Overlays.Notifications; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; @@ -198,7 +199,7 @@ namespace osu.Game.Database ProgressNotification notification = new ProgressNotification { State = ProgressNotificationState.Active, - Text = $"Exporting {itemFilename}...", + Text = NotificationsStrings.FileExportOngoing(itemFilename), }; PostNotification?.Invoke(notification); @@ -225,7 +226,7 @@ namespace osu.Game.Database throw; } - notification.CompletionText = $"Exported {itemFilename}! Click to view."; + notification.CompletionText = NotificationsStrings.FileExportFinished(itemFilename); notification.CompletionClickAction = () => ExportStorage.PresentFileExternally(filename); notification.State = ProgressNotificationState.Completed; }); diff --git a/osu.Game/Database/LegacyExporter.cs b/osu.Game/Database/LegacyExporter.cs index cc0fd8f8a2..eec6bf9341 100644 --- a/osu.Game/Database/LegacyExporter.cs +++ b/osu.Game/Database/LegacyExporter.cs @@ -10,6 +10,7 @@ using System.Threading.Tasks; using osu.Framework.Platform; using osu.Game.Extensions; using osu.Game.IO; +using osu.Game.Localisation; using osu.Game.Overlays.Notifications; using osu.Game.Utils; using Realms; @@ -83,7 +84,7 @@ namespace osu.Game.Database ProgressNotification notification = new ProgressNotification { State = ProgressNotificationState.Active, - Text = $"Exporting {itemFilename}...", + Text = NotificationsStrings.FileExportOngoing(itemFilename), }; PostNotification?.Invoke(notification); @@ -106,7 +107,7 @@ namespace osu.Game.Database throw; } - notification.CompletionText = $"Exported {itemFilename}! Click to view."; + notification.CompletionText = NotificationsStrings.FileExportFinished(itemFilename); notification.CompletionClickAction = () => ExportStorage.PresentFileExternally(filename); notification.State = ProgressNotificationState.Completed; } diff --git a/osu.Game/Graphics/ScreenshotManager.cs b/osu.Game/Graphics/ScreenshotManager.cs index a085558b3a..28795b5799 100644 --- a/osu.Game/Graphics/ScreenshotManager.cs +++ b/osu.Game/Graphics/ScreenshotManager.cs @@ -17,6 +17,7 @@ using osu.Framework.Platform; using osu.Framework.Threading; using osu.Game.Configuration; using osu.Game.Input.Bindings; +using osu.Game.Localisation; using osu.Game.Online.Multiplayer; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; @@ -169,7 +170,7 @@ namespace osu.Game.Graphics notificationOverlay.Post(new SimpleNotification { - Text = $"Screenshot {filename} saved!", + Text = NotificationsStrings.ScreenshotSaved(filename), Activated = () => { storage.PresentFileExternally(filename); diff --git a/osu.Game/Graphics/UserInterfaceV2/FormButton.cs b/osu.Game/Graphics/UserInterfaceV2/FormButton.cs index 85198191b8..7b95a7e976 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormButton.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormButton.cs @@ -4,6 +4,7 @@ using System; using System.Diagnostics; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; @@ -12,6 +13,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; using osu.Framework.Localisation; +using osu.Framework.Utils; using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; @@ -28,62 +30,133 @@ namespace osu.Game.Graphics.UserInterfaceV2 /// public LocalisableString Caption { get; init; } + /// + /// Sets text inside the button. + /// public LocalisableString ButtonText { get; init; } - public Action? Action { get; init; } + /// + /// Sets a custom button icon. Not shown when is set. + /// + public IconUsage ButtonIcon { get; init; } = FontAwesome.Solid.ChevronRight; + + private readonly Color4? backgroundColour; + + /// + /// Sets a custom background colour for the button. + /// + public Color4? BackgroundColour + { + get => backgroundColour; + init + { + backgroundColour = value; + + if (IsLoaded) + updateState(); + } + } + + /// + /// The action to invoke when the button is clicked. + /// + public Action? Action { get; set; } + + /// + /// Whether the button is enabled. + /// + public readonly BindableBool Enabled = new BindableBool(true); [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; + private Container content = null!; + private Box background = null!; + private OsuTextFlowContainer text = null!; + private Button button = null!; + [BackgroundDependencyLoader] private void load() { RelativeSizeAxes = Axes.X; - Height = 50; + AutoSizeAxes = Axes.Y; - Masking = true; - CornerRadius = 5; - CornerExponent = 2.5f; - - InternalChildren = new Drawable[] + InternalChild = content = new Container { - new Box + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Masking = true, + CornerRadius = 5, + CornerExponent = 2.5f, + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Background5, - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding + background = new Box { - Left = 9, - Right = 5, - Vertical = 5, + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background5, }, - Children = new Drawable[] + new TrianglesV2 { - new OsuTextFlowContainer + SpawnRatio = 0.5f, + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientVertical(colourProvider.Background4, colourProvider.Background5), + }, + new HoverClickSounds(HoverSampleSet.Button) + { + Enabled = { BindTarget = Enabled }, + }, + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Width = 0.45f, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Text = Caption, + Left = 9, + Right = 5, + Vertical = 5, }, - new Button + Children = new Drawable[] { - Action = Action, - Text = ButtonText, - RelativeSizeAxes = ButtonText == default ? Axes.None : Axes.X, - Width = ButtonText == default ? 90 : 0.45f, - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - } + text = new OsuTextFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Text = Caption, + }, + button = new Button + { + Action = () => Action?.Invoke(), + Text = ButtonText, + Icon = ButtonIcon, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Enabled = { BindTarget = Enabled }, + } + }, }, - }, + } }; + + if (ButtonText == default) + { + text.Padding = new MarginPadding { Right = 100 }; + button.Width = 90; + } + else + { + text.Width = 0.55f; + text.Padding = new MarginPadding { Right = 10 }; + button.RelativeSizeAxes = Axes.X; + button.Width = 0.45f; + } + } + + protected override void LoadComplete() + { + base.LoadComplete(); + Enabled.BindValueChanged(_ => updateState(), true); } protected override bool OnHover(HoverEvent e) @@ -98,12 +171,34 @@ namespace osu.Game.Graphics.UserInterfaceV2 updateState(); } + protected override bool OnClick(ClickEvent e) + { + if (Enabled.Value) + { + background.FlashColour(ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark2), 800, Easing.OutQuint); + button.TriggerClick(); + } + + return true; + } + private void updateState() { - BorderThickness = IsHovered ? 2 : 0; + text.Colour = Enabled.Value ? colourProvider.Content1 : colourProvider.Background1; - if (IsHovered) - BorderColour = colourProvider.Light4; + background.FadeColour(IsHovered + ? ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark4) + : colourProvider.Background5, 200, Easing.OutQuint); + + content.BorderThickness = IsHovered ? 2 : 0; + + if (BackgroundColour != null) + { + button.BackgroundColour = BackgroundColour.Value; + content.BorderColour = Enabled.Value ? BackgroundColour.Value : Interpolation.ValueAt(0.75, BackgroundColour.Value, colourProvider.Dark1, 0, 1); + } + else + content.BorderColour = Enabled.Value ? colourProvider.Light4 : colourProvider.Dark1; } public partial class Button : OsuButton @@ -125,6 +220,8 @@ namespace osu.Game.Graphics.UserInterfaceV2 } } + public IconUsage Icon { get; init; } + [BackgroundDependencyLoader] private void load(OverlayColourProvider overlayColourProvider) { @@ -135,7 +232,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 { Add(new SpriteIcon { - Icon = FontAwesome.Solid.ChevronRight, + Icon = Icon, Size = new Vector2(16), Shadow = true, Anchor = Anchor.Centre, diff --git a/osu.Game/Graphics/UserInterfaceV2/FormDropdown.cs b/osu.Game/Graphics/UserInterfaceV2/FormDropdown.cs index 91ea036ec6..9482f1f831 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormDropdown.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormDropdown.cs @@ -40,6 +40,8 @@ namespace osu.Game.Graphics.UserInterfaceV2 private FormDropdownHeader header = null!; + private const float header_menu_spacing = 5; + [BackgroundDependencyLoader] private void load() { @@ -47,6 +49,10 @@ namespace osu.Game.Graphics.UserInterfaceV2 header.Caption = Caption; header.HintText = HintText; + + // there's bottom margin applied inside the header to give spacing between the header and the menu. + // however when the menu is closed the extra spacing remains present. to remove it, apply negative bottom padding here. + Margin = new MarginPadding { Bottom = -header_menu_spacing }; } protected override void LoadComplete() @@ -145,6 +151,8 @@ namespace osu.Game.Graphics.UserInterfaceV2 Masking = true; CornerRadius = 5; + Margin = new MarginPadding { Bottom = header_menu_spacing }; + Foreground.Padding = new MarginPadding(9); Foreground.Children = new Drawable[] { @@ -278,11 +286,26 @@ namespace osu.Game.Graphics.UserInterfaceV2 private void load(OverlayColourProvider colourProvider) { ItemsContainer.Padding = new MarginPadding(9); - Margin = new MarginPadding { Top = 5 }; MaskingContainer.BorderThickness = 2; MaskingContainer.BorderColour = colourProvider.Highlight1; } + + protected override void AnimateOpen() + { + base.AnimateOpen(); + + // there's negative bottom margin applied on the whole dropdown control to remove extra spacing when the menu is closed. + // however, when the menu is open, we want spacing between the menu and the next control below it. therefore apply bottom margin here. + // we use a transform to keep the open animation smooth while margin is adjusted. + this.TransformTo(nameof(Margin), new MarginPadding { Bottom = header_menu_spacing }, 300, Easing.OutQuint); + } + + protected override void AnimateClose() + { + base.AnimateClose(); + this.TransformTo(nameof(Margin), new MarginPadding { Bottom = 0 }, 300, Easing.OutQuint); + } } } diff --git a/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs b/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs index 625967dc66..e228491880 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs @@ -134,6 +134,8 @@ namespace osu.Game.Graphics.UserInterfaceV2 private readonly Bindable currentLanguage = new Bindable(); + public bool TakeFocus() => GetContainingFocusManager()?.ChangeFocus(textBox) == true; + public FormSliderBar() { LabelFormat ??= defaultLabelFormat; @@ -233,6 +235,11 @@ namespace osu.Game.Graphics.UserInterfaceV2 TooltipFormat = TooltipFormat, DisplayAsPercentage = DisplayAsPercentage, PlaySamplesOnAdjust = PlaySamplesOnAdjust, + ResetToDefault = () => + { + if (!IsDisabled) + SetDefault(); + } } }, }, @@ -323,7 +330,11 @@ namespace osu.Game.Graphics.UserInterfaceV2 break; case Bindable bindableDouble: - bindableDouble.Value = double.Parse(textBox.Current.Value); + bindableDouble.Value = double.Parse(textBox.Current.Value) / (DisplayAsPercentage ? 100 : 1); + break; + + case Bindable bindableFloat: + bindableFloat.Value = float.Parse(textBox.Current.Value) / (DisplayAsPercentage ? 100 : 1); break; default: @@ -390,7 +401,18 @@ namespace osu.Game.Graphics.UserInterfaceV2 { if (updatingFromTextBox) return; - textBox.Text = currentNumberInstantaneous.Value.ToStandardFormattedString(OsuSliderBar.MAX_DECIMAL_DIGITS); + if (DisplayAsPercentage) + { + double floatValue = double.CreateTruncating(currentNumberInstantaneous.Value); + + if (currentNumberInstantaneous.Value is int) + floatValue /= 100; + + textBox.Text = floatValue.ToStandardFormattedString(Math.Max(0, OsuSliderBar.MAX_DECIMAL_DIGITS - 2)); + } + else + textBox.Text = currentNumberInstantaneous.Value.ToStandardFormattedString(OsuSliderBar.MAX_DECIMAL_DIGITS); + valueLabel.Text = LabelFormat(currentNumberInstantaneous.Value); } @@ -400,9 +422,11 @@ namespace osu.Game.Graphics.UserInterfaceV2 { public BindableBool Focused { get; } = new BindableBool(); - public BindableBool IsDragging { get; set; } = new BindableBool(); + public BindableBool IsDragging { get; } = new BindableBool(); - public Action? OnCommit { get; set; } + public Action? ResetToDefault { get; init; } + + public Action? OnCommit { get; init; } public sealed override LocalisableString TooltipText => base.TooltipText; @@ -453,11 +477,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 Padding = new MarginPadding { Horizontal = RangePadding, }, Child = nub = new InnerSliderNub { - ResetToDefault = () => - { - if (!Current.Disabled) - Current.SetDefault(); - } + ResetToDefault = ResetToDefault, } }, sounds = new HoverClickSounds() diff --git a/osu.Game/Localisation/NotificationsStrings.cs b/osu.Game/Localisation/NotificationsStrings.cs index 0d22d98eb1..835dfef08e 100644 --- a/osu.Game/Localisation/NotificationsStrings.cs +++ b/osu.Game/Localisation/NotificationsStrings.cs @@ -146,14 +146,9 @@ Click to see what's new!", version); public static LocalisableString FriendOffline(string info) => new TranslatableString(getKey(@"friend_offline"), @"Offline: {0}", info); /// - /// "Connection to API was lost. Can't continue with online play." + /// "Connection to online services was interrupted. osu! will be operating with limited functionality." /// - public static LocalisableString APIDisconnect => new TranslatableString(getKey(@"api_disconnect"), @"Connection to API was lost. Can't continue with online play."); - - /// - /// "Connection to the multiplayer server was lost. Exiting multiplayer." - /// - public static LocalisableString MultiplayerDisconnect => new TranslatableString(getKey(@"multiplayer_disconnect"), @"Connection to the multiplayer server was lost. Exiting multiplayer."); + public static LocalisableString APIConnectionInterrupted => new TranslatableString(getKey(@"api_connection_interrupted"), @"Connection to online services was interrupted. osu! will be operating with limited functionality."); /// /// "You have been logged out on this device due to a login to your account on another device." @@ -215,6 +210,56 @@ Click to see what's new!", version); /// public static LocalisableString ActionInterruptedByDialog => new TranslatableString(getKey(@"action_interrupted_by_dialog"), @"An action was interrupted due to a dialog being displayed."); + /// + /// "Exporting {0}..." + /// + public static LocalisableString FileExportOngoing(string filename) => new TranslatableString(getKey(@"file_export_ongoing"), @"Exporting {0}...", filename); + + /// + /// "Exported {0}! Click to view." + /// + public static LocalisableString FileExportFinished(string filename) => new TranslatableString(getKey(@"file_export_finished"), @"Exported {0}! Click to view.", filename); + + /// + /// "Exporting logs..." + /// + public static LocalisableString LogsExportOngoing => new TranslatableString(getKey(@"logs_export_ongoing"), @"Exporting logs..."); + + /// + /// "Exported logs! Click to view." + /// + public static LocalisableString LogsExportFinished => new TranslatableString(getKey(@"logs_export_finished"), @"Exported logs! Click to view."); + + /// + /// "Running osu! as {0} does not improve performance, may break integrations and poses a security risk. Please run the game as a normal user." + /// + public static LocalisableString ElevatedPrivileges(LocalisableString user) => new TranslatableString(getKey(@"elevated_privileges"), @"Running osu! as {0} does not improve performance, may break integrations and poses a security risk. Please run the game as a normal user.", user); + + /// + /// "Screenshot {0} saved!" + /// + public static LocalisableString ScreenshotSaved(string filename) => new TranslatableString(getKey(@"screenshot_saved"), @"Screenshot {0} saved!", filename); + + /// + /// "The multiplayer server will be right back..." + /// + public static LocalisableString MultiplayerServerShuttingDownImmediately => new TranslatableString(getKey(@"multiplayer_server_shutting_down_immediately"), @"The multiplayer server will be right back..."); + + /// + /// "The multiplayer server is restarting in {0}." + /// + public static LocalisableString MultiplayerServerShuttingDownRemaining(string remainingTime) => new TranslatableString(getKey(@"multiplayer_server_shutting_down_remaining"), @"The multiplayer server is restarting in {0}.", remainingTime); + + /// + /// "Created new collection "{0}" with {1} beatmaps." + /// + public static LocalisableString CollectionCreated(string name, int beatmapsCount) => new TranslatableString(getKey(@"collection_created"), @"Created new collection ""{0}"" with {1} beatmaps.", name, beatmapsCount); + + /// + /// "Added {0} beatmaps to collection "{1}"." + /// + public static LocalisableString CollectionBeatmapsAdded(string name, int beatmapsCount) => new TranslatableString(getKey(@"collection_beatmaps_added"), @"Added {0} beatmaps to collection ""{1}"".", beatmapsCount, name); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Online/Multiplayer/MultiplayerClientExtensions.cs b/osu.Game/Online/Multiplayer/MultiplayerClientExtensions.cs index 1cc5a8e70a..61824914b5 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClientExtensions.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClientExtensions.cs @@ -3,6 +3,7 @@ using System; using System.Diagnostics; +using System.Net.WebSockets; using System.Threading.Tasks; using Microsoft.AspNetCore.SignalR; using osu.Framework.Extensions.ExceptionExtensions; @@ -20,13 +21,23 @@ namespace osu.Game.Online.Multiplayer Debug.Assert(t.Exception != null); Exception exception = t.Exception.AsSingular(); + onError?.Invoke(exception); + + if (exception is WebSocketException wse && wse.Message == @"The remote party closed the WebSocket connection without completing the close handshake.") + { + // OnlineStatusNotifier is already letting users know about interruptions to connections. + // Silence these because it gets very spammy otherwise. + return; + } + if (exception.GetHubExceptionMessage() is string message) + { // Hub exceptions generally contain something we can show the user directly. Logger.Log(message, level: LogLevel.Important); - else - Logger.Error(exception, $"Unobserved exception occurred via {nameof(FireAndForget)} call: {exception.Message}"); + return; + } - onError?.Invoke(exception); + Logger.Error(exception, $"Unobserved exception occurred via {nameof(FireAndForget)} call: {exception.Message}"); } else { diff --git a/osu.Game/Online/Multiplayer/ServerShutdownNotification.cs b/osu.Game/Online/Multiplayer/ServerShutdownNotification.cs index 1de18e44a7..5a6367b06b 100644 --- a/osu.Game/Online/Multiplayer/ServerShutdownNotification.cs +++ b/osu.Game/Online/Multiplayer/ServerShutdownNotification.cs @@ -5,6 +5,7 @@ using System; using Humanizer.Localisation; using osu.Framework.Allocation; using osu.Framework.Threading; +using osu.Game.Localisation; using osu.Game.Overlays.Notifications; using osu.Game.Utils; @@ -53,10 +54,10 @@ namespace osu.Game.Online.Multiplayer if (remaining.TotalSeconds <= 5) { updateDelegate?.Cancel(); - Text = "The multiplayer server will be right back..."; + Text = NotificationsStrings.MultiplayerServerShuttingDownImmediately; } else - Text = $"The multiplayer server is restarting in {HumanizerUtils.Humanize(remaining, precision: 3, minUnit: TimeUnit.Second)}."; + Text = NotificationsStrings.MultiplayerServerShuttingDownRemaining(HumanizerUtils.Humanize(remaining, precision: 3, minUnit: TimeUnit.Second)); } } } diff --git a/osu.Game/Online/OnlineStatusNotifier.cs b/osu.Game/Online/OnlineStatusNotifier.cs index da58dc3d46..f4c33c67aa 100644 --- a/osu.Game/Online/OnlineStatusNotifier.cs +++ b/osu.Game/Online/OnlineStatusNotifier.cs @@ -17,6 +17,7 @@ using osu.Game.Online.Spectator; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; using osu.Game.Screens.OnlinePlay; +using osu.Game.Screens.Play; namespace osu.Game.Online { @@ -75,22 +76,16 @@ namespace osu.Game.Online apiState.BindValueChanged(state => { - if (state.NewValue == APIState.Online) + switch (state.NewValue) { - userNotified = false; - return; - } + case APIState.Online: + userNotified = false; + return; - if (userNotified) return; - - if (state.NewValue == APIState.Offline && getCurrentScreen() is OnlinePlayScreen) - { - userNotified = true; - notificationOverlay?.Post(new SimpleErrorNotification - { - Icon = FontAwesome.Solid.ExclamationCircle, - Text = NotificationsStrings.APIDisconnect, - }); + case APIState.Offline: + if (getCurrentScreen() is OnlinePlayScreen) + notifyApiDisconnection(); + break; } }); @@ -102,22 +97,37 @@ namespace osu.Game.Online return; } - if (userNotified) return; - if (multiplayerClient.Room != null) - { - userNotified = true; - notificationOverlay?.Post(new SimpleErrorNotification - { - Icon = FontAwesome.Solid.ExclamationCircle, - Text = NotificationsStrings.MultiplayerDisconnect, - }); - } + notifyApiDisconnection(); })); - spectatorState.BindValueChanged(_ => + spectatorState.BindValueChanged(connected => Schedule(() => { - // TODO: handle spectator server failure somehow? + if (connected.NewValue) + { + userNotified = false; + return; + } + + switch (getCurrentScreen()) + { + case SpectatorPlayer: // obvious issues + case SubmittingPlayer: // replay sending issues + notifyApiDisconnection(); + break; + } + })); + } + + private void notifyApiDisconnection() + { + if (userNotified) return; + + userNotified = true; + notificationOverlay?.Post(new SimpleErrorNotification + { + Icon = FontAwesome.Solid.ExclamationCircle, + Text = NotificationsStrings.APIConnectionInterrupted, }); } diff --git a/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs b/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs index 8cec85b748..8fa2520e8f 100644 --- a/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs +++ b/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs @@ -3,7 +3,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; @@ -86,7 +85,7 @@ namespace osu.Game.Overlays.Music private readonly GlobalAction action; public MusicActionToast(LocalisableString value, GlobalAction action) - : base(ToastStrings.MusicPlayback, value, string.Empty) + : base(ToastStrings.MusicPlayback, value) { this.action = action; } @@ -94,7 +93,7 @@ namespace osu.Game.Overlays.Music [BackgroundDependencyLoader] private void load(RealmKeyBindingStore keyBindingStore) { - ShortcutText.Text = keyBindingStore.GetBindingsStringFor(action).ToUpper(); + ExtraText = keyBindingStore.GetBindingsStringFor(action); } } } diff --git a/osu.Game/Overlays/OSD/CopiedToClipboardToast.cs b/osu.Game/Overlays/OSD/CopiedToClipboardToast.cs index 4059a274ad..455d93d7ad 100644 --- a/osu.Game/Overlays/OSD/CopiedToClipboardToast.cs +++ b/osu.Game/Overlays/OSD/CopiedToClipboardToast.cs @@ -8,7 +8,7 @@ namespace osu.Game.Overlays.OSD public partial class CopiedToClipboardToast : Toast { public CopiedToClipboardToast() - : base(CommonStrings.General, ToastStrings.CopiedToClipboard, "") + : base(CommonStrings.General, ToastStrings.CopiedToClipboard) { } } diff --git a/osu.Game/Overlays/OSD/SpeedChangeToast.cs b/osu.Game/Overlays/OSD/SpeedChangeToast.cs index 652c043357..48cf6d2b0a 100644 --- a/osu.Game/Overlays/OSD/SpeedChangeToast.cs +++ b/osu.Game/Overlays/OSD/SpeedChangeToast.cs @@ -1,6 +1,7 @@ // 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.Game.Input; using osu.Game.Input.Bindings; using osu.Game.Localisation; @@ -9,9 +10,15 @@ namespace osu.Game.Overlays.OSD { public partial class SpeedChangeToast : Toast { - public SpeedChangeToast(RealmKeyBindingStore keyBindingStore, double newSpeed) - : base(ModSelectOverlayStrings.ModCustomisation, ToastStrings.SpeedChangedTo(newSpeed), keyBindingStore.GetBindingsStringFor(GlobalAction.IncreaseModSpeed) + " / " + keyBindingStore.GetBindingsStringFor(GlobalAction.DecreaseModSpeed)) + public SpeedChangeToast(double newSpeed) + : base(ModSelectOverlayStrings.ModCustomisation, ToastStrings.SpeedChangedTo(newSpeed)) { } + + [BackgroundDependencyLoader] + private void load(RealmKeyBindingStore keyBindingStore) + { + ExtraText = keyBindingStore.GetBindingsStringFor(GlobalAction.IncreaseModSpeed) + " / " + keyBindingStore.GetBindingsStringFor(GlobalAction.DecreaseModSpeed); + } } } diff --git a/osu.Game/Overlays/OSD/Toast.cs b/osu.Game/Overlays/OSD/Toast.cs index 7df534d90d..a809c37800 100644 --- a/osu.Game/Overlays/OSD/Toast.cs +++ b/osu.Game/Overlays/OSD/Toast.cs @@ -10,23 +10,30 @@ using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osuTK; using osuTK.Graphics; -using osu.Game.Localisation; namespace osu.Game.Overlays.OSD { public abstract partial class Toast : Container { + /// + /// Extra text to be shown at the bottom of the toast. Usually a key binding if available. + /// + public LocalisableString ExtraText + { + get => extraText.Text; + set => extraText.Text = value.ToUpper(); + } + private const int toast_minimum_width = 240; private readonly Container content; protected override Container Content => content; - protected readonly OsuSpriteText ValueText; + protected readonly OsuSpriteText ValueSpriteText; + private readonly OsuSpriteText extraText; - protected readonly OsuSpriteText ShortcutText; - - protected Toast(LocalisableString description, LocalisableString value, LocalisableString shortcut) + protected Toast(LocalisableString description, LocalisableString value) { Anchor = Anchor.Centre; Origin = Anchor.Centre; @@ -65,7 +72,7 @@ namespace osu.Game.Overlays.OSD Origin = Anchor.TopCentre, Text = description.ToUpper() }, - ValueText = new OsuSpriteText + ValueSpriteText = new OsuSpriteText { Font = OsuFont.GetFont(size: 24, weight: FontWeight.Light), Padding = new MarginPadding { Horizontal = 10 }, @@ -74,15 +81,14 @@ namespace osu.Game.Overlays.OSD Origin = Anchor.Centre, Text = value }, - ShortcutText = new OsuSpriteText + extraText = new OsuSpriteText { Anchor = Anchor.BottomCentre, Origin = Anchor.BottomCentre, - Name = "Shortcut", + Name = "Extra Text", Alpha = 0.3f, Margin = new MarginPadding { Bottom = 15, Horizontal = 10 }, Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold), - Text = string.IsNullOrEmpty(shortcut.ToString()) ? ToastStrings.NoKeyBound.ToUpper() : shortcut.ToUpper() }, }; } diff --git a/osu.Game/Overlays/OSD/TrackedSettingToast.cs b/osu.Game/Overlays/OSD/TrackedSettingToast.cs index 1aa6de423e..73fed25b1a 100644 --- a/osu.Game/Overlays/OSD/TrackedSettingToast.cs +++ b/osu.Game/Overlays/OSD/TrackedSettingToast.cs @@ -35,8 +35,10 @@ namespace osu.Game.Overlays.OSD private Bindable lastPlaybackTime; public TrackedSettingToast(SettingDescription description) - : base(description.Name, description.Value, description.Shortcut) + : base(description.Name, description.Value) { + ExtraText = description.Shortcut; + FillFlowContainer optionLights; Children = new Drawable[] @@ -75,7 +77,7 @@ namespace osu.Game.Overlays.OSD break; } - ValueText.Origin = optionCount > 0 ? Anchor.BottomCentre : Anchor.Centre; + ValueSpriteText.Origin = optionCount > 0 ? Anchor.BottomCentre : Anchor.Centre; for (int i = 0; i < optionCount; i++) optionLights.Add(new OptionLight { Glowing = i == selectedOption }); diff --git a/osu.Game/Overlays/Settings/Sections/General/QuickActionSettings.cs b/osu.Game/Overlays/Settings/Sections/General/QuickActionSettings.cs index b6b78a6d00..d01a63f87a 100644 --- a/osu.Game/Overlays/Settings/Sections/General/QuickActionSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/QuickActionSettings.cs @@ -84,7 +84,7 @@ namespace osu.Game.Overlays.Settings.Sections.General ProgressNotification notification = new ProgressNotification { State = ProgressNotificationState.Active, - Text = "Exporting logs...", + Text = NotificationsStrings.LogsExportOngoing, }; notifications?.Post(notification); @@ -116,7 +116,7 @@ namespace osu.Game.Overlays.Settings.Sections.General throw; } - notification.CompletionText = "Exported logs! Click to view."; + notification.CompletionText = NotificationsStrings.LogsExportFinished; notification.CompletionClickAction = () => exportStorage.PresentFileExternally(archive_filename); notification.State = ProgressNotificationState.Completed; diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index cdf315f076..3cb06d6405 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -772,8 +772,9 @@ namespace osu.Game.Overlays.SkinEditor private partial class SkinEditorToast : Toast { public SkinEditorToast(LocalisableString value, string skinDisplayName) - : base(SkinSettingsStrings.SkinLayoutEditor, value, skinDisplayName) + : base(SkinSettingsStrings.SkinLayoutEditor, value) { + ExtraText = skinDisplayName; } } diff --git a/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs b/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs index 64f938ba6c..0b8df0ad41 100644 --- a/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs +++ b/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs @@ -308,7 +308,7 @@ namespace osu.Game.Rulesets.Edit private readonly ValueChangedEvent change; public DistanceSpacingToast(LocalisableString value, ValueChangedEvent change) - : base(getAction(change).GetLocalisableDescription(), value, string.Empty) + : base(getAction(change).GetLocalisableDescription(), value) { this.change = change; } @@ -316,7 +316,7 @@ namespace osu.Game.Rulesets.Edit [BackgroundDependencyLoader] private void load(RealmKeyBindingStore keyBindingStore) { - ShortcutText.Text = keyBindingStore.GetBindingsStringFor(getAction(change)).ToUpper(); + ExtraText = keyBindingStore.GetBindingsStringFor(getAction(change)); } private static GlobalAction getAction(ValueChangedEvent change) => change.NewValue - change.OldValue > 0 diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index e838687065..799556acdf 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -19,7 +19,6 @@ using osu.Framework.Utils; using osu.Game.Audio; using osu.Game.Configuration; using osu.Game.Graphics; -using osu.Game.LAsEzExtensions.Audio; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Pooling; using osu.Game.Rulesets.Objects.Types; @@ -741,9 +740,7 @@ namespace osu.Game.Rulesets.Objects.Drawables Result.GameplayRate = (Clock as IGameplayClock)?.GetTrueGameplayRate() ?? Clock.Rate; if (Result.HasResult) - { UpdateState(Result.IsHit ? ArmedState.Hit : ArmedState.Miss); - } OnNewResult?.Invoke(this, Result); } diff --git a/osu.Game/Screens/Edit/Components/FormSampleSet.cs b/osu.Game/Screens/Edit/Components/FormSampleSet.cs index 50c0532c02..f2d31dea06 100644 --- a/osu.Game/Screens/Edit/Components/FormSampleSet.cs +++ b/osu.Game/Screens/Edit/Components/FormSampleSet.cs @@ -42,7 +42,7 @@ namespace osu.Game.Screens.Edit.Components set => current.Current = value; } - public Func? SampleAddRequested { get; init; } + public Func? SampleAddRequested { get; init; } public Action? SampleRemoveRequested { get; init; } private readonly BindableWithCurrent current = new BindableWithCurrent(); @@ -194,7 +194,7 @@ namespace osu.Game.Screens.Edit.Components /// /// Invoked when a new sample is selected via this button. /// - public Func? SampleAddRequested { get; init; } + public Func? SampleAddRequested { get; init; } /// /// Invoked when a sample removal is selected via this button. @@ -294,7 +294,18 @@ namespace osu.Game.Screens.Edit.Components AddInternal(hoverSounds = (ActualFilename.Value == null ? new HoverClickSounds(HoverSampleSet.Button) : new HoverSounds(HoverSampleSet.Button))); - sample = ActualFilename.Value == null ? null : editorBeatmap?.BeatmapSkin?.Skin.Samples?.Get(ActualFilename.Value); + if (ActualFilename.Value != null) + { + // to cover all bases, invalidate the extensionless filename (which gameplay is most likely to use) + // as well as the filename with extension (which we are using here). + editorBeatmap?.BeatmapSkin?.Skin.Samples?.Invalidate(ExpectedFilename.Value); + editorBeatmap?.BeatmapSkin?.Skin.Samples?.Invalidate(ActualFilename.Value); + sample = editorBeatmap?.BeatmapSkin?.Skin.Samples?.Get(ActualFilename.Value); + } + else + { + sample = null; + } } protected override bool OnHover(HoverEvent e) @@ -317,7 +328,7 @@ namespace osu.Game.Screens.Edit.Components return; this.HidePopover(); - ActualFilename.Value = SampleAddRequested?.Invoke(selectedFile.Value) ?? selectedFile.Value.ToString(); + ActualFilename.Value = SampleAddRequested?.Invoke(selectedFile.Value, ExpectedFilename.Value) ?? selectedFile.Value.ToString(); } private void deleteSample() diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 03a0942e19..991f10497d 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -1620,8 +1620,9 @@ namespace osu.Game.Screens.Edit private partial class BeatmapEditorToast : Toast { public BeatmapEditorToast(LocalisableString value, string beatmapDisplayName) - : base(InputSettingsStrings.EditorSection, value, beatmapDisplayName) + : base(InputSettingsStrings.EditorSection, value) { + ExtraText = beatmapDisplayName; } } diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index 91ae4593dd..48b793d1b5 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -101,11 +101,8 @@ namespace osu.Game.Screens.Edit this.beatmapInfo = beatmapInfo ?? playableBeatmap.BeatmapInfo; - if (beatmapSkin is Skin skin) - { - BeatmapSkin = new EditorBeatmapSkin(skin); - BeatmapSkin.BeatmapSkinChanged += SaveState; - } + if (beatmapSkin is LegacyBeatmapSkin skin) + BeatmapSkin = new EditorBeatmapSkin(this, skin); beatmapProcessor = new EditorBeatmapProcessor(this, playableBeatmap.BeatmapInfo.Ruleset.CreateInstance()); @@ -532,5 +529,11 @@ namespace osu.Game.Screens.Edit public double GetBeatLengthAtTime(double referenceTime) => ControlPointInfo.TimingPointAt(referenceTime).BeatLength / BeatDivisor; public int BeatDivisor => beatDivisor?.Value ?? 1; + + protected override void Dispose(bool isDisposing) + { + BeatmapSkin?.Dispose(); + base.Dispose(isDisposing); + } } } diff --git a/osu.Game/Screens/Edit/EditorBeatmapSkin.cs b/osu.Game/Screens/Edit/EditorBeatmapSkin.cs index 531f77d238..8992211ace 100644 --- a/osu.Game/Screens/Edit/EditorBeatmapSkin.cs +++ b/osu.Game/Screens/Edit/EditorBeatmapSkin.cs @@ -18,14 +18,14 @@ namespace osu.Game.Screens.Edit /// /// A beatmap skin which is being edited. /// - public class EditorBeatmapSkin : ISkin + public class EditorBeatmapSkin : ISkin, IDisposable { public event Action? BeatmapSkinChanged; /// /// The underlying beatmap skin. /// - protected internal readonly Skin Skin; + protected internal readonly LegacyBeatmapSkin Skin; /// /// The combo colours of this skin. @@ -33,10 +33,13 @@ namespace osu.Game.Screens.Edit /// public BindableList ComboColours { get; } - public EditorBeatmapSkin(Skin skin) - { - Skin = skin; + private readonly EditorBeatmap editorBeatmap; + public EditorBeatmapSkin(EditorBeatmap editorBeatmap, LegacyBeatmapSkin skin) + { + this.editorBeatmap = editorBeatmap; + + Skin = skin; ComboColours = new BindableList(); if (Skin.Configuration.ComboColours is IReadOnlyList comboColours) @@ -50,9 +53,14 @@ namespace osu.Game.Screens.Edit } ComboColours.BindCollectionChanged((_, _) => updateColours()); + + if (skin.BeatmapSetResources != null) + skin.BeatmapSetResources.CacheInvalidated += InvokeSkinChanged; } - private void invokeSkinChanged() => BeatmapSkinChanged?.Invoke(); + public void InvokeSkinChanged() => BeatmapSkinChanged?.Invoke(); + + #region Combo colours private void updateColours() { @@ -60,9 +68,14 @@ namespace osu.Game.Screens.Edit Skin.Configuration.CustomComboColours.Clear(); for (int i = 0; i < ComboColours.Count; ++i) Skin.Configuration.CustomComboColours.Add(ComboColours[(ComboColours.Count + i - 1) % ComboColours.Count]); - invokeSkinChanged(); + InvokeSkinChanged(); + editorBeatmap.SaveState(); } + #endregion + + #region Sample sets + public record SampleSet(int SampleSetIndex, string Name) { public SampleSet(int sampleSetIndex) @@ -88,7 +101,7 @@ namespace osu.Game.Screens.Edit string[] possiblePrefixes = possibleSounds.SelectMany(sound => possibleBanks.Select(bank => $@"{bank}-{sound}")).ToArray(); - HashSet indices = new HashSet(); + Dictionary sampleSets = new Dictionary(); if (Skin.Samples != null) { @@ -96,19 +109,39 @@ namespace osu.Game.Screens.Edit { foreach (string possiblePrefix in possiblePrefixes) { - if (!sample.StartsWith(possiblePrefix, StringComparison.InvariantCultureIgnoreCase)) + if (!sample.StartsWith(possiblePrefix, StringComparison.Ordinal)) continue; string indexString = Path.GetFileNameWithoutExtension(sample)[possiblePrefix.Length..]; + int? index = null; + if (string.IsNullOrEmpty(indexString)) - indices.Add(1); - if (int.TryParse(indexString, out int index)) - indices.Add(index); + index = 1; + if (int.TryParse(indexString, out int parsed)) + index = parsed; + + if (!index.HasValue) + continue; + + SampleSet? sampleSet; + if (!sampleSets.TryGetValue(index.Value, out sampleSet)) + sampleSet = sampleSets[index.Value] = new SampleSet(index.Value); + + sampleSet.Filenames.Add(sample); } } } - return indices.OrderBy(i => i).Select(i => new SampleSet(i)); + return sampleSets.OrderBy(i => i.Key).Select(i => i.Value); + } + + #endregion + + public void Dispose() + { + if (Skin.BeatmapSetResources != null) + Skin.BeatmapSetResources.CacheInvalidated -= InvokeSkinChanged; + Skin.Dispose(); } #region Delegated ISkin implementation diff --git a/osu.Game/Screens/Edit/Setup/FormSampleSetChooser.cs b/osu.Game/Screens/Edit/Setup/FormSampleSetChooser.cs new file mode 100644 index 0000000000..750586dfe3 --- /dev/null +++ b/osu.Game/Screens/Edit/Setup/FormSampleSetChooser.cs @@ -0,0 +1,144 @@ +// 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.Globalization; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Events; +using osu.Framework.Localisation; +using osu.Game.Graphics.UserInterfaceV2; + +namespace osu.Game.Screens.Edit.Setup +{ + public partial class FormSampleSetChooser : FormDropdown, IHasPopover + { + private EditorBeatmapSkin? beatmapSkin; + + public FormSampleSetChooser() + { + Caption = "Custom sample sets"; + } + + [BackgroundDependencyLoader] + private void load(EditorBeatmap editorBeatmap) + { + beatmapSkin = editorBeatmap.BeatmapSkin; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + populateItems(); + if (beatmapSkin != null) + beatmapSkin.BeatmapSkinChanged += populateItems; + + Current.Value = Items.FirstOrDefault(i => i?.SampleSetIndex > 0); + Current.BindValueChanged(val => + { + if (val.NewValue?.SampleSetIndex == -1) + this.ShowPopover(); + }); + } + + private void populateItems() + { + var items = beatmapSkin?.GetAvailableSampleSets().ToList() ?? []; + items.Add(new EditorBeatmapSkin.SampleSet(-1, "Add new...")); + Items = items; + } + + protected override LocalisableString GenerateItemText(EditorBeatmapSkin.SampleSet? item) + { + if (item == null) + return string.Empty; + + return base.GenerateItemText(item); + } + + public Popover GetPopover() => new NewSampleSetPopover( + Items.Any(i => i?.SampleSetIndex > 0) ? Items.Max(i => i!.SampleSetIndex) : 0, + idx => + { + if (idx == null) + { + Current.Value = Items.FirstOrDefault(i => i?.SampleSetIndex > 0); + return; + } + + if (Items.SingleOrDefault(i => i?.SampleSetIndex == idx) is EditorBeatmapSkin.SampleSet existing) + { + Current.Value = existing; + return; + } + + var sampleSet = new EditorBeatmapSkin.SampleSet(idx.Value, $@"Custom #{idx}"); + var newItems = Items.ToList(); + newItems.Insert(newItems.Count - 1, sampleSet); + Items = newItems; + Current.Value = sampleSet; + }); + + protected override void Dispose(bool isDisposing) + { + if (beatmapSkin != null) + beatmapSkin.BeatmapSkinChanged -= populateItems; + + base.Dispose(isDisposing); + } + + private partial class NewSampleSetPopover : OsuPopover + { + private readonly int currentLargestIndex; + private readonly Action onCommit; + + private int? committedIndex; + + private LabelledNumberBox numberBox = null!; + + public NewSampleSetPopover(int currentLargestIndex, Action onCommit) + { + this.currentLargestIndex = currentLargestIndex; + this.onCommit = onCommit; + } + + [BackgroundDependencyLoader] + private void load() + { + Child = numberBox = new LabelledNumberBox + { + RelativeSizeAxes = Axes.None, + Width = 250, + Label = "Sample set index", + Current = { Value = (currentLargestIndex + 1).ToString(CultureInfo.InvariantCulture) } + }; + numberBox.OnCommit += (_, _) => + { + committedIndex = int.Parse(numberBox.Current.Value); + Hide(); + }; + } + + protected override void OnFocus(FocusEvent e) + { + base.OnFocus(e); + // avoids infinite refocus loop + if (committedIndex == null) + GetContainingFocusManager()?.ChangeFocus(numberBox); + } + + public override void Hide() + { + if (State.Value == Visibility.Visible) + onCommit.Invoke(committedIndex > 0 ? committedIndex : null); + base.Hide(); + } + } + } +} diff --git a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs index f52d865d5f..8a55db8420 100644 --- a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs +++ b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs @@ -14,6 +14,7 @@ using osu.Game.Localisation; using osu.Game.Models; using osu.Game.Overlays; using osu.Game.Screens.Backgrounds; +using osu.Game.Screens.Edit.Components; using osu.Game.Utils; namespace osu.Game.Screens.Edit.Setup @@ -23,6 +24,8 @@ namespace osu.Game.Screens.Edit.Setup private FormBeatmapFileSelector audioTrackChooser = null!; private FormBeatmapFileSelector backgroundChooser = null!; + private readonly Bindable currentSampleSet = new Bindable(); + public override LocalisableString Title => EditorSetupStrings.ResourcesHeader; [Resolved] @@ -65,6 +68,27 @@ namespace osu.Game.Screens.Edit.Setup Caption = EditorSetupStrings.AudioTrack, PlaceholderText = EditorSetupStrings.ClickToSelectTrack, }, + new FormSampleSetChooser + { + Current = { BindTarget = currentSampleSet }, + }, + new FormSampleSet + { + Current = { BindTarget = currentSampleSet }, + SampleAddRequested = (file, targetName) => + { + string actualFilename = string.Concat(targetName, file.Extension); + using var stream = file.OpenRead(); + beatmaps.AddFile(working.Value.BeatmapSetInfo, stream, actualFilename); + return actualFilename; + }, + SampleRemoveRequested = filename => + { + var file = working.Value.BeatmapSetInfo.GetFile(filename); + if (file != null) + beatmaps.DeleteFile(working.Value.BeatmapSetInfo, file); + } + }, }; backgroundChooser.PreviewContainer.Add(headerBackground); diff --git a/osu.Game/Screens/Edit/Timing/EffectSection.cs b/osu.Game/Screens/Edit/Timing/EffectSection.cs index f9ef460232..325d0cfaf6 100644 --- a/osu.Game/Screens/Edit/Timing/EffectSection.cs +++ b/osu.Game/Screens/Edit/Timing/EffectSection.cs @@ -11,21 +11,24 @@ namespace osu.Game.Screens.Edit.Timing { internal partial class EffectSection : Section { - private LabelledSwitchButton kiai = null!; + private FormCheckBox kiai = null!; - private SliderWithTextBoxInput scrollSpeedSlider = null!; + private FormSliderBar scrollSpeedSlider { get; set; } = null!; [BackgroundDependencyLoader] private void load() { Flow.AddRange(new Drawable[] { - kiai = new LabelledSwitchButton { Label = "Kiai Time" }, - scrollSpeedSlider = new SliderWithTextBoxInput("Scroll Speed") + kiai = new FormCheckBox { Caption = "Kiai Time" }, + scrollSpeedSlider = new FormSliderBar { + Caption = "Scroll Speed", Current = new EffectControlPoint().ScrollSpeedBindable, - KeyboardStep = 0.1f - } + KeyboardStep = 0.1f, + TransferValueOnCommit = true, + TabbableContentContainer = this + }, }); } diff --git a/osu.Game/Screens/Edit/Timing/IndeterminateSliderWithTextBoxInput.cs b/osu.Game/Screens/Edit/Timing/IndeterminateSliderWithTextBoxInput.cs index 00cf2e3493..0e7b4767a7 100644 --- a/osu.Game/Screens/Edit/Timing/IndeterminateSliderWithTextBoxInput.cs +++ b/osu.Game/Screens/Edit/Timing/IndeterminateSliderWithTextBoxInput.cs @@ -17,7 +17,7 @@ using Vector2 = osuTK.Vector2; namespace osu.Game.Screens.Edit.Timing { /// - /// Analogous to , but supports scenarios + /// Analogous to SliderWithTextBoxInput, but supports scenarios /// where multiple objects with multiple different property values are selected /// by providing an "indeterminate state". /// diff --git a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs index 47629981f1..5a68781397 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs @@ -10,6 +10,7 @@ using osu.Game.Beatmaps; using osu.Game.Collections; using osu.Game.Database; using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Localisation; using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; @@ -69,10 +70,14 @@ namespace osu.Game.Screens.OnlinePlay.Playlists countAfter = c.BeatmapMD5Hashes.Count; }).ContinueWith(_ => Schedule(() => { + LocalisableString message; + if (countBefore == 0) - notifications?.Post(new SimpleNotification { Text = $"Created new collection \"{room.Name}\" with {countAfter} beatmaps." }); + message = NotificationsStrings.CollectionCreated(room.Name, countAfter); else - notifications?.Post(new SimpleNotification { Text = $"Added {countAfter - countBefore} beatmaps to collection \"{room.Name}\"." }); + message = NotificationsStrings.CollectionBeatmapsAdded(room.Name, countAfter - countBefore); + + notifications?.Post(new SimpleNotification { Text = message }); })); }; } diff --git a/osu.Game/Screens/Select/ModSpeedHotkeyHandler.cs b/osu.Game/Screens/Select/ModSpeedHotkeyHandler.cs index 998f94849c..6d9f58bcfd 100644 --- a/osu.Game/Screens/Select/ModSpeedHotkeyHandler.cs +++ b/osu.Game/Screens/Select/ModSpeedHotkeyHandler.cs @@ -8,7 +8,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Utils; using osu.Game.Configuration; -using osu.Game.Input; using osu.Game.Overlays; using osu.Game.Overlays.OSD; using osu.Game.Rulesets.Mods; @@ -21,9 +20,6 @@ namespace osu.Game.Screens.Select [Resolved] private Bindable> selectedMods { get; set; } = null!; - [Resolved] - private RealmKeyBindingStore keyBindingStore { get; set; } = null!; - [Resolved] private OnScreenDisplay? onScreenDisplay { get; set; } @@ -56,7 +52,7 @@ namespace osu.Game.Screens.Select if (Precision.AlmostEquals(targetSpeed, 1, 0.005)) { selectedMods.Value = selectedMods.Value.Where(m => m is not ModRateAdjust).ToList(); - onScreenDisplay?.Display(new SpeedChangeToast(keyBindingStore, targetSpeed)); + onScreenDisplay?.Display(new SpeedChangeToast(targetSpeed)); return true; } @@ -109,7 +105,7 @@ namespace osu.Game.Screens.Select return false; selectedMods.Value = intendedMods; - onScreenDisplay?.Display(new SpeedChangeToast(keyBindingStore, targetMod.SpeedChange.Value)); + onScreenDisplay?.Display(new SpeedChangeToast(targetMod.SpeedChange.Value)); return true; } } diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge.DifficultyDisplay.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge.DifficultyDisplay.cs index 65e55ff277..fc6ce0ac20 100644 --- a/osu.Game/Screens/SelectV2/BeatmapTitleWedge.DifficultyDisplay.cs +++ b/osu.Game/Screens/SelectV2/BeatmapTitleWedge.DifficultyDisplay.cs @@ -206,8 +206,13 @@ namespace osu.Game.Screens.SelectV2 { base.LoadComplete(); - beatmap.BindValueChanged(_ => updateDisplay()); - ruleset.BindValueChanged(_ => updateDisplay()); + // it is not uncommon for the beatmap and the ruleset to change in conjunction during a single update frame. + // in that process, it is possible for the global bindable triad (beatmap / ruleset / mods) to briefly be partially invalid in combination (e.g. mods invalid for given ruleset). + // `updateDisplay()` will initiate a difficulty calculation, and if it is allowed to run in that invalid intermediate state, it will loudly fail. + // therefore, all changes that may initiate a difficulty calculation are debounced until the next frame to ensure the global bindable state is fully consistent - + // and it's what you'd want to do anyway for performance reasons. + beatmap.BindValueChanged(_ => Scheduler.AddOnce(updateDisplay)); + ruleset.BindValueChanged(_ => Scheduler.AddOnce(updateDisplay)); mods.BindValueChanged(m => { diff --git a/osu.Game/Skinning/LegacyBeatmapSkin.cs b/osu.Game/Skinning/LegacyBeatmapSkin.cs index e198d43be7..365652e6ad 100644 --- a/osu.Game/Skinning/LegacyBeatmapSkin.cs +++ b/osu.Game/Skinning/LegacyBeatmapSkin.cs @@ -26,6 +26,8 @@ namespace osu.Game.Skinning // 2. https://github.com/peppy/osu-stable-reference/blob/dc0994645801010d4b628fff5ff79cd3c286ca83/osu!/Graphics/Textures/TextureManager.cs#L158-L196 (user skin textures lookup) protected override bool AllowHighResolutionSprites => false; + public RealmBackedResourceStore? BeatmapSetResources => FallbackStore as RealmBackedResourceStore; + /// /// Construct a new legacy beatmap skin instance. /// diff --git a/osu.Game/Skinning/RealmBackedResourceStore.cs b/osu.Game/Skinning/RealmBackedResourceStore.cs index 0932485349..36561e0d69 100644 --- a/osu.Game/Skinning/RealmBackedResourceStore.cs +++ b/osu.Game/Skinning/RealmBackedResourceStore.cs @@ -16,6 +16,8 @@ namespace osu.Game.Skinning public class RealmBackedResourceStore : ResourceStore where T : RealmObject, IHasRealmFiles, IHasGuidPrimaryKey { + public event Action? CacheInvalidated; + private Lazy> fileToStoragePathMapping; private readonly Live liveSource; @@ -56,7 +58,11 @@ namespace osu.Game.Skinning private string? getPathForFile(string filename) => fileToStoragePathMapping.Value.GetValueOrDefault(filename.ToLowerInvariant()); - private void invalidateCache() => fileToStoragePathMapping = new Lazy>(initialiseFileCache); + private void invalidateCache() + { + fileToStoragePathMapping = new Lazy>(initialiseFileCache); + CacheInvalidated?.Invoke(); + } private Dictionary initialiseFileCache() => liveSource.PerformRead(source => { diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index 3a0d0b16e5..3affd5afa6 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -63,6 +63,8 @@ namespace osu.Game.Skinning public string Name { get; } + protected IResourceStore? FallbackStore { get; } + /// /// Construct a new skin. /// @@ -102,6 +104,7 @@ namespace osu.Game.Skinning SkinInfo = skin.ToLiveUnmanaged(); } + FallbackStore = fallbackStore; if (fallbackStore != null) store.AddStore(fallbackStore); @@ -339,6 +342,7 @@ namespace osu.Game.Skinning Textures?.Dispose(); Samples?.Dispose(); + FallbackStore?.Dispose(); store.Dispose(); }