From 970388d4e22ca71b05021f7ee506a29313cfcc14 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 Feb 2023 17:35:12 +0900 Subject: [PATCH 01/43] Move `Overlays` container to accept input and be frame-stable --- osu.Game/Rulesets/UI/DrawableRuleset.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index 71b452c309..c49f2d003f 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -184,11 +184,13 @@ namespace osu.Game.Rulesets.UI { RelativeSizeAxes = Axes.Both, Child = KeyBindingInputManager - .WithChild(CreatePlayfieldAdjustmentContainer() - .WithChild(Playfield) - ), + .WithChildren(new Drawable[] + { + CreatePlayfieldAdjustmentContainer() + .WithChild(Playfield), + Overlays + }), }, - Overlays, } }; From b42b5f97cfc5944b3a3d84d008c88e000d939cad Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 Feb 2023 17:36:01 +0900 Subject: [PATCH 02/43] Use `Overlays` container rather than `KeyBindingInputManager` for flashlight --- osu.Game/Rulesets/Mods/ModFlashlight.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/ModFlashlight.cs b/osu.Game/Rulesets/Mods/ModFlashlight.cs index 45fa55c7f2..9fe0864c7a 100644 --- a/osu.Game/Rulesets/Mods/ModFlashlight.cs +++ b/osu.Game/Rulesets/Mods/ModFlashlight.cs @@ -82,7 +82,7 @@ namespace osu.Game.Rulesets.Mods flashlight.Colour = Color4.Black; flashlight.Combo.BindTo(Combo); - drawableRuleset.KeyBindingInputManager.Add(flashlight); + drawableRuleset.Overlays.Add(flashlight); } protected abstract Flashlight CreateFlashlight(); From 5ec5222d8acd2f902689e0b0e0eeffd697e46d2a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 Feb 2023 17:35:41 +0900 Subject: [PATCH 03/43] Expose and consume `OsuInputManager` explicitly --- osu.Game.Rulesets.Osu/Mods/InputBlockingMod.cs | 7 ++++--- osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs | 3 ++- osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs | 3 ++- osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs | 4 +++- osu.Game/Rulesets/UI/DrawableRuleset.cs | 2 +- 5 files changed, 12 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/InputBlockingMod.cs b/osu.Game.Rulesets.Osu/Mods/InputBlockingMod.cs index 7db4e2625b..b76b8d8cf5 100644 --- a/osu.Game.Rulesets.Osu/Mods/InputBlockingMod.cs +++ b/osu.Game.Rulesets.Osu/Mods/InputBlockingMod.cs @@ -11,6 +11,7 @@ using osu.Game.Beatmaps.Timing; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Screens.Play; @@ -26,7 +27,7 @@ namespace osu.Game.Rulesets.Osu.Mods private const double flash_duration = 1000; - private DrawableRuleset ruleset = null!; + private DrawableOsuRuleset ruleset = null!; protected OsuAction? LastAcceptedAction { get; private set; } @@ -42,8 +43,8 @@ namespace osu.Game.Rulesets.Osu.Mods public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { - ruleset = drawableRuleset; - drawableRuleset.KeyBindingInputManager.Add(new InputInterceptor(this)); + ruleset = (DrawableOsuRuleset)drawableRuleset; + ruleset.InputManager.Add(new InputInterceptor(this)); var periods = new List(); diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs index 6772cfe0be..909238954a 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs @@ -11,6 +11,7 @@ using osu.Game.Graphics; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Replays; +using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Osu.Mods @@ -55,7 +56,7 @@ namespace osu.Game.Rulesets.Osu.Mods public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { // Grab the input manager to disable the user's cursor, and for future use - inputManager = (OsuInputManager)drawableRuleset.KeyBindingInputManager; + inputManager = ((DrawableOsuRuleset)drawableRuleset).InputManager; inputManager.AllowUserCursorMovement = false; // Generate the replay frames the cursor should follow diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs index 753de6231a..4feb0f9537 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs @@ -10,6 +10,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.UI; using osu.Game.Screens.Play; @@ -42,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.Mods public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { // grab the input manager for future use. - osuInputManager = (OsuInputManager)drawableRuleset.KeyBindingInputManager; + osuInputManager = ((DrawableOsuRuleset)drawableRuleset).InputManager; } public void ApplyToPlayer(Player player) diff --git a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs index d1fa0d09cc..4ad95a8012 100644 --- a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs @@ -26,6 +26,8 @@ namespace osu.Game.Rulesets.Osu.UI { protected new OsuRulesetConfigManager Config => (OsuRulesetConfigManager)base.Config; + public OsuInputManager InputManager { get; private set; } + public new OsuPlayfield Playfield => (OsuPlayfield)base.Playfield; public DrawableOsuRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null) @@ -39,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.UI protected override Playfield CreatePlayfield() => new OsuPlayfield(); - protected override PassThroughInputManager CreateInputManager() => new OsuInputManager(Ruleset.RulesetInfo); + protected override PassThroughInputManager CreateInputManager() => InputManager = new OsuInputManager(Ruleset.RulesetInfo); public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new OsuPlayfieldAdjustmentContainer { AlignWithStoryboard = true }; diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index c49f2d003f..11b238480a 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.UI /// /// The key conversion input manager for this DrawableRuleset. /// - public PassThroughInputManager KeyBindingInputManager; + protected PassThroughInputManager KeyBindingInputManager; public override double GameplayStartTime => Objects.FirstOrDefault()?.StartTime - 2000 ?? 0; From c540d78fbc05065d859a298ebb2c4d717e3fb944 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 Feb 2023 18:10:25 +0900 Subject: [PATCH 04/43] Expose the actual `KeyBindingInputManager` Turns out that `CreateInputManager` is called more than once, and some mods (ie. `InputBlockingMod`) rely on consuming the "main" one. So let's go back to accessing and exposing in `DrawableOsuRuleset` rather than storing out own reference. --- osu.Game.Rulesets.Osu/Mods/InputBlockingMod.cs | 2 +- osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs | 2 +- osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs | 2 +- osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/InputBlockingMod.cs b/osu.Game.Rulesets.Osu/Mods/InputBlockingMod.cs index b76b8d8cf5..b56fdbdf74 100644 --- a/osu.Game.Rulesets.Osu/Mods/InputBlockingMod.cs +++ b/osu.Game.Rulesets.Osu/Mods/InputBlockingMod.cs @@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Osu.Mods public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { ruleset = (DrawableOsuRuleset)drawableRuleset; - ruleset.InputManager.Add(new InputInterceptor(this)); + ruleset.KeyBindingInputManager.Add(new InputInterceptor(this)); var periods = new List(); diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs index 909238954a..d39ca06da3 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs @@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Osu.Mods public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { // Grab the input manager to disable the user's cursor, and for future use - inputManager = ((DrawableOsuRuleset)drawableRuleset).InputManager; + inputManager = ((DrawableOsuRuleset)drawableRuleset).KeyBindingInputManager; inputManager.AllowUserCursorMovement = false; // Generate the replay frames the cursor should follow diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs index 4feb0f9537..32ffb545e0 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs @@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.Mods public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { // grab the input manager for future use. - osuInputManager = ((DrawableOsuRuleset)drawableRuleset).InputManager; + osuInputManager = ((DrawableOsuRuleset)drawableRuleset).KeyBindingInputManager; } public void ApplyToPlayer(Player player) diff --git a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs index 4ad95a8012..c3efd48053 100644 --- a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.UI { protected new OsuRulesetConfigManager Config => (OsuRulesetConfigManager)base.Config; - public OsuInputManager InputManager { get; private set; } + public new OsuInputManager KeyBindingInputManager => (OsuInputManager)base.KeyBindingInputManager; public new OsuPlayfield Playfield => (OsuPlayfield)base.Playfield; @@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.UI protected override Playfield CreatePlayfield() => new OsuPlayfield(); - protected override PassThroughInputManager CreateInputManager() => InputManager = new OsuInputManager(Ruleset.RulesetInfo); + protected override PassThroughInputManager CreateInputManager() => new OsuInputManager(Ruleset.RulesetInfo); public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new OsuPlayfieldAdjustmentContainer { AlignWithStoryboard = true }; From d653335b6f22faae7952204d8fe807233f53f01b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 15 Feb 2023 19:26:44 +0900 Subject: [PATCH 05/43] Add basic skin editor clipboard implementation --- osu.Game/Overlays/SkinEditor/SkinEditor.cs | 89 ++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index 133ec10202..9c5d6677b9 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -7,6 +7,7 @@ using System.Diagnostics; using System.IO; using System.Linq; using System.Threading.Tasks; +using Newtonsoft.Json; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -48,6 +49,9 @@ namespace osu.Game.Overlays.SkinEditor private Bindable currentSkin = null!; + [Cached] + public readonly EditorClipboard Clipboard = new EditorClipboard(); + [Resolved] private OsuGame? game { get; set; } @@ -78,6 +82,15 @@ namespace osu.Game.Overlays.SkinEditor private EditorMenuItem undoMenuItem = null!; private EditorMenuItem redoMenuItem = null!; + private EditorMenuItem cutMenuItem = null!; + private EditorMenuItem copyMenuItem = null!; + private EditorMenuItem cloneMenuItem = null!; + private EditorMenuItem pasteMenuItem = null!; + + private readonly BindableWithCurrent canCut = new BindableWithCurrent(); + private readonly BindableWithCurrent canCopy = new BindableWithCurrent(); + private readonly BindableWithCurrent canPaste = new BindableWithCurrent(); + [Resolved] private OnScreenDisplay? onScreenDisplay { get; set; } @@ -143,6 +156,11 @@ namespace osu.Game.Overlays.SkinEditor { undoMenuItem = new EditorMenuItem(CommonStrings.Undo, MenuItemType.Standard, Undo), redoMenuItem = new EditorMenuItem(CommonStrings.Redo, MenuItemType.Standard, Redo), + new EditorMenuItemSpacer(), + cutMenuItem = new EditorMenuItem(CommonStrings.Cut, MenuItemType.Standard, Cut), + copyMenuItem = new EditorMenuItem(CommonStrings.Copy, MenuItemType.Standard, Copy), + pasteMenuItem = new EditorMenuItem(CommonStrings.Paste, MenuItemType.Standard, Paste), + cloneMenuItem = new EditorMenuItem(CommonStrings.Clone, MenuItemType.Standard, Clone), } }, } @@ -201,6 +219,21 @@ namespace osu.Game.Overlays.SkinEditor { base.LoadComplete(); + canCut.Current.BindValueChanged(cut => cutMenuItem.Action.Disabled = !cut.NewValue, true); + canCopy.Current.BindValueChanged(copy => + { + copyMenuItem.Action.Disabled = !copy.NewValue; + cloneMenuItem.Action.Disabled = !copy.NewValue; + }, true); + canPaste.Current.BindValueChanged(paste => pasteMenuItem.Action.Disabled = !paste.NewValue, true); + + SelectedComponents.BindCollectionChanged((_, _) => + { + canCopy.Value = canCut.Value = SelectedComponents.Any(); + }, true); + + Clipboard.Content.BindValueChanged(content => canPaste.Value = !string.IsNullOrEmpty(content.NewValue), true); + Show(); game?.RegisterImportHandler(this); @@ -224,6 +257,18 @@ namespace osu.Game.Overlays.SkinEditor { switch (e.Action) { + case PlatformAction.Cut: + Cut(); + return true; + + case PlatformAction.Copy: + Copy(); + return true; + + case PlatformAction.Paste: + Paste(); + return true; + case PlatformAction.Undo: Undo(); return true; @@ -361,6 +406,50 @@ namespace osu.Game.Overlays.SkinEditor } } + protected void Cut() + { + Copy(); + DeleteItems(SelectedComponents.ToArray()); + } + + protected void Copy() + { + Clipboard.Content.Value = JsonConvert.SerializeObject(SelectedComponents.Cast().Select(s => s.CreateSerialisedInfo()).ToArray()); + } + + protected void Clone() + { + // Avoid attempting to clone if copying is not available (as it may result in pasting something unexpected). + if (!canCopy.Value) + return; + + // This is an initial implementation just to get an idea of how people used this function. + // There are a couple of differences from osu!stable's implementation which will require more work to match: + // - The "clipboard" is not populated during the duplication process. + // - The duplicated hitobjects are inserted after the original pattern (add one beat_length and then quantize using beat snap). + // - The duplicated hitobjects are selected (but this is also applied for all paste operations so should be changed there). + Copy(); + Paste(); + } + + protected void Paste() + { + var drawableInfo = JsonConvert.DeserializeObject(Clipboard.Content.Value); + + if (drawableInfo == null) + return; + + var instances = drawableInfo.Select(d => d.CreateInstance()) + .OfType() + .ToArray(); + + foreach (var i in instances) + placeComponent(i); + + SelectedComponents.Clear(); + SelectedComponents.AddRange(instances); + } + protected void Undo() => changeHandler?.RestoreState(-1); protected void Redo() => changeHandler?.RestoreState(1); From bcf2555545b6250432ffa9b51906f2cc6d6d6b13 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 15 Feb 2023 19:34:42 +0900 Subject: [PATCH 06/43] Fix components having incorrect default positions --- osu.Game/Overlays/SkinEditor/SkinEditor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index 9c5d6677b9..5adbe7273c 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -444,7 +444,7 @@ namespace osu.Game.Overlays.SkinEditor .ToArray(); foreach (var i in instances) - placeComponent(i); + placeComponent(i, false); SelectedComponents.Clear(); SelectedComponents.AddRange(instances); From bc83b0c2642936246e3482ff777474190a3f55f8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 15 Feb 2023 19:35:22 +0900 Subject: [PATCH 07/43] Fix clipboard changes not batching as undo steps --- osu.Game/Overlays/SkinEditor/SkinEditor.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index 5adbe7273c..5d061a5851 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -434,7 +434,9 @@ namespace osu.Game.Overlays.SkinEditor protected void Paste() { - var drawableInfo = JsonConvert.DeserializeObject(Clipboard.Content.Value); + changeHandler?.BeginChange(); + + var drawableInfo = JsonConvert.DeserializeObject(clipboard.Content.Value); if (drawableInfo == null) return; @@ -448,6 +450,8 @@ namespace osu.Game.Overlays.SkinEditor SelectedComponents.Clear(); SelectedComponents.AddRange(instances); + + changeHandler?.EndChange(); } protected void Undo() => changeHandler?.RestoreState(-1); @@ -491,8 +495,12 @@ namespace osu.Game.Overlays.SkinEditor public void DeleteItems(ISerialisableDrawable[] items) { + changeHandler?.BeginChange(); + foreach (var item in items) availableTargets.FirstOrDefault(t => t.Components.Contains(item))?.Remove(item); + + changeHandler?.EndChange(); } #region Drag & drop import handling From 925deb7ca548b7ed3476126af11740b5cb1cc2b8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 15 Feb 2023 19:35:37 +0900 Subject: [PATCH 08/43] Make skin editor clipboard shared between screens and skins to allow moving elements over --- osu.Game/Overlays/SkinEditor/SkinEditor.cs | 10 +++++----- osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs | 4 ++++ 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index 5d061a5851..ad07099d48 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -49,9 +49,6 @@ namespace osu.Game.Overlays.SkinEditor private Bindable currentSkin = null!; - [Cached] - public readonly EditorClipboard Clipboard = new EditorClipboard(); - [Resolved] private OsuGame? game { get; set; } @@ -64,6 +61,9 @@ namespace osu.Game.Overlays.SkinEditor [Resolved] private RealmAccess realm { get; set; } = null!; + [Resolved] + private EditorClipboard clipboard { get; set; } = null!; + [Resolved] private SkinEditorOverlay? skinEditorOverlay { get; set; } @@ -232,7 +232,7 @@ namespace osu.Game.Overlays.SkinEditor canCopy.Value = canCut.Value = SelectedComponents.Any(); }, true); - Clipboard.Content.BindValueChanged(content => canPaste.Value = !string.IsNullOrEmpty(content.NewValue), true); + clipboard.Content.BindValueChanged(content => canPaste.Value = !string.IsNullOrEmpty(content.NewValue), true); Show(); @@ -414,7 +414,7 @@ namespace osu.Game.Overlays.SkinEditor protected void Copy() { - Clipboard.Content.Value = JsonConvert.SerializeObject(SelectedComponents.Cast().Select(s => s.CreateSerialisedInfo()).ToArray()); + clipboard.Content.Value = JsonConvert.SerializeObject(SelectedComponents.Cast().Select(s => s.CreateSerialisedInfo()).ToArray()); } protected void Clone() diff --git a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs index c87e60e47f..1c0ece28fe 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs @@ -11,6 +11,7 @@ using osu.Framework.Input.Events; using osu.Game.Graphics.Containers; using osu.Game.Input.Bindings; using osu.Game.Screens; +using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Components; using osuTK; @@ -28,6 +29,9 @@ namespace osu.Game.Overlays.SkinEditor private SkinEditor? skinEditor; + [Cached] + public readonly EditorClipboard Clipboard = new EditorClipboard(); + [Resolved] private OsuGame game { get; set; } = null!; From 2fbaf88a3c8edd6f629b18a81df33acc96138cc4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 16 Feb 2023 15:24:37 +0900 Subject: [PATCH 09/43] Add clipboard dependency to `SkinEditor` specific tests This is usually provided by the `SkinEditorOverlay`, which is not always present in tests. --- osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs | 4 ++++ .../Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs index 62fdb67a30..2f20d75813 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs @@ -12,6 +12,7 @@ using osu.Game.Overlays.Settings; using osu.Game.Overlays.SkinEditor; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; +using osu.Game.Screens.Edit; using osu.Game.Screens.Play.HUD.HitErrorMeters; using osu.Game.Skinning; using osuTK.Input; @@ -27,6 +28,9 @@ namespace osu.Game.Tests.Visual.Gameplay [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue); + [Cached] + public readonly EditorClipboard Clipboard = new EditorClipboard(); + [SetUpSteps] public override void SetUpSteps() { diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs index d5b6ac38cb..a7da8f9832 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs @@ -12,6 +12,7 @@ using osu.Game.Overlays.SkinEditor; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Scoring; +using osu.Game.Screens.Edit; using osu.Game.Screens.Play; using osu.Game.Tests.Gameplay; using osuTK.Input; @@ -32,6 +33,9 @@ namespace osu.Game.Tests.Visual.Gameplay [Cached(typeof(IGameplayClock))] private readonly IGameplayClock gameplayClock = new GameplayClockContainer(new FramedClock()); + [Cached] + public readonly EditorClipboard Clipboard = new EditorClipboard(); + [SetUpSteps] public void SetUpSteps() { From b59ec551f6179d204cf3ee14219e8fe101e2844e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 16 Feb 2023 18:13:11 +0900 Subject: [PATCH 10/43] Add test coverage of `GameplaySampleTriggerSource` not considering nested objects --- .../TestSceneGameplaySampleTriggerSource.cs | 41 ++++++++++++++++--- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs index e7bdf7b9ba..86dfce438a 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs @@ -10,13 +10,16 @@ using osu.Framework.Timing; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Beatmaps.Legacy; using osu.Game.Rulesets; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.UI; using osu.Game.Storyboards; +using osuTK; using osuTK.Input; namespace osu.Game.Tests.Visual.Gameplay @@ -36,13 +39,16 @@ namespace osu.Game.Tests.Visual.Gameplay protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) { + ControlPointInfo controlPointInfo = new LegacyControlPointInfo(); + beatmap = new Beatmap { BeatmapInfo = new BeatmapInfo { Difficulty = new BeatmapDifficulty { CircleSize = 6, SliderMultiplier = 3 }, Ruleset = ruleset - } + }, + ControlPointInfo = controlPointInfo }; const double start_offset = 8000; @@ -51,7 +57,7 @@ namespace osu.Game.Tests.Visual.Gameplay // intentionally start objects a bit late so we can test the case of no alive objects. double t = start_offset; - beatmap.HitObjects.AddRange(new[] + beatmap.HitObjects.AddRange(new HitObject[] { new HitCircle { @@ -71,12 +77,24 @@ namespace osu.Game.Tests.Visual.Gameplay }, new HitCircle { - StartTime = t + spacing, + StartTime = t += spacing, + }, + new Slider + { + StartTime = t += spacing, + Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, Vector2.UnitY * 200 }), Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_WHISTLE) }, SampleControlPoint = new SampleControlPoint { SampleBank = "soft" }, }, }); + // Add a change in volume halfway through final slider. + controlPointInfo.Add(t, new SampleControlPoint + { + SampleBank = "normal", + SampleVolume = 20, + }); + return beatmap; } @@ -128,10 +146,23 @@ namespace osu.Game.Tests.Visual.Gameplay waitForAliveObjectIndex(3); checkValidObjectIndex(3); - AddStep("Seek into future", () => Beatmap.Value.Track.Seek(beatmap.HitObjects.Last().GetEndTime() + 10000)); + seekBeforeIndex(4); + waitForAliveObjectIndex(4); + // Even before the object, we should prefer the first nested object's sample. + // This is because the (parent) object will only play its sample at the final EndTime. + AddAssert("check valid object is slider's first nested", () => sampleTriggerSource.GetMostValidObject(), () => Is.EqualTo(beatmap.HitObjects[4].NestedHitObjects.First())); + + AddStep("seek to just after slider", () => Beatmap.Value.Track.Seek(beatmap.HitObjects[4].GetEndTime() + 100)); + AddUntilStep("wait until valid object is slider's last nested", () => sampleTriggerSource.GetMostValidObject(), () => Is.EqualTo(beatmap.HitObjects[4].NestedHitObjects.Last())); + + // After we get far enough away, the samples of the object itself should be used, not any nested object. + AddStep("seek to further after slider", () => Beatmap.Value.Track.Seek(beatmap.HitObjects[4].GetEndTime() + 1000)); + AddUntilStep("wait until valid object is slider itself", () => sampleTriggerSource.GetMostValidObject(), () => Is.EqualTo(beatmap.HitObjects[4])); + + AddStep("Seek into future", () => Beatmap.Value.Track.Seek(beatmap.HitObjects.Last().GetEndTime() + 10000)); waitForAliveObjectIndex(null); - checkValidObjectIndex(3); + checkValidObjectIndex(4); } private void seekBeforeIndex(int index) => From affa9507a1549e0d7393cd244ebc5b1b5195c1b7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 16 Feb 2023 17:34:38 +0900 Subject: [PATCH 11/43] Fix `GameplaySampleTriggerSource` not considering nested objects when determining the best sample to play --- .../UI/GameplaySampleTriggerSource.cs | 67 ++++++++++++++----- 1 file changed, 51 insertions(+), 16 deletions(-) diff --git a/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs b/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs index d2244df3b8..de16cc05c7 100644 --- a/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs +++ b/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs @@ -7,6 +7,7 @@ using System.Linq; using osu.Framework.Graphics.Containers; using osu.Game.Audio; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Skinning; namespace osu.Game.Rulesets.UI @@ -68,27 +69,61 @@ namespace osu.Game.Rulesets.UI protected HitObject GetMostValidObject() { // The most optimal lookup case we have is when an object is alive. There are usually very few alive objects so there's no drawbacks in attempting this lookup each time. - var hitObject = hitObjectContainer.AliveObjects.FirstOrDefault(h => h.Result?.HasResult != true)?.HitObject; + var drawableHitObject = hitObjectContainer.AliveObjects.FirstOrDefault(h => h.Result?.HasResult != true); - // In the case a next object isn't available in drawable form, we need to do a somewhat expensive traversal to get a valid sound to play. - if (hitObject == null) + if (drawableHitObject != null) { - // This lookup can be skipped if the last entry is still valid (in the future and not yet hit). - if (fallbackObject == null || fallbackObject.Result?.HasResult == true) - { - // We need to use lifetime entries to find the next object (we can't just use `hitObjectContainer.Objects` due to pooling - it may even be empty). - // If required, we can make this lookup more efficient by adding support to get next-future-entry in LifetimeEntryManager. - fallbackObject = hitObjectContainer.Entries - .Where(e => e.Result?.HasResult != true).MinBy(e => e.HitObject.StartTime); + // A hit object may have a more valid nested object. + drawableHitObject = getMostValidNestedDrawable(drawableHitObject); - // In the case there are no unjudged objects, the last hit object should be used instead. - fallbackObject ??= hitObjectContainer.Entries.LastOrDefault(); - } - - hitObject = fallbackObject?.HitObject; + return drawableHitObject.HitObject; } - return hitObject; + // In the case a next object isn't available in drawable form, we need to do a somewhat expensive traversal to get a valid sound to play. + // This lookup can be skipped if the last entry is still valid (in the future and not yet hit). + if (fallbackObject == null || fallbackObject.Result?.HasResult == true) + { + // We need to use lifetime entries to find the next object (we can't just use `hitObjectContainer.Objects` due to pooling - it may even be empty). + // If required, we can make this lookup more efficient by adding support to get next-future-entry in LifetimeEntryManager. + fallbackObject = hitObjectContainer.Entries + .Where(e => e.Result?.HasResult != true).MinBy(e => e.HitObject.StartTime); + + if (fallbackObject != null) + return fallbackObject.HitObject; + + // In the case there are no unjudged objects, the last hit object should be used instead. + fallbackObject ??= hitObjectContainer.Entries.LastOrDefault(); + } + + if (fallbackObject == null) + return null; + + bool fallbackHasResult = fallbackObject.Result?.HasResult == true; + + // If the fallback has been judged then we want the sample from the object itself. + if (fallbackHasResult) + return fallbackObject.HitObject; + + // Else we want the earliest (including nested). + // In cases of nested objects, they will always have earlier sample data than their parent object. + return getEarliestNestedObject(fallbackObject.HitObject); + } + + private DrawableHitObject getMostValidNestedDrawable(DrawableHitObject o) + { + var nestedWithoutResult = o.NestedHitObjects.FirstOrDefault(n => n.Result?.HasResult != true); + + if (nestedWithoutResult == null) + return o; + + return getMostValidNestedDrawable(nestedWithoutResult); + } + + private HitObject getEarliestNestedObject(HitObject hitObject) + { + var nested = hitObject.NestedHitObjects.FirstOrDefault(); + + return nested != null ? getEarliestNestedObject(nested) : hitObject; } private SkinnableSound getNextSample() From 19d5293ad1f69a483a7d87df7feecad425589344 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Feb 2023 18:59:31 +0900 Subject: [PATCH 12/43] Change early return to also find the earliest nested object --- osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs b/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs index de16cc05c7..e1c03e49e3 100644 --- a/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs +++ b/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs @@ -89,9 +89,9 @@ namespace osu.Game.Rulesets.UI .Where(e => e.Result?.HasResult != true).MinBy(e => e.HitObject.StartTime); if (fallbackObject != null) - return fallbackObject.HitObject; + return getEarliestNestedObject(fallbackObject.HitObject); - // In the case there are no unjudged objects, the last hit object should be used instead. + // In the case there are no non-judged objects, the last hit object should be used instead. fallbackObject ??= hitObjectContainer.Entries.LastOrDefault(); } From c03b6cec2317c939091cce7880a3d32282d1192b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Feb 2023 15:01:11 +0900 Subject: [PATCH 13/43] Add `IEquatable` and `ToString` support to `SkinComponentsContainerLookup` --- .../Skinning/SkinComponentsContainerLookup.cs | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/osu.Game/Skinning/SkinComponentsContainerLookup.cs b/osu.Game/Skinning/SkinComponentsContainerLookup.cs index f6c462ddaa..9256c1b547 100644 --- a/osu.Game/Skinning/SkinComponentsContainerLookup.cs +++ b/osu.Game/Skinning/SkinComponentsContainerLookup.cs @@ -1,6 +1,9 @@ // 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.ComponentModel; +using osu.Framework.Extensions; using osu.Game.Rulesets; namespace osu.Game.Skinning @@ -8,7 +11,7 @@ namespace osu.Game.Skinning /// /// Represents a lookup of a collection of elements that make up a particular skinnable of the game. /// - public class SkinComponentsContainerLookup : ISkinComponentLookup + public class SkinComponentsContainerLookup : ISkinComponentLookup, IEquatable { /// /// The target area / layer of the game for which skin components will be returned. @@ -25,12 +28,44 @@ namespace osu.Game.Skinning Ruleset = ruleset; } + public override string ToString() + { + if (Ruleset == null) return Target.GetDescription(); + + return $"{Target.GetDescription()} (\"{Ruleset.Name}\" only)"; + } + + public bool Equals(SkinComponentsContainerLookup? other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + + return Target == other.Target && (ReferenceEquals(Ruleset, other.Ruleset) || Ruleset?.Equals(other.Ruleset) == true); + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + + return Equals((SkinComponentsContainerLookup)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine((int)Target, Ruleset); + } + /// /// Represents a particular area or part of a game screen whose layout can be customised using the skin editor. /// public enum TargetArea { + [Description("HUD")] MainHUDComponents, + + [Description("Song select")] SongSelect } } From ba5a87ca048bfcf78c82071cf1eda951e31d0048 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Feb 2023 15:02:42 +0900 Subject: [PATCH 14/43] Add basic target layer selection in skin editor --- osu.Game/Overlays/SkinEditor/SkinEditor.cs | 29 +++++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index 133ec10202..fccddac84f 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -24,6 +24,7 @@ using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; using osu.Game.Overlays.OSD; +using osu.Game.Overlays.Settings; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Components; using osu.Game.Screens.Edit.Components.Menus; @@ -66,6 +67,8 @@ namespace osu.Game.Overlays.SkinEditor [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue); + private readonly Bindable selectedTarget = new Bindable(); + private bool hasBegunMutating; private Container? content; @@ -271,9 +274,27 @@ namespace osu.Game.Overlays.SkinEditor content.Child = new SkinBlueprintContainer(targetScreen); - componentsSidebar.Child = new SkinComponentToolbox(getFirstTarget() as CompositeDrawable) + selectedTarget.Default = getFirstTarget()?.Lookup; + if (!availableTargets.Any(t => t.Lookup.Equals(selectedTarget.Value))) + selectedTarget.Value = getFirstTarget()?.Lookup; + + componentsSidebar.Children = new[] { - RequestPlacement = placeComponent + new EditorSidebarSection("Current working layer") + { + Children = new Drawable[] + { + new SettingsDropdown + { + Items = availableTargets.Select(t => t.Lookup), + Current = selectedTarget, + } + } + }, + new SkinComponentToolbox(getFirstTarget()) + { + RequestPlacement = placeComponent + } }; } } @@ -341,9 +362,9 @@ namespace osu.Game.Overlays.SkinEditor private IEnumerable availableTargets => targetScreen.ChildrenOfType(); - private ISerialisableDrawableContainer? getFirstTarget() => availableTargets.FirstOrDefault(); + private SkinComponentsContainer? getFirstTarget() => availableTargets.FirstOrDefault(); - private ISerialisableDrawableContainer? getTarget(SkinComponentsContainerLookup.TargetArea target) + private SkinComponentsContainer? getTarget(SkinComponentsContainerLookup.TargetArea target) { return availableTargets.FirstOrDefault(c => c.Lookup.Target == target); } From 00fcee0c5a98565122a8ac48fafaf7145bf76dd7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Feb 2023 15:50:48 +0900 Subject: [PATCH 15/43] Add per-ruleset component toolbox and placement support --- .../SkinEditor/SkinComponentToolbox.cs | 9 ++-- osu.Game/Overlays/SkinEditor/SkinEditor.cs | 49 ++++++++++++++----- 2 files changed, 42 insertions(+), 16 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinComponentToolbox.cs b/osu.Game/Overlays/SkinEditor/SkinComponentToolbox.cs index cd9f7cc935..de2bb46611 100644 --- a/osu.Game/Overlays/SkinEditor/SkinComponentToolbox.cs +++ b/osu.Game/Overlays/SkinEditor/SkinComponentToolbox.cs @@ -6,6 +6,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; +using osu.Framework.Localisation; using osu.Framework.Logging; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; @@ -21,12 +22,12 @@ namespace osu.Game.Overlays.SkinEditor { public Action? RequestPlacement; - private readonly CompositeDrawable? target; + private readonly SkinComponentsContainer? target; private FillFlowContainer fill = null!; - public SkinComponentToolbox(CompositeDrawable? target = null) - : base(SkinEditorStrings.Components) + public SkinComponentToolbox(SkinComponentsContainer? target = null) + : base(target?.Lookup.Ruleset == null ? SkinEditorStrings.Components : LocalisableString.Interpolate($"{SkinEditorStrings.Components} ({target.Lookup.Ruleset.Name})")) { this.target = target; } @@ -49,7 +50,7 @@ namespace osu.Game.Overlays.SkinEditor { fill.Clear(); - var skinnableTypes = SerialisedDrawableInfo.GetAllAvailableDrawables(); + var skinnableTypes = SerialisedDrawableInfo.GetAllAvailableDrawables(target?.Lookup.Ruleset); foreach (var type in skinnableTypes) attemptAddComponent(type); } diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index fccddac84f..b3813b4d2c 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -221,6 +221,8 @@ namespace osu.Game.Overlays.SkinEditor }, true); SelectedComponents.BindCollectionChanged((_, _) => Scheduler.AddOnce(populateSettings), true); + + selectedTarget.BindValueChanged(targetChanged, true); } public bool OnPressed(KeyBindingPressEvent e) @@ -274,10 +276,6 @@ namespace osu.Game.Overlays.SkinEditor content.Child = new SkinBlueprintContainer(targetScreen); - selectedTarget.Default = getFirstTarget()?.Lookup; - if (!availableTargets.Any(t => t.Lookup.Equals(selectedTarget.Value))) - selectedTarget.Value = getFirstTarget()?.Lookup; - componentsSidebar.Children = new[] { new EditorSidebarSection("Current working layer") @@ -291,14 +289,41 @@ namespace osu.Game.Overlays.SkinEditor } } }, - new SkinComponentToolbox(getFirstTarget()) - { - RequestPlacement = placeComponent - } }; + + selectedTarget.Default = getFirstTarget()?.Lookup; + + if (!availableTargets.Any(t => t.Lookup.Equals(selectedTarget.Value))) + selectedTarget.Value = getFirstTarget()?.Lookup; + else + selectedTarget.TriggerChange(); } } + private void targetChanged(ValueChangedEvent target) + { + foreach (var toolbox in componentsSidebar.OfType()) + toolbox.Expire(); + + if (target.NewValue == null) + return; + + // If the new target has a ruleset, let's show ruleset-specific items at the top, and the rest below. + if (target.NewValue.Ruleset != null) + { + componentsSidebar.Add(new SkinComponentToolbox(getTarget(target.NewValue)) + { + RequestPlacement = placeComponent + }); + } + + // Remove the ruleset from the lookup to get base components. + componentsSidebar.Add(new SkinComponentToolbox(getTarget(new SkinComponentsContainerLookup(target.NewValue.Target))) + { + RequestPlacement = placeComponent + }); + } + private void skinChanged() { headerText.Clear(); @@ -331,7 +356,7 @@ namespace osu.Game.Overlays.SkinEditor private void placeComponent(ISerialisableDrawable component, bool applyDefaults = true) { - var targetContainer = getFirstTarget(); + var targetContainer = getTarget(selectedTarget.Value); if (targetContainer == null) return; @@ -364,9 +389,9 @@ namespace osu.Game.Overlays.SkinEditor private SkinComponentsContainer? getFirstTarget() => availableTargets.FirstOrDefault(); - private SkinComponentsContainer? getTarget(SkinComponentsContainerLookup.TargetArea target) + private SkinComponentsContainer? getTarget(SkinComponentsContainerLookup? target) { - return availableTargets.FirstOrDefault(c => c.Lookup.Target == target); + return availableTargets.FirstOrDefault(c => c.Lookup.Equals(target)); } private void revert() @@ -378,7 +403,7 @@ namespace osu.Game.Overlays.SkinEditor currentSkin.Value.ResetDrawableTarget(t); // add back default components - getTarget(t.Lookup.Target)?.Reload(); + getTarget(t.Lookup)?.Reload(); } } From 0a018514e167db5c43a03c08cb54b3a921ae3bcd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Feb 2023 16:57:36 +0900 Subject: [PATCH 16/43] Make skin editor focus only one layer at a time --- osu.Game/Overlays/SkinEditor/SkinEditor.cs | 57 ++++++++++++---------- 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index b3813b4d2c..338458c1b2 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -258,8 +258,6 @@ namespace osu.Game.Overlays.SkinEditor changeHandler?.Dispose(); - SelectedComponents.Clear(); - // Immediately clear the previous blueprint container to ensure it doesn't try to interact with the old target. content?.Clear(); @@ -268,29 +266,6 @@ namespace osu.Game.Overlays.SkinEditor void loadBlueprintContainer() { - Debug.Assert(content != null); - - changeHandler = new SkinEditorChangeHandler(targetScreen); - changeHandler.CanUndo.BindValueChanged(v => undoMenuItem.Action.Disabled = !v.NewValue, true); - changeHandler.CanRedo.BindValueChanged(v => redoMenuItem.Action.Disabled = !v.NewValue, true); - - content.Child = new SkinBlueprintContainer(targetScreen); - - componentsSidebar.Children = new[] - { - new EditorSidebarSection("Current working layer") - { - Children = new Drawable[] - { - new SettingsDropdown - { - Items = availableTargets.Select(t => t.Lookup), - Current = selectedTarget, - } - } - }, - }; - selectedTarget.Default = getFirstTarget()?.Lookup; if (!availableTargets.Any(t => t.Lookup.Equals(selectedTarget.Value))) @@ -308,10 +283,40 @@ namespace osu.Game.Overlays.SkinEditor if (target.NewValue == null) return; + Debug.Assert(content != null); + + SelectedComponents.Clear(); + + var skinComponentsContainer = getTarget(target.NewValue); + + if (skinComponentsContainer == null) + return; + + changeHandler = new SkinEditorChangeHandler(skinComponentsContainer); + changeHandler.CanUndo.BindValueChanged(v => undoMenuItem.Action.Disabled = !v.NewValue, true); + changeHandler.CanRedo.BindValueChanged(v => redoMenuItem.Action.Disabled = !v.NewValue, true); + + content.Child = new SkinBlueprintContainer(skinComponentsContainer); + + componentsSidebar.Children = new[] + { + new EditorSidebarSection("Current working layer") + { + Children = new Drawable[] + { + new SettingsDropdown + { + Items = availableTargets.Select(t => t.Lookup), + Current = selectedTarget, + } + } + }, + }; + // If the new target has a ruleset, let's show ruleset-specific items at the top, and the rest below. if (target.NewValue.Ruleset != null) { - componentsSidebar.Add(new SkinComponentToolbox(getTarget(target.NewValue)) + componentsSidebar.Add(new SkinComponentToolbox(skinComponentsContainer) { RequestPlacement = placeComponent }); From a9c7edd08787a442e8390f453043ccbed0350d43 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Feb 2023 19:57:16 +0900 Subject: [PATCH 17/43] Remove copy pasted comment --- osu.Game/Overlays/SkinEditor/SkinEditor.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index ad07099d48..ae831f4d66 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -423,11 +423,6 @@ namespace osu.Game.Overlays.SkinEditor if (!canCopy.Value) return; - // This is an initial implementation just to get an idea of how people used this function. - // There are a couple of differences from osu!stable's implementation which will require more work to match: - // - The "clipboard" is not populated during the duplication process. - // - The duplicated hitobjects are inserted after the original pattern (add one beat_length and then quantize using beat snap). - // - The duplicated hitobjects are selected (but this is also applied for all paste operations so should be changed there). Copy(); Paste(); } From b68562b03359e5e8832df72f4cc6d3a4e3731486 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Feb 2023 20:00:12 +0900 Subject: [PATCH 18/43] Make `placeComponent` resilient to missing dependencies --- osu.Game/Overlays/SkinEditor/SkinEditor.cs | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index ae831f4d66..8177c31058 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -353,12 +353,18 @@ namespace osu.Game.Overlays.SkinEditor placeComponent(component); } - private void placeComponent(ISerialisableDrawable component, bool applyDefaults = true) + /// + /// Attempt to place a given component in the current target. + /// + /// The component to be placed. + /// Whether to apply default anchor / origin / position values. + /// Whether placement succeeded. Could fail if no target is available, or if the current target has missing dependency requirements for the component. + private bool placeComponent(ISerialisableDrawable component, bool applyDefaults = true) { var targetContainer = getFirstTarget(); if (targetContainer == null) - return; + return false; var drawableComponent = (Drawable)component; @@ -370,10 +376,19 @@ namespace osu.Game.Overlays.SkinEditor drawableComponent.Y = targetContainer.DrawSize.Y / 2; } - targetContainer.Add(component); + try + { + targetContainer.Add(component); + } + catch + { + // May fail if dependencies are not available, for instance. + return false; + } SelectedComponents.Clear(); SelectedComponents.Add(component); + return true; } private void populateSettings() From 43d33d45caa852707a1b5064f8cb23549ddc45de Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Feb 2023 20:02:43 +0900 Subject: [PATCH 19/43] Only add valid placed components to selected collection on paste --- osu.Game/Overlays/SkinEditor/SkinEditor.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index 8177c31058..944a64d131 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -455,11 +455,13 @@ namespace osu.Game.Overlays.SkinEditor .OfType() .ToArray(); - foreach (var i in instances) - placeComponent(i, false); - SelectedComponents.Clear(); - SelectedComponents.AddRange(instances); + + foreach (var i in instances) + { + if (placeComponent(i, false)) + SelectedComponents.Add(i); + } changeHandler?.EndChange(); } From 0d229d959bff7eb55eb05623e8c414b085ceed74 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Feb 2023 13:50:19 +0900 Subject: [PATCH 20/43] Remove unnecessary `TriggerChange` call --- osu.Game/Overlays/SkinEditor/SkinEditor.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index 338458c1b2..0caea4d0c7 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -269,9 +269,7 @@ namespace osu.Game.Overlays.SkinEditor selectedTarget.Default = getFirstTarget()?.Lookup; if (!availableTargets.Any(t => t.Lookup.Equals(selectedTarget.Value))) - selectedTarget.Value = getFirstTarget()?.Lookup; - else - selectedTarget.TriggerChange(); + selectedTarget.SetDefault(); } } From e686b4393ef9b3c6d9a27636a824a9bb8c76f0d4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Feb 2023 14:04:19 +0900 Subject: [PATCH 21/43] Add wait steps to ensure frame-stable clock has caught up before checking state --- .../Gameplay/TestSceneGameplaySampleTriggerSource.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs index f9f5581b43..f7641c0cc9 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs @@ -7,6 +7,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Timing; +using osu.Framework.Utils; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; @@ -155,19 +156,28 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("check valid object is slider's first nested", () => sampleTriggerSource.GetMostValidObject(), () => Is.EqualTo(beatmap.HitObjects[4].NestedHitObjects.First())); AddStep("seek to just after slider", () => Beatmap.Value.Track.Seek(beatmap.HitObjects[4].GetEndTime() + 100)); + waitForCatchUp(); AddUntilStep("wait until valid object is slider's last nested", () => sampleTriggerSource.GetMostValidObject(), () => Is.EqualTo(beatmap.HitObjects[4].NestedHitObjects.Last())); // After we get far enough away, the samples of the object itself should be used, not any nested object. AddStep("seek to further after slider", () => Beatmap.Value.Track.Seek(beatmap.HitObjects[4].GetEndTime() + 1000)); + waitForCatchUp(); AddUntilStep("wait until valid object is slider itself", () => sampleTriggerSource.GetMostValidObject(), () => Is.EqualTo(beatmap.HitObjects[4])); AddStep("Seek into future", () => Beatmap.Value.Track.Seek(beatmap.HitObjects.Last().GetEndTime() + 10000)); + waitForCatchUp(); waitForAliveObjectIndex(null); checkValidObjectIndex(4); } - private void seekBeforeIndex(int index) => + private void seekBeforeIndex(int index) + { AddStep($"seek to just before object {index}", () => Beatmap.Value.Track.Seek(beatmap.HitObjects[index].StartTime - 100)); + waitForCatchUp(); + } + + private void waitForCatchUp() => + AddUntilStep("wait for frame stable clock to catch up", () => Precision.AlmostEquals(Beatmap.Value.Track.CurrentTime, Player.DrawableRuleset.FrameStableClock.CurrentTime)); private void waitForAliveObjectIndex(int? index) { From 9321ec29dc942b109f885c9d0b34302d581f7998 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Feb 2023 14:04:37 +0900 Subject: [PATCH 22/43] Update slider sample source asserts to match expected behaviour As pointed out in review, if the current time is after the end of the slider, the correct hit object to use for sample retrieval is the object itself, not any nested object. --- .../Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs index f7641c0cc9..7f4f1ed027 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs @@ -155,7 +155,7 @@ namespace osu.Game.Tests.Visual.Gameplay // This is because the (parent) object will only play its sample at the final EndTime. AddAssert("check valid object is slider's first nested", () => sampleTriggerSource.GetMostValidObject(), () => Is.EqualTo(beatmap.HitObjects[4].NestedHitObjects.First())); - AddStep("seek to just after slider", () => Beatmap.Value.Track.Seek(beatmap.HitObjects[4].GetEndTime() + 100)); + AddStep("seek to just before slider ends", () => Beatmap.Value.Track.Seek(beatmap.HitObjects[4].GetEndTime() - 100)); waitForCatchUp(); AddUntilStep("wait until valid object is slider's last nested", () => sampleTriggerSource.GetMostValidObject(), () => Is.EqualTo(beatmap.HitObjects[4].NestedHitObjects.Last())); From af062e7a68c7b21ce9c8250c7f13bace75c19e2d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Feb 2023 14:07:47 +0900 Subject: [PATCH 23/43] Change `placeComponent` to only add to selection, not clear an existing selection --- osu.Game/Overlays/SkinEditor/SkinEditor.cs | 26 +++++++++------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index 944a64d131..d6521e759e 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -318,7 +318,14 @@ namespace osu.Game.Overlays.SkinEditor componentsSidebar.Child = new SkinComponentToolbox(getFirstTarget() as CompositeDrawable) { - RequestPlacement = placeComponent + RequestPlacement = type => + { + if (!(Activator.CreateInstance(type) is ISerialisableDrawable component)) + throw new InvalidOperationException($"Attempted to instantiate a component for placement which was not an {typeof(ISerialisableDrawable)}."); + + SelectedComponents.Clear(); + placeComponent(component); + } }; } } @@ -345,16 +352,8 @@ namespace osu.Game.Overlays.SkinEditor hasBegunMutating = true; } - private void placeComponent(Type type) - { - if (!(Activator.CreateInstance(type) is ISerialisableDrawable component)) - throw new InvalidOperationException($"Attempted to instantiate a component for placement which was not an {typeof(ISerialisableDrawable)}."); - - placeComponent(component); - } - /// - /// Attempt to place a given component in the current target. + /// Attempt to place a given component in the current target. If successful, the new component will be added to . /// /// The component to be placed. /// Whether to apply default anchor / origin / position values. @@ -386,7 +385,6 @@ namespace osu.Game.Overlays.SkinEditor return false; } - SelectedComponents.Clear(); SelectedComponents.Add(component); return true; } @@ -458,10 +456,7 @@ namespace osu.Game.Overlays.SkinEditor SelectedComponents.Clear(); foreach (var i in instances) - { - if (placeComponent(i, false)) - SelectedComponents.Add(i); - } + placeComponent(i, false); changeHandler?.EndChange(); } @@ -549,6 +544,7 @@ namespace osu.Game.Overlays.SkinEditor Position = skinnableTarget.ToLocalSpace(GetContainingInputManager().CurrentState.Mouse.Position), }; + SelectedComponents.Clear(); placeComponent(sprite, false); SkinSelectionHandler.ApplyClosestAnchor(sprite); From 1acc5362480b5eec0f8f150d2f44a6755b47f76a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Feb 2023 19:03:52 +0900 Subject: [PATCH 24/43] Move `DrawableRuleset.Audio` to a less generic level --- osu.Game/Rulesets/UI/DrawableRuleset.cs | 44 +++++++++++-------------- 1 file changed, 20 insertions(+), 24 deletions(-) diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index 11b238480a..7d7361b9b6 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -66,6 +66,10 @@ namespace osu.Game.Rulesets.UI public override Container Overlays { get; } = new Container { RelativeSizeAxes = Axes.Both }; + public override IAdjustableAudioComponent Audio => audioContainer; + + private readonly AudioContainer audioContainer = new AudioContainer { RelativeSizeAxes = Axes.Both }; + public override Container FrameStableComponents { get; } = new Container { RelativeSizeAxes = Axes.Both }; public override IFrameStableClock FrameStableClock => frameStabilityContainer; @@ -102,14 +106,6 @@ namespace osu.Game.Rulesets.UI private DrawableRulesetDependencies dependencies; - /// - /// Audio adjustments which are applied to the playfield. - /// - /// - /// Does not affect . - /// - public IAdjustableAudioComponent Audio { get; private set; } - /// /// Creates a ruleset visualisation for the provided ruleset and beatmap. /// @@ -172,30 +168,22 @@ namespace osu.Game.Rulesets.UI [BackgroundDependencyLoader] private void load(CancellationToken? cancellationToken) { - AudioContainer audioContainer; - InternalChild = frameStabilityContainer = new FrameStabilityContainer(GameplayStartTime) { FrameStablePlayback = FrameStablePlayback, Children = new Drawable[] { FrameStableComponents, - audioContainer = new AudioContainer - { - RelativeSizeAxes = Axes.Both, - Child = KeyBindingInputManager - .WithChildren(new Drawable[] - { - CreatePlayfieldAdjustmentContainer() - .WithChild(Playfield), - Overlays - }), - }, + audioContainer.WithChild(KeyBindingInputManager + .WithChildren(new Drawable[] + { + CreatePlayfieldAdjustmentContainer() + .WithChild(Playfield), + Overlays + })), } }; - Audio = audioContainer; - if ((ResumeOverlay = CreateResumeOverlay()) != null) { AddInternal(CreateInputManager() @@ -438,13 +426,21 @@ namespace osu.Game.Rulesets.UI /// public readonly BindableBool IsPaused = new BindableBool(); + /// + /// Audio adjustments which are applied to the playfield. + /// + /// + /// Does not affect . + /// + public abstract IAdjustableAudioComponent Audio { get; } + /// /// The playfield. /// public abstract Playfield Playfield { get; } /// - /// Content to be placed above hitobjects. Will be affected by frame stability. + /// Content to be placed above hitobjects. Will be affected by frame stability and adjustments applied to . /// public abstract Container Overlays { get; } From 9384687d6d38f02e98af67975b008abfde406167 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Feb 2023 19:04:06 +0900 Subject: [PATCH 25/43] Switch `ModMuted` to add its metronome to components rather than overlays --- osu.Game/Rulesets/Mods/ModMuted.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/ModMuted.cs b/osu.Game/Rulesets/Mods/ModMuted.cs index 367ceeb446..131f501630 100644 --- a/osu.Game/Rulesets/Mods/ModMuted.cs +++ b/osu.Game/Rulesets/Mods/ModMuted.cs @@ -67,7 +67,8 @@ namespace osu.Game.Rulesets.Mods { MetronomeBeat metronomeBeat; - drawableRuleset.Overlays.Add(metronomeBeat = new MetronomeBeat(drawableRuleset.Beatmap.HitObjects.First().StartTime)); + // Importantly, this is added to FrameStableComponents and not Overlays as the latter would cause it to be self-muted by the mod's volume adjustment. + drawableRuleset.FrameStableComponents.Add(metronomeBeat = new MetronomeBeat(drawableRuleset.Beatmap.HitObjects.First().StartTime)); metronomeBeat.AddAdjustment(AdjustableProperty.Volume, metronomeVolumeAdjust); } From d59d153654f6543e2f51556454964c51bb9c8f1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 21 Feb 2023 21:03:00 +0100 Subject: [PATCH 26/43] Fix test compile failures from `Audio` hoisting --- osu.Game.Tests/NonVisual/FirstAvailableHitWindowsTest.cs | 2 ++ osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/osu.Game.Tests/NonVisual/FirstAvailableHitWindowsTest.cs b/osu.Game.Tests/NonVisual/FirstAvailableHitWindowsTest.cs index 9a32b8e894..0bdd0ceae6 100644 --- a/osu.Game.Tests/NonVisual/FirstAvailableHitWindowsTest.cs +++ b/osu.Game.Tests/NonVisual/FirstAvailableHitWindowsTest.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using NUnit.Framework; +using osu.Framework.Audio; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; @@ -93,6 +94,7 @@ namespace osu.Game.Tests.NonVisual remove => throw new InvalidOperationException($"{nameof(RevertResult)} operations not supported in test context"); } + public override IAdjustableAudioComponent Audio { get; } public override Playfield Playfield { get; } public override Container Overlays { get; } public override Container FrameStableComponents { get; } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs index e2ff2780e0..56900a0549 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs @@ -9,6 +9,7 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Audio; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -281,6 +282,7 @@ namespace osu.Game.Tests.Visual.Gameplay remove => throw new InvalidOperationException($"{nameof(RevertResult)} operations not supported in test context"); } + public override IAdjustableAudioComponent Audio { get; } public override Playfield Playfield { get; } public override Container Overlays { get; } public override Container FrameStableComponents { get; } From ab97b022355abdc2bfd33473d0f102af9d8baa62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 21 Feb 2023 21:05:46 +0100 Subject: [PATCH 27/43] Remove contradictory remark from xmldoc --- osu.Game/Rulesets/UI/DrawableRuleset.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index 7d7361b9b6..df482a6459 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -429,9 +429,6 @@ namespace osu.Game.Rulesets.UI /// /// Audio adjustments which are applied to the playfield. /// - /// - /// Does not affect . - /// public abstract IAdjustableAudioComponent Audio { get; } /// From a511e64fa5badba12ca279f0f90409a52883e9d5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Feb 2023 14:41:20 +0900 Subject: [PATCH 28/43] Seek using `GameplayClockContainer` --- .../Gameplay/TestSceneGameplaySampleTriggerSource.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs index 7f4f1ed027..b30bef7c30 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs @@ -155,16 +155,16 @@ namespace osu.Game.Tests.Visual.Gameplay // This is because the (parent) object will only play its sample at the final EndTime. AddAssert("check valid object is slider's first nested", () => sampleTriggerSource.GetMostValidObject(), () => Is.EqualTo(beatmap.HitObjects[4].NestedHitObjects.First())); - AddStep("seek to just before slider ends", () => Beatmap.Value.Track.Seek(beatmap.HitObjects[4].GetEndTime() - 100)); + AddStep("seek to just before slider ends", () => Player.GameplayClockContainer.Seek(beatmap.HitObjects[4].GetEndTime() - 100)); waitForCatchUp(); AddUntilStep("wait until valid object is slider's last nested", () => sampleTriggerSource.GetMostValidObject(), () => Is.EqualTo(beatmap.HitObjects[4].NestedHitObjects.Last())); // After we get far enough away, the samples of the object itself should be used, not any nested object. - AddStep("seek to further after slider", () => Beatmap.Value.Track.Seek(beatmap.HitObjects[4].GetEndTime() + 1000)); + AddStep("seek to further after slider", () => Player.GameplayClockContainer.Seek(beatmap.HitObjects[4].GetEndTime() + 1000)); waitForCatchUp(); AddUntilStep("wait until valid object is slider itself", () => sampleTriggerSource.GetMostValidObject(), () => Is.EqualTo(beatmap.HitObjects[4])); - AddStep("Seek into future", () => Beatmap.Value.Track.Seek(beatmap.HitObjects.Last().GetEndTime() + 10000)); + AddStep("Seek into future", () => Player.GameplayClockContainer.Seek(beatmap.HitObjects.Last().GetEndTime() + 10000)); waitForCatchUp(); waitForAliveObjectIndex(null); checkValidObjectIndex(4); @@ -172,7 +172,7 @@ namespace osu.Game.Tests.Visual.Gameplay private void seekBeforeIndex(int index) { - AddStep($"seek to just before object {index}", () => Beatmap.Value.Track.Seek(beatmap.HitObjects[index].StartTime - 100)); + AddStep($"seek to just before object {index}", () => Player.GameplayClockContainer.Seek(beatmap.HitObjects[index].StartTime - 100)); waitForCatchUp(); } From f61fbcf3fc28ef676f7eb28d14e5a557100ef4bb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Feb 2023 15:26:09 +0900 Subject: [PATCH 29/43] Update assertion to also check `GameplayClockContainer`'s current time --- .../Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs index b30bef7c30..31133f00d9 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs @@ -177,7 +177,7 @@ namespace osu.Game.Tests.Visual.Gameplay } private void waitForCatchUp() => - AddUntilStep("wait for frame stable clock to catch up", () => Precision.AlmostEquals(Beatmap.Value.Track.CurrentTime, Player.DrawableRuleset.FrameStableClock.CurrentTime)); + AddUntilStep("wait for frame stable clock to catch up", () => Precision.AlmostEquals(Player.GameplayClockContainer.CurrentTime, Player.DrawableRuleset.FrameStableClock.CurrentTime)); private void waitForAliveObjectIndex(int? index) { From 16c8a392a1c7c73d0aeae9aa02edcd1a6e4d2f7e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Feb 2023 17:45:38 +0900 Subject: [PATCH 30/43] Add ability to send selected skin components to front or back --- osu.Game/Overlays/SkinEditor/SkinEditor.cs | 37 ++++++++++++++++++- .../SkinEditor/SkinSelectionHandler.cs | 9 +++++ .../ISerialisableDrawableContainer.cs | 3 +- osu.Game/Skinning/SkinComponentsContainer.cs | 4 +- 4 files changed, 49 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index 9d470f58f1..95b88b141f 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -556,11 +556,46 @@ namespace osu.Game.Overlays.SkinEditor changeHandler?.BeginChange(); foreach (var item in items) - availableTargets.FirstOrDefault(t => t.Components.Contains(item))?.Remove(item); + availableTargets.FirstOrDefault(t => t.Components.Contains(item))?.Remove(item, true); changeHandler?.EndChange(); } + public void BringSelectionToFront() + { + if (getTarget(selectedTarget.Value) is not SkinComponentsContainer target) + return; + + // Iterating by target components order ensures we maintain the same order across selected components, regardless + // of the order they were selected in. + foreach (var d in target.Components.ToArray()) + { + if (!SelectedComponents.Contains(d)) + continue; + + target.Remove(d, false); + + // Selection would be reset by the remove. + SelectedComponents.Add(d); + target.Add(d); + } + } + + public void SendSelectionToBack() + { + if (getTarget(selectedTarget.Value) is not SkinComponentsContainer target) + return; + + foreach (var d in target.Components.ToArray()) + { + if (SelectedComponents.Contains(d)) + continue; + + target.Remove(d, false); + target.Add(d); + } + } + #region Drag & drop import handling public Task Import(params string[] paths) diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs index c628ad8480..b43f4eeb00 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs @@ -13,6 +13,7 @@ using osu.Framework.Utils; using osu.Game.Extensions; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Edit; +using osu.Game.Screens.Edit.Components.Menus; using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Skinning; using osuTK; @@ -206,6 +207,14 @@ namespace osu.Game.Overlays.SkinEditor ((Drawable)blueprint.Item).Position = Vector2.Zero; }); + yield return new EditorMenuItemSpacer(); + + yield return new OsuMenuItem("Bring to front", MenuItemType.Standard, () => skinEditor.BringSelectionToFront()); + + yield return new OsuMenuItem("Send to back", MenuItemType.Standard, () => skinEditor.SendSelectionToBack()); + + yield return new EditorMenuItemSpacer(); + foreach (var item in base.GetContextMenuItemsForSelection(selection)) yield return item; diff --git a/osu.Game/Skinning/ISerialisableDrawableContainer.cs b/osu.Game/Skinning/ISerialisableDrawableContainer.cs index 9f93d8a2e3..a19c8c5162 100644 --- a/osu.Game/Skinning/ISerialisableDrawableContainer.cs +++ b/osu.Game/Skinning/ISerialisableDrawableContainer.cs @@ -45,6 +45,7 @@ namespace osu.Game.Skinning /// Remove an existing skinnable component from this target. /// /// The component to remove. - void Remove(ISerialisableDrawable component); + /// Whether removed items should be immediately disposed. + void Remove(ISerialisableDrawable component, bool disposeImmediately); } } diff --git a/osu.Game/Skinning/SkinComponentsContainer.cs b/osu.Game/Skinning/SkinComponentsContainer.cs index d18e9023cd..adf0a288b4 100644 --- a/osu.Game/Skinning/SkinComponentsContainer.cs +++ b/osu.Game/Skinning/SkinComponentsContainer.cs @@ -100,7 +100,7 @@ namespace osu.Game.Skinning /// /// Thrown when attempting to add an element to a target which is not supported by the current skin. /// Thrown if the provided instance is not a . - public void Remove(ISerialisableDrawable component) + public void Remove(ISerialisableDrawable component, bool disposeImmediately) { if (content == null) throw new NotSupportedException("Attempting to remove a new component from a target container which is not supported by the current skin."); @@ -108,7 +108,7 @@ namespace osu.Game.Skinning if (!(component is Drawable drawable)) throw new ArgumentException($"Provided argument must be of type {nameof(Drawable)}.", nameof(component)); - content.Remove(drawable, true); + content.Remove(drawable, disposeImmediately); components.Remove(component); } From 90ca635a1771d9953f3891656296b7b7d98dda84 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Feb 2023 17:56:59 +0900 Subject: [PATCH 31/43] Fix weird nullability in `TestSceneSkinEditor` --- .../Visual/Gameplay/TestSceneSkinEditor.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs index 2f20d75813..83609b9ae7 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs @@ -4,6 +4,7 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.UserInterface; using osu.Framework.Testing; @@ -21,7 +22,7 @@ namespace osu.Game.Tests.Visual.Gameplay { public partial class TestSceneSkinEditor : PlayerTestScene { - private SkinEditor? skinEditor; + private SkinEditor skinEditor = null!; protected override bool Autoplay => true; @@ -40,17 +41,18 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("reload skin editor", () => { - skinEditor?.Expire(); + if (skinEditor.IsNotNull()) + skinEditor.Expire(); Player.ScaleTo(0.4f); LoadComponentAsync(skinEditor = new SkinEditor(Player), Add); }); - AddUntilStep("wait for loaded", () => skinEditor!.IsLoaded); + AddUntilStep("wait for loaded", () => skinEditor.IsLoaded); } [Test] public void TestToggleEditor() { - AddToggleStep("toggle editor visibility", _ => skinEditor!.ToggleVisibility()); + AddToggleStep("toggle editor visibility", _ => skinEditor.ToggleVisibility()); } [Test] @@ -63,7 +65,7 @@ namespace osu.Game.Tests.Visual.Gameplay var blueprint = skinEditor.ChildrenOfType().First(b => b.Item is BarHitErrorMeter); hitErrorMeter = (BarHitErrorMeter)blueprint.Item; - skinEditor!.SelectedComponents.Clear(); + skinEditor.SelectedComponents.Clear(); skinEditor.SelectedComponents.Add(blueprint.Item); }); From 32a9c066dfd2a02c0cc40f5554417ab5bd8e8c97 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Feb 2023 18:17:03 +0900 Subject: [PATCH 32/43] Add test coverage of bring-to-front / send-to-back operations --- .../Visual/Gameplay/TestSceneSkinEditor.cs | 51 ++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs index 83609b9ae7..9690d00d4c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.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 System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; @@ -32,12 +33,14 @@ namespace osu.Game.Tests.Visual.Gameplay [Cached] public readonly EditorClipboard Clipboard = new EditorClipboard(); + private SkinComponentsContainer targetContainer => Player.ChildrenOfType().First(); + [SetUpSteps] public override void SetUpSteps() { base.SetUpSteps(); - AddUntilStep("wait for hud load", () => Player.ChildrenOfType().All(c => c.ComponentsLoaded)); + AddUntilStep("wait for hud load", () => targetContainer.ComponentsLoaded); AddStep("reload skin editor", () => { @@ -49,6 +52,52 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("wait for loaded", () => skinEditor.IsLoaded); } + [TestCase(false)] + [TestCase(true)] + public void TestBringToFront(bool alterSelectionOrder) + { + AddAssert("Ensure over three components available", () => targetContainer.Components.Count, () => Is.GreaterThan(3)); + + IEnumerable originalOrder = null!; + + AddStep("Save order of components before operation", () => originalOrder = targetContainer.Components.Take(3).ToArray()); + + if (alterSelectionOrder) + AddStep("Select first three components in reverse order", () => skinEditor.SelectedComponents.AddRange(originalOrder.Reverse())); + else + AddStep("Select first three components", () => skinEditor.SelectedComponents.AddRange(originalOrder)); + + AddAssert("Components are not front-most", () => targetContainer.Components.TakeLast(3).ToArray(), () => Is.Not.EqualTo(skinEditor.SelectedComponents)); + + AddStep("Bring to front", () => skinEditor.BringSelectionToFront()); + AddAssert("Ensure components are now front-most in original order", () => targetContainer.Components.TakeLast(3).ToArray(), () => Is.EqualTo(originalOrder)); + AddStep("Bring to front again", () => skinEditor.BringSelectionToFront()); + AddAssert("Ensure components are still front-most in original order", () => targetContainer.Components.TakeLast(3).ToArray(), () => Is.EqualTo(originalOrder)); + } + + [TestCase(false)] + [TestCase(true)] + public void TestSendToBack(bool alterSelectionOrder) + { + AddAssert("Ensure over three components available", () => targetContainer.Components.Count, () => Is.GreaterThan(3)); + + IEnumerable originalOrder = null!; + + AddStep("Save order of components before operation", () => originalOrder = targetContainer.Components.TakeLast(3).ToArray()); + + if (alterSelectionOrder) + AddStep("Select last three components in reverse order", () => skinEditor.SelectedComponents.AddRange(originalOrder.Reverse())); + else + AddStep("Select last three components", () => skinEditor.SelectedComponents.AddRange(originalOrder)); + + AddAssert("Components are not back-most", () => targetContainer.Components.Take(3).ToArray(), () => Is.Not.EqualTo(skinEditor.SelectedComponents)); + + AddStep("Send to back", () => skinEditor.SendSelectionToBack()); + AddAssert("Ensure components are now back-most in original order", () => targetContainer.Components.Take(3).ToArray(), () => Is.EqualTo(originalOrder)); + AddStep("Send to back again", () => skinEditor.SendSelectionToBack()); + AddAssert("Ensure components are still back-most in original order", () => targetContainer.Components.Take(3).ToArray(), () => Is.EqualTo(originalOrder)); + } + [Test] public void TestToggleEditor() { From c48aceb055433f57bd00d885691d8a61e8317460 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Feb 2023 20:03:36 +0900 Subject: [PATCH 33/43] Fix undo history not being batched correctly for depth change operations --- osu.Game/Overlays/SkinEditor/SkinEditor.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index 95b88b141f..02ede8c08b 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -566,6 +566,8 @@ namespace osu.Game.Overlays.SkinEditor if (getTarget(selectedTarget.Value) is not SkinComponentsContainer target) return; + changeHandler?.BeginChange(); + // Iterating by target components order ensures we maintain the same order across selected components, regardless // of the order they were selected in. foreach (var d in target.Components.ToArray()) @@ -579,6 +581,8 @@ namespace osu.Game.Overlays.SkinEditor SelectedComponents.Add(d); target.Add(d); } + + changeHandler?.EndChange(); } public void SendSelectionToBack() @@ -586,6 +590,8 @@ namespace osu.Game.Overlays.SkinEditor if (getTarget(selectedTarget.Value) is not SkinComponentsContainer target) return; + changeHandler?.BeginChange(); + foreach (var d in target.Components.ToArray()) { if (SelectedComponents.Contains(d)) @@ -594,6 +600,8 @@ namespace osu.Game.Overlays.SkinEditor target.Remove(d, false); target.Add(d); } + + changeHandler?.EndChange(); } #region Drag & drop import handling From dc3c1150b8401b4f4d49b1ecb6c8ba30663fe217 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Feb 2023 21:10:15 +0900 Subject: [PATCH 34/43] Set better defaults for `SkinBlueprint` transforms --- osu.Game/Overlays/SkinEditor/SkinBlueprint.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinBlueprint.cs b/osu.Game/Overlays/SkinEditor/SkinBlueprint.cs index 034ca11c5c..c090878899 100644 --- a/osu.Game/Overlays/SkinEditor/SkinBlueprint.cs +++ b/osu.Game/Overlays/SkinEditor/SkinBlueprint.cs @@ -82,6 +82,7 @@ namespace osu.Game.Overlays.SkinEditor { Text = Item.GetType().Name, Font = OsuFont.Default.With(size: 10, weight: FontWeight.Bold), + Alpha = 0, Anchor = Anchor.BottomRight, Origin = Anchor.TopRight, }, @@ -99,7 +100,6 @@ namespace osu.Game.Overlays.SkinEditor base.LoadComplete(); updateSelectedState(); - this.FadeInFromZero(200, Easing.OutQuint); } protected override bool OnHover(HoverEvent e) From d98d330da20e2bced3a4d753f40c6a50f614f926 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sun, 26 Feb 2023 14:30:46 -0800 Subject: [PATCH 35/43] Add expected behavior test for scroll back to previous position --- .../TestSceneOverlayScrollContainer.cs | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs index 926bc01aea..ade950e2d5 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs @@ -61,6 +61,18 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("scroll to 500", () => scroll.ScrollTo(500)); AddUntilStep("scrolled to 500", () => Precision.AlmostEquals(scroll.Current, 500, 0.1f)); AddAssert("button is visible", () => scroll.Button.State == Visibility.Visible); + + AddStep("click button", () => + { + InputManager.MoveMouseTo(scroll.Button); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("button is visible", () => scroll.Button.State == Visibility.Visible); + + AddStep("user scroll down by 1", () => InputManager.ScrollVerticalBy(-1)); + + AddAssert("button is hidden", () => scroll.Button.State == Visibility.Hidden); } [Test] @@ -71,6 +83,10 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("invoke action", () => scroll.Button.Action.Invoke()); AddUntilStep("scrolled back to start", () => Precision.AlmostEquals(scroll.Current, 0, 0.1f)); + + AddStep("invoke action", () => scroll.Button.Action.Invoke()); + + AddAssert("scrolled to end", () => scroll.IsScrolledToEnd()); } [Test] @@ -85,6 +101,14 @@ namespace osu.Game.Tests.Visual.UserInterface }); AddUntilStep("scrolled back to start", () => Precision.AlmostEquals(scroll.Current, 0, 0.1f)); + + AddStep("click button", () => + { + InputManager.MoveMouseTo(scroll.Button); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("scrolled to end", () => scroll.IsScrolledToEnd()); } [Test] @@ -97,7 +121,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("hover button", () => InputManager.MoveMouseTo(scroll.Button)); AddRepeatStep("click button", () => InputManager.Click(MouseButton.Left), 3); - AddAssert("invocation count is 1", () => invocationCount == 1); + AddAssert("invocation count is 3", () => invocationCount == 3); } private partial class TestScrollContainer : OverlayScrollContainer From dc00905f8df0b24f974315a7efdac099373f09eb Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sun, 26 Feb 2023 14:38:51 -0800 Subject: [PATCH 36/43] Add ability to scroll back to previous position after scrolling to top via button on overlays --- osu.Game/Overlays/OverlayScrollContainer.cs | 50 +++++++++++++++++---- 1 file changed, 42 insertions(+), 8 deletions(-) diff --git a/osu.Game/Overlays/OverlayScrollContainer.cs b/osu.Game/Overlays/OverlayScrollContainer.cs index 5bd7f014a9..8176ddb370 100644 --- a/osu.Game/Overlays/OverlayScrollContainer.cs +++ b/osu.Game/Overlays/OverlayScrollContainer.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -30,16 +31,20 @@ namespace osu.Game.Overlays /// private const int button_scroll_position = 200; - protected readonly ScrollToTopButton Button; + protected ScrollToTopButton Button; - public OverlayScrollContainer() + private readonly Bindable lastScrollTarget = new Bindable(); + + [BackgroundDependencyLoader] + private void load() { AddInternal(Button = new ScrollToTopButton { Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, Margin = new MarginPadding(20), - Action = scrollToTop + Action = scrollBack, + LastScrollTarget = { BindTarget = lastScrollTarget } }); } @@ -53,13 +58,28 @@ namespace osu.Game.Overlays return; } - Button.State = Target > button_scroll_position ? Visibility.Visible : Visibility.Hidden; + Button.State = Target > button_scroll_position || lastScrollTarget.Value != null ? Visibility.Visible : Visibility.Hidden; } - private void scrollToTop() + protected override void OnUserScroll(float value, bool animated = true, double? distanceDecay = default) { - ScrollToStart(); - Button.State = Visibility.Hidden; + base.OnUserScroll(value, animated, distanceDecay); + + lastScrollTarget.Value = null; + } + + private void scrollBack() + { + if (lastScrollTarget.Value == null) + { + lastScrollTarget.Value = Target; + ScrollToStart(); + } + else + { + ScrollTo(lastScrollTarget.Value.Value); + lastScrollTarget.Value = null; + } } public partial class ScrollToTopButton : OsuHoverContainer @@ -88,6 +108,9 @@ namespace osu.Game.Overlays private readonly Container content; private readonly Box background; + private readonly SpriteIcon spriteIcon; + + public Bindable LastScrollTarget = new Bindable(); public ScrollToTopButton() : base(HoverSampleSet.ScrollToTop) @@ -113,7 +136,7 @@ namespace osu.Game.Overlays { RelativeSizeAxes = Axes.Both }, - new SpriteIcon + spriteIcon = new SpriteIcon { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -134,6 +157,17 @@ namespace osu.Game.Overlays flashColour = colourProvider.Light1; } + protected override void LoadComplete() + { + base.LoadComplete(); + + LastScrollTarget.BindValueChanged(target => + { + spriteIcon.RotateTo(target.NewValue != null ? 180 : 0, fade_duration, Easing.OutQuint); + TooltipText = target.NewValue != null ? CommonStrings.ButtonsBackToPrevious : CommonStrings.ButtonsBackToTop; + }, true); + } + protected override bool OnClick(ClickEvent e) { background.FlashColour(flashColour, 800, Easing.OutQuint); From fa710ae1b00dd9f618e481271b25aa2df5d558a4 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sun, 26 Feb 2023 14:39:34 -0800 Subject: [PATCH 37/43] Rename `ScrollToTopButton` to `ScrollBackButton` --- .../UserInterface/TestSceneOverlayScrollContainer.cs | 2 +- osu.Game/Overlays/OverlayScrollContainer.cs | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs index ade950e2d5..77e7178c9e 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs @@ -126,7 +126,7 @@ namespace osu.Game.Tests.Visual.UserInterface private partial class TestScrollContainer : OverlayScrollContainer { - public new ScrollToTopButton Button => base.Button; + public new ScrollBackButton Button => base.Button; } } } diff --git a/osu.Game/Overlays/OverlayScrollContainer.cs b/osu.Game/Overlays/OverlayScrollContainer.cs index 8176ddb370..9752e04f44 100644 --- a/osu.Game/Overlays/OverlayScrollContainer.cs +++ b/osu.Game/Overlays/OverlayScrollContainer.cs @@ -22,23 +22,23 @@ using osuTK.Graphics; namespace osu.Game.Overlays { /// - /// which provides . Mostly used in . + /// which provides . Mostly used in . /// public partial class OverlayScrollContainer : UserTrackingScrollContainer { /// - /// Scroll position at which the will be shown. + /// Scroll position at which the will be shown. /// private const int button_scroll_position = 200; - protected ScrollToTopButton Button; + protected ScrollBackButton Button; private readonly Bindable lastScrollTarget = new Bindable(); [BackgroundDependencyLoader] private void load() { - AddInternal(Button = new ScrollToTopButton + AddInternal(Button = new ScrollBackButton { Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, @@ -82,7 +82,7 @@ namespace osu.Game.Overlays } } - public partial class ScrollToTopButton : OsuHoverContainer + public partial class ScrollBackButton : OsuHoverContainer { private const int fade_duration = 500; @@ -112,7 +112,7 @@ namespace osu.Game.Overlays public Bindable LastScrollTarget = new Bindable(); - public ScrollToTopButton() + public ScrollBackButton() : base(HoverSampleSet.ScrollToTop) { Size = new Vector2(50); From 8d747f240220e98cb40a9edd567f6778c31115e0 Mon Sep 17 00:00:00 2001 From: OpenSauce <48618519+OpenSauce04@users.noreply.github.com> Date: Mon, 27 Feb 2023 16:06:33 +0000 Subject: [PATCH 38/43] Fixed grammar mistake in Taiko Relax mod description --- osu.Game.Rulesets.Taiko/Mods/TaikoModRelax.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModRelax.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModRelax.cs index d1e9ab1428..69a98f8372 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModRelax.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModRelax.cs @@ -8,6 +8,6 @@ namespace osu.Game.Rulesets.Taiko.Mods { public class TaikoModRelax : ModRelax { - public override LocalisableString Description => @"No ninja-like spinners, demanding drumrolls or unexpected katu's."; + public override LocalisableString Description => @"No ninja-like spinners, demanding drumrolls or unexpected katus."; } } From 2615453b3143e59886a56adb2f5d835a241a2a5d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 28 Feb 2023 11:45:32 +0900 Subject: [PATCH 39/43] Rename `SettingSource` tests to match attribute name --- ...ingsSourceAttributeTest.cs => SettingSourceAttributeTest.cs} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename osu.Game.Tests/Mods/{SettingsSourceAttributeTest.cs => SettingSourceAttributeTest.cs} (98%) diff --git a/osu.Game.Tests/Mods/SettingsSourceAttributeTest.cs b/osu.Game.Tests/Mods/SettingSourceAttributeTest.cs similarity index 98% rename from osu.Game.Tests/Mods/SettingsSourceAttributeTest.cs rename to osu.Game.Tests/Mods/SettingSourceAttributeTest.cs index bf59862787..5da303d3a7 100644 --- a/osu.Game.Tests/Mods/SettingsSourceAttributeTest.cs +++ b/osu.Game.Tests/Mods/SettingSourceAttributeTest.cs @@ -12,7 +12,7 @@ using osu.Game.Overlays.Settings; namespace osu.Game.Tests.Mods { [TestFixture] - public partial class SettingsSourceAttributeTest + public partial class SettingSourceAttributeTest { [Test] public void TestOrdering() From bd1460a8d5726a67acfdfbfb08568b872fcc8d83 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 28 Feb 2023 14:32:58 +0900 Subject: [PATCH 40/43] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 4bdf5d68c5..abd8406cf7 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - +