From 521ec1a225f72931941c1ab23714954aa646ec93 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 3 May 2022 04:44:55 +0300 Subject: [PATCH 01/27] Add keybind for distance grid spacing activation --- osu.Game/Input/Bindings/GlobalActionContainer.cs | 4 ++++ .../Localisation/GlobalActionKeyBindingStrings.cs | 5 +++++ .../Rulesets/Edit/DistancedHitObjectComposer.cs | 14 ++++++++------ 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 47cb7be2cf..5656c79975 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -79,6 +79,7 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.F5 }, GlobalAction.EditorTestGameplay), new KeyBinding(new[] { InputKey.Control, InputKey.H }, GlobalAction.EditorFlipHorizontally), new KeyBinding(new[] { InputKey.Control, InputKey.J }, GlobalAction.EditorFlipVertically), + new KeyBinding(new[] { InputKey.Control, InputKey.Alt }, GlobalAction.EditorDistanceSpacing), }; public IEnumerable InGameKeyBindings => new[] @@ -301,5 +302,8 @@ namespace osu.Game.Input.Bindings [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorFlipVertically))] EditorFlipVertically, + + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorDistanceSpacing))] + EditorDistanceSpacing, } } diff --git a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs index 777e97d1e3..73cc7546ab 100644 --- a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs +++ b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs @@ -239,6 +239,11 @@ namespace osu.Game.Localisation /// public static LocalisableString EditorFlipVertically => new TranslatableString(getKey(@"editor_flip_vertically"), @"Flip selection vertically"); + /// + /// "Distance grid spacing (hold)" + /// + public static LocalisableString EditorDistanceSpacing => new TranslatableString(getKey(@"editor_distance_spacing"), @"Distance grid spacing (hold)"); + /// /// "Toggle skin editor" /// diff --git a/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs b/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs index 0505f9ab0e..1a4ea845d0 100644 --- a/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs @@ -4,9 +4,11 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; +using osu.Game.Input.Bindings; using osu.Game.Overlays.Settings.Sections; using osu.Game.Rulesets.Objects; using osuTK; @@ -18,7 +20,7 @@ namespace osu.Game.Rulesets.Edit /// /// The base type of supported objects. [Cached(typeof(IDistanceSnapProvider))] - public abstract class DistancedHitObjectComposer : HitObjectComposer, IDistanceSnapProvider + public abstract class DistancedHitObjectComposer : HitObjectComposer, IDistanceSnapProvider, IKeyBindingHandler where TObject : HitObject { protected Bindable DistanceSpacingMultiplier { get; } = new BindableDouble(1.0) @@ -75,21 +77,21 @@ namespace osu.Game.Rulesets.Edit } } - protected override bool OnKeyDown(KeyDownEvent e) + public bool OnPressed(KeyBindingPressEvent e) { - if (!DistanceSpacingMultiplier.Disabled && e.ControlPressed && e.AltPressed && !e.Repeat) + if (!DistanceSpacingMultiplier.Disabled && e.Action == GlobalAction.EditorDistanceSpacing) { RightSideToolboxContainer.Expanded.Value = true; distanceSpacingScrollActive = true; return true; } - return base.OnKeyDown(e); + return false; } - protected override void OnKeyUp(KeyUpEvent e) + public void OnReleased(KeyBindingReleaseEvent e) { - if (!DistanceSpacingMultiplier.Disabled && distanceSpacingScrollActive && (!e.AltPressed || !e.ControlPressed)) + if (!DistanceSpacingMultiplier.Disabled && e.Action == GlobalAction.EditorDistanceSpacing) { RightSideToolboxContainer.Expanded.Value = false; distanceSpacingScrollActive = false; From b8287f3687e06c88b6a565994cf23233f285d2e2 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 3 May 2022 10:30:32 +0300 Subject: [PATCH 02/27] Display toast notification on editor distance spacing change --- .../Edit/DistancedHitObjectComposer.cs | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs b/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs index 1a4ea845d0..a19944712e 100644 --- a/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs @@ -3,12 +3,16 @@ 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; +using osu.Framework.Localisation; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; +using osu.Game.Overlays; +using osu.Game.Overlays.OSD; using osu.Game.Overlays.Settings.Sections; using osu.Game.Rulesets.Objects; using osuTK; @@ -37,6 +41,9 @@ namespace osu.Game.Rulesets.Edit private ExpandableSlider> distanceSpacingSlider; private bool distanceSpacingScrollActive; + [Resolved] + private OnScreenDisplay onScreenDisplay { get; set; } + protected DistancedHitObjectComposer(Ruleset ruleset) : base(ruleset) { @@ -72,6 +79,10 @@ namespace osu.Game.Rulesets.Edit { distanceSpacingSlider.ContractedLabelText = $"D. S. ({v.NewValue:0.##x})"; distanceSpacingSlider.ExpandedLabelText = $"Distance Spacing ({v.NewValue:0.##x})"; + + if (v.NewValue != v.OldValue) + onScreenDisplay.Display(new DistanceSpacingToast(v.NewValue.ToLocalisableString(@"0.##x"))); + EditorBeatmap.BeatmapInfo.DistanceSpacing = v.NewValue; }, true); } @@ -81,7 +92,6 @@ namespace osu.Game.Rulesets.Edit { if (!DistanceSpacingMultiplier.Disabled && e.Action == GlobalAction.EditorDistanceSpacing) { - RightSideToolboxContainer.Expanded.Value = true; distanceSpacingScrollActive = true; return true; } @@ -92,10 +102,7 @@ namespace osu.Game.Rulesets.Edit public void OnReleased(KeyBindingReleaseEvent e) { if (!DistanceSpacingMultiplier.Disabled && e.Action == GlobalAction.EditorDistanceSpacing) - { - RightSideToolboxContainer.Expanded.Value = false; distanceSpacingScrollActive = false; - } } protected override bool OnScroll(ScrollEvent e) @@ -160,5 +167,13 @@ namespace osu.Game.Rulesets.Edit FillFlow.Spacing = new Vector2(10); } } + + private class DistanceSpacingToast : Toast + { + public DistanceSpacingToast(LocalisableString value) + : base("Distance Spacing", value, string.Empty) + { + } + } } } From 0dd2e1652c2a040e6bd831c2cbdab12d22f33210 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 3 May 2022 11:15:28 +0300 Subject: [PATCH 03/27] Mark `OnScreenDisplay` dependency as nullable --- osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs b/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs index a19944712e..75b87fb6e8 100644 --- a/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs @@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Edit private ExpandableSlider> distanceSpacingSlider; private bool distanceSpacingScrollActive; - [Resolved] + [Resolved(canBeNull: true)] private OnScreenDisplay onScreenDisplay { get; set; } protected DistancedHitObjectComposer(Ruleset ruleset) @@ -81,7 +81,7 @@ namespace osu.Game.Rulesets.Edit distanceSpacingSlider.ExpandedLabelText = $"Distance Spacing ({v.NewValue:0.##x})"; if (v.NewValue != v.OldValue) - onScreenDisplay.Display(new DistanceSpacingToast(v.NewValue.ToLocalisableString(@"0.##x"))); + onScreenDisplay?.Display(new DistanceSpacingToast(v.NewValue.ToLocalisableString(@"0.##x"))); EditorBeatmap.BeatmapInfo.DistanceSpacing = v.NewValue; }, true); From 89d8ed8e20b2850cb96af75fd0d5b69adf575c61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 17 Apr 2022 20:26:16 +0200 Subject: [PATCH 04/27] Port existing test coverage --- .../UserInterface/TestSceneModSelectScreen.cs | 225 ++++++++++++++++++ 1 file changed, 225 insertions(+) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectScreen.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectScreen.cs index ec6e962c6a..bd63023efb 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectScreen.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectScreen.cs @@ -2,17 +2,21 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.Mods; +using osu.Game.Overlays.Settings; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; using osuTK.Input; @@ -169,6 +173,206 @@ namespace osu.Game.Tests.Visual.UserInterface assertCustomisationToggleState(disabled: true, active: false); // config was dismissed without explicit user action. } + /// + /// Ensure that two mod overlays are not cross polluting via central settings instances. + /// + [Test] + public void TestSettingsNotCrossPolluting() + { + Bindable> selectedMods2 = null; + ModSelectScreen modSelectScreen2 = null; + + createScreen(); + AddStep("select diff adjust", () => SelectedMods.Value = new Mod[] { new OsuModDifficultyAdjust() }); + + AddStep("set setting", () => modSelectScreen.ChildrenOfType>().First().Current.Value = 8); + + AddAssert("ensure setting is propagated", () => SelectedMods.Value.OfType().Single().CircleSize.Value == 8); + + AddStep("create second bindable", () => selectedMods2 = new Bindable>(new Mod[] { new OsuModDifficultyAdjust() })); + + AddStep("create second overlay", () => + { + Add(modSelectScreen2 = new UserModSelectScreen().With(d => + { + d.Origin = Anchor.TopCentre; + d.Anchor = Anchor.TopCentre; + d.SelectedMods.BindTarget = selectedMods2; + })); + }); + + AddStep("show", () => modSelectScreen2.Show()); + + AddAssert("ensure first is unchanged", () => SelectedMods.Value.OfType().Single().CircleSize.Value == 8); + AddAssert("ensure second is default", () => selectedMods2.Value.OfType().Single().CircleSize.Value == null); + } + + [Test] + public void TestSettingsResetOnDeselection() + { + var osuModDoubleTime = new OsuModDoubleTime { SpeedChange = { Value = 1.2 } }; + + createScreen(); + changeRuleset(0); + + AddStep("set dt mod with custom rate", () => { SelectedMods.Value = new[] { osuModDoubleTime }; }); + + AddAssert("selected mod matches", () => (SelectedMods.Value.Single() as OsuModDoubleTime)?.SpeedChange.Value == 1.2); + + AddStep("deselect", () => getPanelForMod(typeof(OsuModDoubleTime)).TriggerClick()); + AddAssert("selected mods empty", () => SelectedMods.Value.Count == 0); + + AddStep("reselect", () => getPanelForMod(typeof(OsuModDoubleTime)).TriggerClick()); + AddAssert("selected mod has default value", () => (SelectedMods.Value.Single() as OsuModDoubleTime)?.SpeedChange.IsDefault == true); + } + + [Test] + public void TestAnimationFlushOnClose() + { + createScreen(); + changeRuleset(0); + + AddStep("Select all fun mods", () => + { + modSelectScreen.ChildrenOfType() + .Single(c => c.ModType == ModType.DifficultyIncrease) + .SelectAll(); + }); + + AddUntilStep("many mods selected", () => SelectedMods.Value.Count >= 5); + + AddStep("trigger deselect and close overlay", () => + { + modSelectScreen.ChildrenOfType() + .Single(c => c.ModType == ModType.DifficultyIncrease) + .DeselectAll(); + + modSelectScreen.Hide(); + }); + + AddAssert("all mods deselected", () => SelectedMods.Value.Count == 0); + } + + [Test] + public void TestRulesetChanges() + { + createScreen(); + changeRuleset(0); + + var noFailMod = new OsuRuleset().GetModsFor(ModType.DifficultyReduction).FirstOrDefault(m => m is OsuModNoFail); + + AddStep("set mods externally", () => { SelectedMods.Value = new[] { noFailMod }; }); + + changeRuleset(0); + + AddAssert("ensure mods still selected", () => SelectedMods.Value.SingleOrDefault(m => m is OsuModNoFail) != null); + + changeRuleset(3); + + AddAssert("ensure mods not selected", () => SelectedMods.Value.Count == 0); + + changeRuleset(0); + + AddAssert("ensure mods not selected", () => SelectedMods.Value.Count == 0); + } + + [Test] + public void TestExternallySetCustomizedMod() + { + createScreen(); + changeRuleset(0); + + AddStep("set customized mod externally", () => SelectedMods.Value = new[] { new OsuModDoubleTime { SpeedChange = { Value = 1.01 } } }); + + AddAssert("ensure button is selected and customized accordingly", () => + { + var button = getPanelForMod(SelectedMods.Value.Single().GetType()); + return ((OsuModDoubleTime)button.Mod).SpeedChange.Value == 1.01; + }); + } + + [Test] + public void TestSettingsAreRetainedOnReload() + { + createScreen(); + changeRuleset(0); + + AddStep("set customized mod externally", () => SelectedMods.Value = new[] { new OsuModDoubleTime { SpeedChange = { Value = 1.01 } } }); + AddAssert("setting remains", () => (SelectedMods.Value.SingleOrDefault() as OsuModDoubleTime)?.SpeedChange.Value == 1.01); + + createScreen(); + AddAssert("setting remains", () => (SelectedMods.Value.SingleOrDefault() as OsuModDoubleTime)?.SpeedChange.Value == 1.01); + } + + [Test] + public void TestExternallySetModIsReplacedByOverlayInstance() + { + Mod external = new OsuModDoubleTime(); + Mod overlayButtonMod = null; + + createScreen(); + changeRuleset(0); + + AddStep("set mod externally", () => { SelectedMods.Value = new[] { external }; }); + + AddAssert("ensure button is selected", () => + { + var button = getPanelForMod(SelectedMods.Value.Single().GetType()); + overlayButtonMod = button.Mod; + return button.Active.Value; + }); + + // Right now, when an external change occurs, the ModSelectOverlay will replace the global instance with its own + AddAssert("mod instance doesn't match", () => external != overlayButtonMod); + + AddAssert("one mod present in global selected", () => SelectedMods.Value.Count == 1); + AddAssert("globally selected matches button's mod instance", () => SelectedMods.Value.Contains(overlayButtonMod)); + AddAssert("globally selected doesn't contain original external change", () => !SelectedMods.Value.Contains(external)); + } + + [Test] + public void TestChangeIsValidChangesButtonVisibility() + { + createScreen(); + changeRuleset(0); + + AddAssert("double time visible", () => modSelectScreen.ChildrenOfType().Where(panel => panel.Mod is OsuModDoubleTime).Any(panel => !panel.Filtered.Value)); + + AddStep("make double time invalid", () => modSelectScreen.IsValidMod = m => !(m is OsuModDoubleTime)); + AddUntilStep("double time not visible", () => modSelectScreen.ChildrenOfType().Where(panel => panel.Mod is OsuModDoubleTime).All(panel => panel.Filtered.Value)); + AddAssert("nightcore still visible", () => modSelectScreen.ChildrenOfType().Where(panel => panel.Mod is OsuModNightcore).Any(panel => !panel.Filtered.Value)); + + AddStep("make double time valid again", () => modSelectScreen.IsValidMod = m => true); + AddUntilStep("double time visible", () => modSelectScreen.ChildrenOfType().Where(panel => panel.Mod is OsuModDoubleTime).Any(panel => !panel.Filtered.Value)); + AddAssert("nightcore still visible", () => modSelectScreen.ChildrenOfType().Where(b => b.Mod is OsuModNightcore).Any(panel => !panel.Filtered.Value)); + } + + [Test] + public void TestChangeIsValidPreservesSelection() + { + createScreen(); + changeRuleset(0); + + AddStep("select DT + HD", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime(), new OsuModHidden() }); + AddAssert("DT + HD selected", () => modSelectScreen.ChildrenOfType().Count(panel => panel.Active.Value) == 2); + + AddStep("make NF invalid", () => modSelectScreen.IsValidMod = m => !(m is ModNoFail)); + AddAssert("DT + HD still selected", () => modSelectScreen.ChildrenOfType().Count(panel => panel.Active.Value) == 2); + } + + [Test] + public void TestUnimplementedModIsUnselectable() + { + var testRuleset = new TestUnimplementedModOsuRuleset(); + + createScreen(); + + AddStep("set ruleset", () => Ruleset.Value = testRuleset.RulesetInfo); + waitForColumnLoad(); + + AddAssert("unimplemented mod panel is filtered", () => getPanelForMod(typeof(TestUnimplementedMod)).Filtered.Value); + } + private void waitForColumnLoad() => AddUntilStep("all column content loaded", () => modSelectScreen.ChildrenOfType().Any() && modSelectScreen.ChildrenOfType().All(column => column.IsLoaded && column.ItemsLoaded)); @@ -188,5 +392,26 @@ namespace osu.Game.Tests.Visual.UserInterface private ModPanel getPanelForMod(Type modType) => modSelectScreen.ChildrenOfType().Single(panel => panel.Mod.GetType() == modType); + + private class TestUnimplementedMod : Mod + { + public override string Name => "Unimplemented mod"; + public override string Acronym => "UM"; + public override string Description => "A mod that is not implemented."; + public override double ScoreMultiplier => 1; + public override ModType Type => ModType.Conversion; + } + + private class TestUnimplementedModOsuRuleset : OsuRuleset + { + public override string ShortName => "unimplemented"; + + public override IEnumerable GetModsFor(ModType type) + { + if (type == ModType.Conversion) return base.GetModsFor(type).Concat(new[] { new TestUnimplementedMod() }); + + return base.GetModsFor(type); + } + } } } From 746a4a740316f6bd1fbdd17f8bcc1d63c3474e6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 17 Apr 2022 21:23:54 +0200 Subject: [PATCH 05/27] Fix mod column using wrong equality type --- osu.Game/Overlays/Mods/ModColumn.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs index 018922c074..9e32602a85 100644 --- a/osu.Game/Overlays/Mods/ModColumn.cs +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -296,7 +296,7 @@ namespace osu.Game.Overlays.Mods private void updateActiveState() { foreach (var panel in panelFlow) - panel.Active.Value = SelectedMods.Value.Contains(panel.Mod, EqualityComparer.Default); + panel.Active.Value = SelectedMods.Value.Any(selected => selected.GetType() == panel.Mod.GetType()); } #region Bulk select / deselect From fe59f4ae58ae848af1f6992d9c4b63f9f3d3a0e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 17 Apr 2022 20:32:45 +0200 Subject: [PATCH 06/27] Fix multiselection operation not flushing on close --- osu.Game/Overlays/Mods/ModColumn.cs | 9 +++++++++ osu.Game/Overlays/Mods/ModSelectScreen.cs | 10 ++++++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs index 9e32602a85..c839305746 100644 --- a/osu.Game/Overlays/Mods/ModColumn.cs +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -364,6 +364,15 @@ namespace osu.Game.Overlays.Mods pendingSelectionOperations.Enqueue(() => button.Active.Value = false); } + /// + /// Play out all remaining animations immediately to leave mods in a good (final) state. + /// + public void FlushAnimation() + { + while (pendingSelectionOperations.TryDequeue(out var dequeuedAction)) + dequeuedAction(); + } + private class ToggleAllCheckbox : OsuCheckbox { private Color4 accentColour; diff --git a/osu.Game/Overlays/Mods/ModSelectScreen.cs b/osu.Game/Overlays/Mods/ModSelectScreen.cs index ffd6e9a52c..20c5b5a321 100644 --- a/osu.Game/Overlays/Mods/ModSelectScreen.cs +++ b/osu.Game/Overlays/Mods/ModSelectScreen.cs @@ -313,10 +313,12 @@ namespace osu.Game.Overlays.Mods { const float distance = 700; - columnFlow[i].Column - .TopLevelContent - .MoveToY(i % 2 == 0 ? -distance : distance, fade_out_duration, Easing.OutQuint) - .FadeOut(fade_out_duration, Easing.OutQuint); + var column = columnFlow[i].Column; + + column.FlushAnimation(); + column.TopLevelContent + .MoveToY(i % 2 == 0 ? -distance : distance, fade_out_duration, Easing.OutQuint) + .FadeOut(fade_out_duration, Easing.OutQuint); } } From 7c04bf5c53442b3fe2d0adf05928c674a378923c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 17 Apr 2022 22:12:06 +0200 Subject: [PATCH 07/27] Refactor mod reference management to meet test expectations --- osu.Game/Overlays/Mods/ModColumn.cs | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs index c839305746..c2273abf53 100644 --- a/osu.Game/Overlays/Mods/ModColumn.cs +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +#nullable enable + using System; using System.Collections.Generic; using System.Linq; @@ -10,6 +12,7 @@ using Humanizer; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; @@ -25,8 +28,6 @@ using osuTK; using osuTK.Graphics; using osuTK.Input; -#nullable enable - namespace osu.Game.Overlays.Mods { public class ModColumn : CompositeDrawable @@ -256,7 +257,9 @@ namespace osu.Game.Overlays.Mods private void updateMods() { - var newMods = ModUtils.FlattenMods(availableMods.Value.GetValueOrDefault(ModType) ?? Array.Empty()).ToList(); + var newMods = ModUtils.FlattenMods(availableMods.Value.GetValueOrDefault(ModType) ?? Array.Empty()) + .Select(m => m.DeepClone()) + .ToList(); if (newMods.SequenceEqual(panelFlow.Children.Select(p => p.Mod))) return; @@ -280,9 +283,16 @@ namespace osu.Game.Overlays.Mods panel.Active.BindValueChanged(_ => { updateToggleAllState(); - SelectedMods.Value = panel.Active.Value - ? SelectedMods.Value.Append(panel.Mod).ToArray() - : SelectedMods.Value.Except(new[] { panel.Mod }).ToArray(); + + var newSelectedMods = SelectedMods.Value; + + var matchingModInstance = SelectedMods.Value.SingleOrDefault(selected => selected.GetType() == panel.Mod.GetType()); + if (matchingModInstance != null && (matchingModInstance != panel.Mod || !panel.Active.Value)) + newSelectedMods = newSelectedMods.Except(matchingModInstance.Yield()).ToArray(); + if (panel.Active.Value) + newSelectedMods = newSelectedMods.Append(panel.Mod).ToArray(); + + SelectedMods.Value = newSelectedMods.ToArray(); }); } }, (cancellationTokenSource = new CancellationTokenSource()).Token); @@ -296,7 +306,12 @@ namespace osu.Game.Overlays.Mods private void updateActiveState() { foreach (var panel in panelFlow) - panel.Active.Value = SelectedMods.Value.Any(selected => selected.GetType() == panel.Mod.GetType()); + { + var matchingSelectedMod = SelectedMods.Value.SingleOrDefault(selected => selected.GetType() == panel.Mod.GetType()); + panel.Active.Value = matchingSelectedMod != null; + if (matchingSelectedMod != null) + panel.Mod.CopyFrom(matchingSelectedMod); + } } #region Bulk select / deselect From f91ee4b0422bd8b91585b4f0f9a689719a8b0f06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 17 Apr 2022 22:14:28 +0200 Subject: [PATCH 08/27] Reset panel mod instance settings to defaults on deselect --- osu.Game/Overlays/Mods/ModColumn.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs index c2273abf53..d480382fcb 100644 --- a/osu.Game/Overlays/Mods/ModColumn.cs +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -309,8 +309,10 @@ namespace osu.Game.Overlays.Mods { var matchingSelectedMod = SelectedMods.Value.SingleOrDefault(selected => selected.GetType() == panel.Mod.GetType()); panel.Active.Value = matchingSelectedMod != null; - if (matchingSelectedMod != null) + if (panel.Active.Value) panel.Mod.CopyFrom(matchingSelectedMod); + else + panel.Mod.ResetSettingsToDefaults(); } } From e3641213e1832c627def0b48aae6213f455e4b17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 17 Apr 2022 22:16:59 +0200 Subject: [PATCH 09/27] Always hide unimplemented mods on mod select screen --- osu.Game/Overlays/Mods/ModSelectScreen.cs | 2 +- osu.Game/Screens/OnlinePlay/FreeModSelectScreen.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSelectScreen.cs b/osu.Game/Overlays/Mods/ModSelectScreen.cs index 20c5b5a321..2f6eae0afb 100644 --- a/osu.Game/Overlays/Mods/ModSelectScreen.cs +++ b/osu.Game/Overlays/Mods/ModSelectScreen.cs @@ -203,7 +203,7 @@ namespace osu.Game.Overlays.Mods private void updateAvailableMods() { foreach (var column in columnFlow.Columns) - column.Filter = isValidMod; + column.Filter = m => m.HasImplementation && isValidMod.Invoke(m); } private void updateCustomisation(ValueChangedEvent> valueChangedEvent) diff --git a/osu.Game/Screens/OnlinePlay/FreeModSelectScreen.cs b/osu.Game/Screens/OnlinePlay/FreeModSelectScreen.cs index c85a4fc38b..5a7a60b479 100644 --- a/osu.Game/Screens/OnlinePlay/FreeModSelectScreen.cs +++ b/osu.Game/Screens/OnlinePlay/FreeModSelectScreen.cs @@ -16,7 +16,7 @@ namespace osu.Game.Screens.OnlinePlay public new Func IsValidMod { get => base.IsValidMod; - set => base.IsValidMod = m => m.HasImplementation && m.UserPlayable && value.Invoke(m); + set => base.IsValidMod = m => m.UserPlayable && value.Invoke(m); } public FreeModSelectScreen() From 970361676b374995f5c768805e9d9097e23ec855 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 2 May 2022 23:05:35 +0200 Subject: [PATCH 10/27] Fix setting items not creating if mods initially not empty --- .../Visual/UserInterface/TestSceneModSelectScreen.cs | 2 ++ osu.Game/Overlays/Mods/ModSettingsArea.cs | 5 +++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectScreen.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectScreen.cs index bd63023efb..7690029ba2 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectScreen.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectScreen.cs @@ -68,6 +68,7 @@ namespace osu.Game.Tests.Visual.UserInterface return Precision.AlmostEquals(multiplier, modSelectScreen.ChildrenOfType().Single().Current.Value); }); assertCustomisationToggleState(disabled: false, active: false); + AddAssert("setting items created", () => modSelectScreen.ChildrenOfType().Any()); } [Test] @@ -82,6 +83,7 @@ namespace osu.Game.Tests.Visual.UserInterface return Precision.AlmostEquals(multiplier, modSelectScreen.ChildrenOfType().Single().Current.Value); }); assertCustomisationToggleState(disabled: false, active: false); + AddAssert("setting items created", () => modSelectScreen.ChildrenOfType().Any()); } [Test] diff --git a/osu.Game/Overlays/Mods/ModSettingsArea.cs b/osu.Game/Overlays/Mods/ModSettingsArea.cs index be72c1e3e3..9c5f3b7f11 100644 --- a/osu.Game/Overlays/Mods/ModSettingsArea.cs +++ b/osu.Game/Overlays/Mods/ModSettingsArea.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; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; @@ -21,7 +22,7 @@ namespace osu.Game.Overlays.Mods { public class ModSettingsArea : CompositeDrawable { - public Bindable> SelectedMods { get; } = new Bindable>(); + public Bindable> SelectedMods { get; } = new Bindable>(Array.Empty()); public const float HEIGHT = 250; @@ -77,7 +78,7 @@ namespace osu.Game.Overlays.Mods protected override void LoadComplete() { base.LoadComplete(); - SelectedMods.BindValueChanged(_ => updateMods()); + SelectedMods.BindValueChanged(_ => updateMods(), true); } private void updateMods() From 216dfb7e9121b5e0f8b662d3f9039aeb7860fbcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 2 May 2022 23:29:30 +0200 Subject: [PATCH 11/27] Expand incompatibility test to cover logic more thoroughly --- .../Visual/UserInterface/TestSceneModSelectScreen.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectScreen.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectScreen.cs index 7690029ba2..0bd138275b 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectScreen.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectScreen.cs @@ -104,17 +104,25 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("activate DT", () => getPanelForMod(typeof(OsuModDoubleTime)).TriggerClick()); AddAssert("DT active", () => SelectedMods.Value.Single().GetType() == typeof(OsuModDoubleTime)); + AddAssert("DT panel active", () => getPanelForMod(typeof(OsuModDoubleTime)).Active.Value); AddStep("activate NC", () => getPanelForMod(typeof(OsuModNightcore)).TriggerClick()); AddAssert("only NC active", () => SelectedMods.Value.Single().GetType() == typeof(OsuModNightcore)); + AddAssert("DT panel not active", () => !getPanelForMod(typeof(OsuModDoubleTime)).Active.Value); + AddAssert("NC panel active", () => getPanelForMod(typeof(OsuModNightcore)).Active.Value); AddStep("activate HR", () => getPanelForMod(typeof(OsuModHardRock)).TriggerClick()); AddAssert("NC+HR active", () => SelectedMods.Value.Any(mod => mod.GetType() == typeof(OsuModNightcore)) && SelectedMods.Value.Any(mod => mod.GetType() == typeof(OsuModHardRock))); + AddAssert("NC panel active", () => getPanelForMod(typeof(OsuModNightcore)).Active.Value); + AddAssert("HR panel active", () => getPanelForMod(typeof(OsuModHardRock)).Active.Value); AddStep("activate MR", () => getPanelForMod(typeof(OsuModMirror)).TriggerClick()); AddAssert("NC+MR active", () => SelectedMods.Value.Any(mod => mod.GetType() == typeof(OsuModNightcore)) && SelectedMods.Value.Any(mod => mod.GetType() == typeof(OsuModMirror))); + AddAssert("NC panel active", () => getPanelForMod(typeof(OsuModNightcore)).Active.Value); + AddAssert("HR panel not active", () => !getPanelForMod(typeof(OsuModHardRock)).Active.Value); + AddAssert("MR panel active", () => getPanelForMod(typeof(OsuModMirror)).Active.Value); } [Test] From f5fa41356e33a528039742f9909d45dff71e451e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 3 May 2022 21:44:44 +0200 Subject: [PATCH 12/27] Rewrite mod instance management again to pass tests --- .../UserInterface/TestSceneModSelectScreen.cs | 4 +- osu.Game/Overlays/Mods/ModColumn.cs | 129 ++++++++++++------ osu.Game/Overlays/Mods/ModSelectScreen.cs | 33 ++--- osu.Game/Overlays/Mods/UserModSelectScreen.cs | 7 +- 4 files changed, 111 insertions(+), 62 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectScreen.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectScreen.cs index 0bd138275b..bdb423a43c 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectScreen.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectScreen.cs @@ -336,8 +336,8 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("mod instance doesn't match", () => external != overlayButtonMod); AddAssert("one mod present in global selected", () => SelectedMods.Value.Count == 1); - AddAssert("globally selected matches button's mod instance", () => SelectedMods.Value.Contains(overlayButtonMod)); - AddAssert("globally selected doesn't contain original external change", () => !SelectedMods.Value.Contains(external)); + AddAssert("globally selected matches button's mod instance", () => SelectedMods.Value.Any(mod => ReferenceEquals(mod, overlayButtonMod))); + AddAssert("globally selected doesn't contain original external change", () => !SelectedMods.Value.Any(mod => ReferenceEquals(mod, external))); } [Test] diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs index d480382fcb..f9332dd28b 100644 --- a/osu.Game/Overlays/Mods/ModColumn.cs +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -53,9 +53,22 @@ namespace osu.Game.Overlays.Mods } } - public Bindable> SelectedMods = new Bindable>(Array.Empty()); public Bindable Active = new BindableBool(true); + /// + /// List of mods marked as selected in this column. + /// + /// + /// Note that the mod instances returned by this property are owned solely by this column + /// (as in, they are locally-managed clones, to ensure proper isolation from any other external instances). + /// + public IReadOnlyList SelectedMods { get; private set; } = Array.Empty(); + + /// + /// Invoked when a mod panel has been selected interactively by the user. + /// + public event Action? SelectionChangedByUser; + protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) => base.ReceivePositionalInputAtSubTree(screenSpacePos) && Active.Value; protected virtual ModPanel CreateModPanel(Mod mod) => new ModPanel(mod); @@ -64,6 +77,15 @@ namespace osu.Game.Overlays.Mods private readonly Bindable>> availableMods = new Bindable>>(); + /// + /// All mods that are available for the current ruleset in this particular column. + /// + /// + /// Note that the mod instances in this list are owned solely by this column + /// (as in, they are locally-managed clones, to ensure proper isolation from any other external instances). + /// + private IReadOnlyList localAvailableMods = Array.Empty(); + private readonly TextFlowContainer headerText; private readonly Box headerBackground; private readonly Container contentContainer; @@ -227,6 +249,9 @@ namespace osu.Game.Overlays.Mods private void load(OsuGameBase game, OverlayColourProvider colourProvider, OsuColour colours) { availableMods.BindTo(game.AvailableMods); + // this `BindValueChanged` callback is intentionally here, to ensure that local available mods are constructed as early as possible. + // this is needed to make sure no external changes to mods are dropped while mod panels are asynchronously loading. + availableMods.BindValueChanged(_ => updateLocalAvailableMods(), true); headerBackground.Colour = accentColour = colours.ForModType(ModType); @@ -240,33 +265,26 @@ namespace osu.Game.Overlays.Mods contentBackground.Colour = colourProvider.Background4; } - protected override void LoadComplete() - { - base.LoadComplete(); - availableMods.BindValueChanged(_ => Scheduler.AddOnce(updateMods)); - SelectedMods.BindValueChanged(_ => - { - // if a load is in progress, don't try to update the selection - the load flow will do so. - if (latestLoadTask == null) - updateActiveState(); - }); - updateMods(); - } - - private CancellationTokenSource? cancellationTokenSource; - - private void updateMods() + private void updateLocalAvailableMods() { var newMods = ModUtils.FlattenMods(availableMods.Value.GetValueOrDefault(ModType) ?? Array.Empty()) .Select(m => m.DeepClone()) .ToList(); - if (newMods.SequenceEqual(panelFlow.Children.Select(p => p.Mod))) + if (newMods.SequenceEqual(localAvailableMods)) return; + localAvailableMods = newMods; + Scheduler.AddOnce(loadPanels); + } + + private CancellationTokenSource? cancellationTokenSource; + + private void loadPanels() + { cancellationTokenSource?.Cancel(); - var panels = newMods.Select(mod => CreateModPanel(mod).With(panel => panel.Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0))); + var panels = localAvailableMods.Select(mod => CreateModPanel(mod).With(panel => panel.Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0))); Task? loadTask; @@ -280,20 +298,7 @@ namespace osu.Game.Overlays.Mods foreach (var panel in panelFlow) { - panel.Active.BindValueChanged(_ => - { - updateToggleAllState(); - - var newSelectedMods = SelectedMods.Value; - - var matchingModInstance = SelectedMods.Value.SingleOrDefault(selected => selected.GetType() == panel.Mod.GetType()); - if (matchingModInstance != null && (matchingModInstance != panel.Mod || !panel.Active.Value)) - newSelectedMods = newSelectedMods.Except(matchingModInstance.Yield()).ToArray(); - if (panel.Active.Value) - newSelectedMods = newSelectedMods.Append(panel.Mod).ToArray(); - - SelectedMods.Value = newSelectedMods.ToArray(); - }); + panel.Active.BindValueChanged(_ => panelStateChanged(panel)); } }, (cancellationTokenSource = new CancellationTokenSource()).Token); loadTask.ContinueWith(_ => @@ -306,14 +311,62 @@ namespace osu.Game.Overlays.Mods private void updateActiveState() { foreach (var panel in panelFlow) + panel.Active.Value = SelectedMods.Contains(panel.Mod); + } + + /// + /// This flag helps to determine the source of changes to . + /// If the value is false, then are changing due to a user selection on the UI. + /// If the value is true, then are changing due to an external call. + /// + private bool externalSelectionUpdateInProgress; + + private void panelStateChanged(ModPanel panel) + { + updateToggleAllState(); + + var newSelectedMods = panel.Active.Value + ? SelectedMods.Append(panel.Mod) + : SelectedMods.Except(panel.Mod.Yield()); + + SelectedMods = newSelectedMods.ToArray(); + if (!externalSelectionUpdateInProgress) + SelectionChangedByUser?.Invoke(); + } + + /// + /// Adjusts the set of selected mods in this column to match the passed in . + /// + /// + /// This method exists to be able to receive mod instances that come from potentially-external sources and to copy the changes across to this column's state. + /// uses this to substitute any external mod references in + /// to references that are owned by this column. + /// + internal void SetSelection(IReadOnlyList mods) + { + externalSelectionUpdateInProgress = true; + + var newSelection = new List(); + + foreach (var mod in localAvailableMods) { - var matchingSelectedMod = SelectedMods.Value.SingleOrDefault(selected => selected.GetType() == panel.Mod.GetType()); - panel.Active.Value = matchingSelectedMod != null; - if (panel.Active.Value) - panel.Mod.CopyFrom(matchingSelectedMod); + var matchingSelectedMod = mods.SingleOrDefault(selected => selected.GetType() == mod.GetType()); + + if (matchingSelectedMod != null) + { + mod.CopyFrom(matchingSelectedMod); + newSelection.Add(mod); + } else - panel.Mod.ResetSettingsToDefaults(); + { + mod.ResetSettingsToDefaults(); + } } + + SelectedMods = newSelection; + updateActiveState(); + + externalSelectionUpdateInProgress = false; } #region Bulk select / deselect diff --git a/osu.Game/Overlays/Mods/ModSelectScreen.cs b/osu.Game/Overlays/Mods/ModSelectScreen.cs index 2f6eae0afb..bb12143d7c 100644 --- a/osu.Game/Overlays/Mods/ModSelectScreen.cs +++ b/osu.Game/Overlays/Mods/ModSelectScreen.cs @@ -179,7 +179,7 @@ namespace osu.Game.Overlays.Mods foreach (var column in columnFlow.Columns) { - column.SelectedMods.BindValueChanged(updateBindableFromSelection); + column.SelectionChangedByUser += updateBindableFromSelection; } customisationVisible.BindValueChanged(_ => updateCustomisationVisualState(), true); @@ -250,33 +250,26 @@ namespace osu.Game.Overlays.Mods private void updateSelectionFromBindable() { - // note that selectionBindableSyncInProgress is purposefully not checked here. - // this is because in the case of mod selection in solo gameplay, a user selection of a mod can actually lead to deselection of other incompatible mods. - // to synchronise state correctly, updateBindableFromSelection() computes the final mods (including incompatibility rules) and updates SelectedMods, - // and this method then runs unconditionally again to make sure the new visual selection accurately reflects the final set of selected mods. - // selectionBindableSyncInProgress ensures that mutual infinite recursion does not happen after that unconditional call. + // `SelectedMods` may contain mod references that come from external sources. + // to ensure isolation, first pull in the potentially-external change into the mod columns... foreach (var column in columnFlow.Columns) - column.SelectedMods.Value = SelectedMods.Value.Where(mod => mod.Type == column.ModType).ToArray(); + column.SetSelection(SelectedMods.Value); + + // and then, when done, replace the potentially-external mod references in `SelectedMods` with ones we own. + updateBindableFromSelection(); } - private bool selectionBindableSyncInProgress; - - private void updateBindableFromSelection(ValueChangedEvent> modSelectionChange) + private void updateBindableFromSelection() { - if (selectionBindableSyncInProgress) + var candidateSelection = columnFlow.Columns.SelectMany(column => column.SelectedMods).ToArray(); + + if (candidateSelection.SequenceEqual(SelectedMods.Value)) return; - selectionBindableSyncInProgress = true; - - SelectedMods.Value = ComputeNewModsFromSelection( - modSelectionChange.NewValue.Except(modSelectionChange.OldValue), - modSelectionChange.OldValue.Except(modSelectionChange.NewValue)); - - selectionBindableSyncInProgress = false; + SelectedMods.Value = ComputeNewModsFromSelection(SelectedMods.Value, candidateSelection); } - protected virtual IReadOnlyList ComputeNewModsFromSelection(IEnumerable addedMods, IEnumerable removedMods) - => columnFlow.Columns.SelectMany(column => column.SelectedMods.Value).ToArray(); + protected virtual IReadOnlyList ComputeNewModsFromSelection(IReadOnlyList oldSelection, IReadOnlyList newSelection) => newSelection; protected override void PopIn() { diff --git a/osu.Game/Overlays/Mods/UserModSelectScreen.cs b/osu.Game/Overlays/Mods/UserModSelectScreen.cs index ed0a07521b..ca33d35605 100644 --- a/osu.Game/Overlays/Mods/UserModSelectScreen.cs +++ b/osu.Game/Overlays/Mods/UserModSelectScreen.cs @@ -14,9 +14,12 @@ namespace osu.Game.Overlays.Mods { protected override ModColumn CreateModColumn(ModType modType, Key[] toggleKeys = null) => new UserModColumn(modType, false, toggleKeys); - protected override IReadOnlyList ComputeNewModsFromSelection(IEnumerable addedMods, IEnumerable removedMods) + protected override IReadOnlyList ComputeNewModsFromSelection(IReadOnlyList oldSelection, IReadOnlyList newSelection) { - IEnumerable modsAfterRemoval = SelectedMods.Value.Except(removedMods).ToList(); + var addedMods = newSelection.Except(oldSelection); + var removedMods = oldSelection.Except(newSelection); + + IEnumerable modsAfterRemoval = newSelection.Except(removedMods).ToList(); // the preference is that all new mods should override potential incompatible old mods. // in general that's a bit difficult to compute if more than one mod is added at a time, From bb086800b1e528205cfcd635c093798bc87e2a0c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 May 2022 13:41:30 +0900 Subject: [PATCH 13/27] Remove ugly playfield border --- osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index bd26a99e51..ba7c6e9d33 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -59,11 +59,6 @@ namespace osu.Game.Rulesets.Osu.Edit { LayerBelowRuleset.AddRange(new Drawable[] { - new PlayfieldBorder - { - RelativeSizeAxes = Axes.Both, - PlayfieldBorderStyle = { Value = PlayfieldBorderStyle.Corners } - }, distanceSnapGridContainer = new Container { RelativeSizeAxes = Axes.Both From 0bb90c7b07169096984972b9741b6f9a9dbebb2d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 May 2022 13:41:55 +0900 Subject: [PATCH 14/27] Fix gridline centering and ensure lines are always rendered using a fixed screen-space width --- .../Components/RectangularPositionSnapGrid.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/RectangularPositionSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/RectangularPositionSnapGrid.cs index 95b4b2fe53..4c0b4f8beb 100644 --- a/osu.Game/Screens/Edit/Compose/Components/RectangularPositionSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/RectangularPositionSnapGrid.cs @@ -6,6 +6,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Layout; +using osu.Framework.Utils; using osuTK; namespace osu.Game.Screens.Edit.Compose.Components @@ -72,25 +73,29 @@ namespace osu.Game.Screens.Edit.Compose.Components int index = 0; float currentPosition = startPosition; - while ((endPosition - currentPosition) * Math.Sign(step) > 0) + // Make lines the same width independent of display resolution. + float lineWidth = DrawWidth / ScreenSpaceDrawQuad.Width; + + while (Precision.AlmostBigger((endPosition - currentPosition) * Math.Sign(step), 0)) { var gridLine = new Box { Colour = Colour4.White, Alpha = index == 0 ? 0.3f : 0.1f, - EdgeSmoothness = new Vector2(0.2f) }; if (direction == Direction.Horizontal) { + gridLine.Origin = Anchor.CentreLeft; gridLine.RelativeSizeAxes = Axes.X; - gridLine.Height = 1; + gridLine.Height = lineWidth; gridLine.Y = currentPosition; } else { + gridLine.Origin = Anchor.TopCentre; gridLine.RelativeSizeAxes = Axes.Y; - gridLine.Width = 1; + gridLine.Width = lineWidth; gridLine.X = currentPosition; } From df530cb5ab2d2c598fcf8f02d20475d20abde36a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 May 2022 13:49:02 +0900 Subject: [PATCH 15/27] Add highlighting to the position snap grid edges in addition to centre lines --- .../Components/RectangularPositionSnapGrid.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/RectangularPositionSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/RectangularPositionSnapGrid.cs index 4c0b4f8beb..f0d26c7b6a 100644 --- a/osu.Game/Screens/Edit/Compose/Components/RectangularPositionSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/RectangularPositionSnapGrid.cs @@ -2,6 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; +using System.Linq; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -76,12 +78,14 @@ namespace osu.Game.Screens.Edit.Compose.Components // Make lines the same width independent of display resolution. float lineWidth = DrawWidth / ScreenSpaceDrawQuad.Width; + List generatedLines = new List(); + while (Precision.AlmostBigger((endPosition - currentPosition) * Math.Sign(step), 0)) { var gridLine = new Box { Colour = Colour4.White, - Alpha = index == 0 ? 0.3f : 0.1f, + Alpha = 0.1f, }; if (direction == Direction.Horizontal) @@ -99,11 +103,19 @@ namespace osu.Game.Screens.Edit.Compose.Components gridLine.X = currentPosition; } - AddInternal(gridLine); + generatedLines.Add(gridLine); index += 1; currentPosition = startPosition + index * step; } + + if (generatedLines.Count == 0) + return; + + generatedLines.First().Alpha = 0.3f; + generatedLines.Last().Alpha = 0.3f; + + AddRangeInternal(generatedLines); } public Vector2 GetSnappedPosition(Vector2 original) From 813d6fed487d06959f18f19e982639e08a95d9dc Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 4 May 2022 09:00:54 +0300 Subject: [PATCH 16/27] Split activation keybind to separate increase/decrease keybinds --- .../Input/Bindings/GlobalActionContainer.cs | 10 +++-- .../GlobalActionKeyBindingStrings.cs | 9 ++++- .../Edit/DistancedHitObjectComposer.cs | 40 +++++++++++-------- 3 files changed, 38 insertions(+), 21 deletions(-) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 5656c79975..3e7051cbf5 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -79,7 +79,8 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.F5 }, GlobalAction.EditorTestGameplay), new KeyBinding(new[] { InputKey.Control, InputKey.H }, GlobalAction.EditorFlipHorizontally), new KeyBinding(new[] { InputKey.Control, InputKey.J }, GlobalAction.EditorFlipVertically), - new KeyBinding(new[] { InputKey.Control, InputKey.Alt }, GlobalAction.EditorDistanceSpacing), + new KeyBinding(new[] { InputKey.Control, InputKey.Alt, InputKey.MouseWheelDown }, GlobalAction.EditorDecreaseDistanceSpacing), + new KeyBinding(new[] { InputKey.Control, InputKey.Alt, InputKey.MouseWheelUp }, GlobalAction.EditorIncreaseDistanceSpacing), }; public IEnumerable InGameKeyBindings => new[] @@ -303,7 +304,10 @@ namespace osu.Game.Input.Bindings [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorFlipVertically))] EditorFlipVertically, - [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorDistanceSpacing))] - EditorDistanceSpacing, + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorIncreaseDistanceSpacing))] + EditorIncreaseDistanceSpacing, + + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorDecreaseDistanceSpacing))] + EditorDecreaseDistanceSpacing, } } diff --git a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs index 73cc7546ab..5086e7f4c7 100644 --- a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs +++ b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs @@ -240,9 +240,14 @@ namespace osu.Game.Localisation public static LocalisableString EditorFlipVertically => new TranslatableString(getKey(@"editor_flip_vertically"), @"Flip selection vertically"); /// - /// "Distance grid spacing (hold)" + /// "Increase distance grid spacing" /// - public static LocalisableString EditorDistanceSpacing => new TranslatableString(getKey(@"editor_distance_spacing"), @"Distance grid spacing (hold)"); + public static LocalisableString EditorIncreaseDistanceSpacing => new TranslatableString(getKey(@"editor_increase_distance_spacing"), @"Increase distance grid spacing"); + + /// + /// "Decrease distance grid spacing" + /// + public static LocalisableString EditorDecreaseDistanceSpacing => new TranslatableString(getKey(@"editor_decrease_distance_spacing"), @"Decrease distance grid spacing"); /// /// "Toggle skin editor" diff --git a/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs b/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs index 1a4ea845d0..e6bc36622f 100644 --- a/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Edit /// /// The base type of supported objects. [Cached(typeof(IDistanceSnapProvider))] - public abstract class DistancedHitObjectComposer : HitObjectComposer, IDistanceSnapProvider, IKeyBindingHandler + public abstract class DistancedHitObjectComposer : HitObjectComposer, IDistanceSnapProvider, IScrollBindingHandler where TObject : HitObject { protected Bindable DistanceSpacingMultiplier { get; } = new BindableDouble(1.0) @@ -35,7 +35,6 @@ namespace osu.Game.Rulesets.Edit protected ExpandingToolboxContainer RightSideToolboxContainer { get; private set; } private ExpandableSlider> distanceSpacingSlider; - private bool distanceSpacingScrollActive; protected DistancedHitObjectComposer(Ruleset ruleset) : base(ruleset) @@ -79,11 +78,11 @@ namespace osu.Game.Rulesets.Edit public bool OnPressed(KeyBindingPressEvent e) { - if (!DistanceSpacingMultiplier.Disabled && e.Action == GlobalAction.EditorDistanceSpacing) + switch (e.Action) { - RightSideToolboxContainer.Expanded.Value = true; - distanceSpacingScrollActive = true; - return true; + case GlobalAction.EditorIncreaseDistanceSpacing: + case GlobalAction.EditorDecreaseDistanceSpacing: + return adjustDistanceSpacing(e.Action, 0.1f); } return false; @@ -91,22 +90,31 @@ namespace osu.Game.Rulesets.Edit public void OnReleased(KeyBindingReleaseEvent e) { - if (!DistanceSpacingMultiplier.Disabled && e.Action == GlobalAction.EditorDistanceSpacing) - { - RightSideToolboxContainer.Expanded.Value = false; - distanceSpacingScrollActive = false; - } } - protected override bool OnScroll(ScrollEvent e) + public bool OnScroll(KeyBindingScrollEvent e) { - if (distanceSpacingScrollActive) + switch (e.Action) { - DistanceSpacingMultiplier.Value += e.ScrollDelta.Y * (e.IsPrecise ? 0.01f : 0.1f); - return true; + case GlobalAction.EditorIncreaseDistanceSpacing: + case GlobalAction.EditorDecreaseDistanceSpacing: + return adjustDistanceSpacing(e.Action, e.IsPrecise ? 0.01f : 0.1f); } - return base.OnScroll(e); + return false; + } + + private bool adjustDistanceSpacing(GlobalAction action, float amount) + { + if (DistanceSpacingMultiplier.Disabled) + return false; + + if (action == GlobalAction.EditorIncreaseDistanceSpacing) + DistanceSpacingMultiplier.Value += amount; + else if (action == GlobalAction.EditorDecreaseDistanceSpacing) + DistanceSpacingMultiplier.Value -= amount; + + return true; } public virtual float GetBeatSnapDistanceAt(HitObject referenceObject) From 504ca5be31d011b8909ea05518265529758a63fb Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 4 May 2022 09:51:53 +0300 Subject: [PATCH 17/27] Fix scrolling no longer adjusting distance spacing by amount --- osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs b/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs index e6bc36622f..66a78640c6 100644 --- a/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs @@ -98,7 +98,7 @@ namespace osu.Game.Rulesets.Edit { case GlobalAction.EditorIncreaseDistanceSpacing: case GlobalAction.EditorDecreaseDistanceSpacing: - return adjustDistanceSpacing(e.Action, e.IsPrecise ? 0.01f : 0.1f); + return adjustDistanceSpacing(e.Action, e.ScrollAmount * (e.IsPrecise ? 0.01f : 0.1f)); } return false; From 732739715aee8149791a0f2bc35fa4b5d14ba3c7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 May 2022 16:31:20 +0900 Subject: [PATCH 18/27] Remove "grid" from strings --- osu.Game/Localisation/GlobalActionKeyBindingStrings.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs index 5086e7f4c7..58afb284b5 100644 --- a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs +++ b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs @@ -240,14 +240,14 @@ namespace osu.Game.Localisation public static LocalisableString EditorFlipVertically => new TranslatableString(getKey(@"editor_flip_vertically"), @"Flip selection vertically"); /// - /// "Increase distance grid spacing" + /// "Increase distance spacing" /// - public static LocalisableString EditorIncreaseDistanceSpacing => new TranslatableString(getKey(@"editor_increase_distance_spacing"), @"Increase distance grid spacing"); + public static LocalisableString EditorIncreaseDistanceSpacing => new TranslatableString(getKey(@"editor_increase_distance_spacing"), @"Increase distance spacing"); /// - /// "Decrease distance grid spacing" + /// "Decrease distance spacing" /// - public static LocalisableString EditorDecreaseDistanceSpacing => new TranslatableString(getKey(@"editor_decrease_distance_spacing"), @"Decrease distance grid spacing"); + public static LocalisableString EditorDecreaseDistanceSpacing => new TranslatableString(getKey(@"editor_decrease_distance_spacing"), @"Decrease distance spacing"); /// /// "Toggle skin editor" From a66743266f6460dc7c50ad873e2b231a813a8d0b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 May 2022 17:20:58 +0900 Subject: [PATCH 19/27] Remove unused `ScrollingToolboxGroup` class --- .../Rulesets/Edit/ScrollingToolboxGroup.cs | 34 ------------------- 1 file changed, 34 deletions(-) delete mode 100644 osu.Game/Rulesets/Edit/ScrollingToolboxGroup.cs diff --git a/osu.Game/Rulesets/Edit/ScrollingToolboxGroup.cs b/osu.Game/Rulesets/Edit/ScrollingToolboxGroup.cs deleted file mode 100644 index 98e026c49a..0000000000 --- a/osu.Game/Rulesets/Edit/ScrollingToolboxGroup.cs +++ /dev/null @@ -1,34 +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 osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Game.Graphics.Containers; - -namespace osu.Game.Rulesets.Edit -{ - public class ScrollingToolboxGroup : EditorToolboxGroup - { - protected readonly OsuScrollContainer Scroll; - - protected readonly FillFlowContainer FillFlow; - - protected override Container Content { get; } - - public ScrollingToolboxGroup(string title, float scrollAreaHeight) - : base(title) - { - base.Content.Add(Scroll = new OsuScrollContainer - { - RelativeSizeAxes = Axes.X, - Height = scrollAreaHeight, - Child = Content = FillFlow = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - }, - }); - } - } -} From b325f0ee0b3ebc286f13ba82d95e590707ece455 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 May 2022 17:41:29 +0900 Subject: [PATCH 20/27] Combine editor toolbox container implementation and fix input blocking Until now, toolbox scroll areas would block input from arriving behind them, even when no visible element was clicked. In addition, clicking on a button inside a toolbox would still send a `MouseDown` event to things behind it. Specifically, the editor's `HitObjectComposer` would receive these events and also place objects when the user does not expect them to be placed. This fixes another regression that occurred due to `ScrollContainer`s no longer blocking input theirselves. --- .../Edit/DistancedHitObjectComposer.cs | 19 ++--------- .../Edit/ExpandingToolboxContainer.cs | 34 +++++++++++++++++++ osu.Game/Rulesets/Edit/HitObjectComposer.cs | 16 ++------- 3 files changed, 38 insertions(+), 31 deletions(-) create mode 100644 osu.Game/Rulesets/Edit/ExpandingToolboxContainer.cs diff --git a/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs b/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs index 66a78640c6..370e8ccbda 100644 --- a/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs @@ -6,12 +6,10 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; -using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Overlays.Settings.Sections; using osu.Game.Rulesets.Objects; -using osuTK; namespace osu.Game.Rulesets.Edit { @@ -44,8 +42,9 @@ namespace osu.Game.Rulesets.Edit [BackgroundDependencyLoader] private void load() { - AddInternal(RightSideToolboxContainer = new ExpandingToolboxContainer + AddInternal(RightSideToolboxContainer = new ExpandingToolboxContainer(130, 250) { + Padding = new MarginPadding { Right = 10 }, Alpha = DistanceSpacingMultiplier.Disabled ? 0 : 1, Anchor = Anchor.TopRight, Origin = Anchor.TopRight, @@ -154,19 +153,5 @@ namespace osu.Game.Rulesets.Edit return DurationToDistance(referenceObject, snappedEndTime - startTime); } - - protected class ExpandingToolboxContainer : ExpandingContainer - { - protected override double HoverExpansionDelay => 250; - - public ExpandingToolboxContainer() - : base(130, 250) - { - RelativeSizeAxes = Axes.Y; - Padding = new MarginPadding { Left = 10 }; - - FillFlow.Spacing = new Vector2(10); - } - } } } diff --git a/osu.Game/Rulesets/Edit/ExpandingToolboxContainer.cs b/osu.Game/Rulesets/Edit/ExpandingToolboxContainer.cs new file mode 100644 index 0000000000..e807dbd482 --- /dev/null +++ b/osu.Game/Rulesets/Edit/ExpandingToolboxContainer.cs @@ -0,0 +1,34 @@ +// 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 osu.Framework.Graphics; +using osu.Framework.Input.Events; +using osu.Game.Graphics.Containers; +using osuTK; + +namespace osu.Game.Rulesets.Edit +{ + public class ExpandingToolboxContainer : ExpandingContainer + { + protected override double HoverExpansionDelay => 250; + + public ExpandingToolboxContainer(float contractedWidth, float expandedWidth) + : base(contractedWidth, expandedWidth) + { + RelativeSizeAxes = Axes.Y; + + FillFlow.Spacing = new Vector2(10); + } + + protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) => base.ReceivePositionalInputAtSubTree(screenSpacePos) && anyToolboxHovered(screenSpacePos); + + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => base.ReceivePositionalInputAt(screenSpacePos) && anyToolboxHovered(screenSpacePos); + + private bool anyToolboxHovered(Vector2 screenSpacePos) => FillFlow.Children.Any(d => d.ScreenSpaceDrawQuad.Contains(screenSpacePos)); + + protected override bool OnMouseDown(MouseDownEvent e) => true; + + protected override bool OnClick(ClickEvent e) => true; + } +} diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index a235a5bc60..1c388fe68f 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -13,7 +13,6 @@ using osu.Framework.Input; using osu.Framework.Input.Events; using osu.Framework.Logging; using osu.Game.Beatmaps; -using osu.Game.Graphics.Containers; using osu.Game.Rulesets.Configuration; using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Mods; @@ -115,8 +114,9 @@ namespace osu.Game.Rulesets.Edit .WithChild(BlueprintContainer = CreateBlueprintContainer()) } }, - new LeftToolboxFlow + new ExpandingToolboxContainer(80, 200) { + Padding = new MarginPadding { Left = 10 }, Children = new Drawable[] { new EditorToolboxGroup("toolbox (1-9)") @@ -382,18 +382,6 @@ namespace osu.Game.Rulesets.Edit } #endregion - - private class LeftToolboxFlow : ExpandingButtonContainer - { - public LeftToolboxFlow() - : base(80, 200) - { - RelativeSizeAxes = Axes.Y; - Padding = new MarginPadding { Right = 10 }; - - FillFlow.Spacing = new Vector2(10); - } - } } /// From 0b8fd2e39f9c4e93d91c43f93da1c08e27244f33 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 4 May 2022 11:41:33 +0300 Subject: [PATCH 21/27] Improve distance spacing toast inline with key binding changes --- .../Edit/DistancedHitObjectComposer.cs | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs b/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs index afd6909874..466b6cba4b 100644 --- a/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs @@ -3,11 +3,13 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Localisation; +using osu.Game.Configuration; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; @@ -80,7 +82,7 @@ namespace osu.Game.Rulesets.Edit distanceSpacingSlider.ExpandedLabelText = $"Distance Spacing ({v.NewValue:0.##x})"; if (v.NewValue != v.OldValue) - onScreenDisplay?.Display(new DistanceSpacingToast(v.NewValue.ToLocalisableString(@"0.##x"))); + onScreenDisplay?.Display(new DistanceSpacingToast(v.NewValue.ToLocalisableString(@"0.##x"), v)); EditorBeatmap.BeatmapInfo.DistanceSpacing = v.NewValue; }, true); @@ -182,10 +184,23 @@ namespace osu.Game.Rulesets.Edit private class DistanceSpacingToast : Toast { - public DistanceSpacingToast(LocalisableString value) - : base("Distance Spacing", value, string.Empty) + private readonly ValueChangedEvent change; + + public DistanceSpacingToast(LocalisableString value, ValueChangedEvent change) + : base(getAction(change).GetLocalisableDescription(), value, string.Empty) { + this.change = change; } + + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + ShortcutText.Text = config.LookupKeyBindings(getAction(change)); + } + + private static GlobalAction getAction(ValueChangedEvent change) => change.NewValue - change.OldValue > 0 + ? GlobalAction.EditorIncreaseDistanceSpacing + : GlobalAction.EditorDecreaseDistanceSpacing; } } } From f899c3e68fa5ad4eb53196de2dd509fd3d9d46b8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 May 2022 17:56:38 +0900 Subject: [PATCH 22/27] Add test coverage of circle placement around editor toolboxes --- .../Editing/TestSceneHitObjectComposer.cs | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs index ae1b691767..231a1f408f 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs @@ -19,6 +19,7 @@ using osu.Game.Rulesets.Osu.Edit; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Components.RadioButtons; +using osu.Game.Screens.Edit.Components.TernaryButtons; using osu.Game.Screens.Edit.Compose.Components; using osuTK; using osuTK.Input; @@ -87,6 +88,65 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("Tool changed", () => hitObjectComposer.ChildrenOfType().First().CurrentTool is HitCircleCompositionTool); } + [Test] + public void TestPlacementFailsWhenClickingButton() + { + AddStep("clear all control points and hitobjects", () => + { + editorBeatmap.ControlPointInfo.Clear(); + editorBeatmap.Clear(); + }); + + AddStep("Add timing point", () => editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint())); + + AddStep("Change to hitcircle", () => hitObjectComposer.ChildrenOfType().First(d => d.Button.Label == "HitCircle").TriggerClick()); + + AddStep("move mouse to overlapping toggle button", () => + { + var playfield = hitObjectComposer.Playfield.ScreenSpaceDrawQuad; + var button = hitObjectComposer + .ChildrenOfType().First() + .ChildrenOfType().First(b => playfield.Contains(b.ScreenSpaceDrawQuad.Centre)); + + InputManager.MoveMouseTo(button); + }); + + AddAssert("no circles placed", () => editorBeatmap.HitObjects.Count == 0); + + AddStep("attempt place circle", () => InputManager.Click(MouseButton.Left)); + + AddAssert("no circles placed", () => editorBeatmap.HitObjects.Count == 0); + } + + [Test] + public void TestPlacementWithinToolboxScrollArea() + { + AddStep("clear all control points and hitobjects", () => + { + editorBeatmap.ControlPointInfo.Clear(); + editorBeatmap.Clear(); + }); + + AddStep("Add timing point", () => editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint())); + + AddStep("Change to hitcircle", () => hitObjectComposer.ChildrenOfType().First(d => d.Button.Label == "HitCircle").TriggerClick()); + + AddStep("move mouse to scroll area", () => + { + // Specifically wanting to test the area of overlap between the left expanding toolbox container + // and the playfield/composer. + var scrollArea = hitObjectComposer.ChildrenOfType().First().ScreenSpaceDrawQuad; + var playfield = hitObjectComposer.Playfield.ScreenSpaceDrawQuad; + InputManager.MoveMouseTo(new Vector2(scrollArea.TopLeft.X, playfield.Centre.Y)); + }); + + AddAssert("no circles placed", () => editorBeatmap.HitObjects.Count == 0); + + AddStep("place circle", () => InputManager.Click(MouseButton.Left)); + + AddAssert("circle placed", () => editorBeatmap.HitObjects.Count == 1); + } + [Test] public void TestDistanceSpacingHotkeys() { From f5d4f02200aa1118485de0eddfa977aa4ec09325 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 4 May 2022 11:59:29 +0300 Subject: [PATCH 23/27] Use `ToUpper` for key binding text --- osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs b/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs index 466b6cba4b..3ca443cc85 100644 --- a/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs @@ -195,7 +195,7 @@ namespace osu.Game.Rulesets.Edit [BackgroundDependencyLoader] private void load(OsuConfigManager config) { - ShortcutText.Text = config.LookupKeyBindings(getAction(change)); + ShortcutText.Text = config.LookupKeyBindings(getAction(change)).ToUpper(); } private static GlobalAction getAction(ValueChangedEvent change) => change.NewValue - change.OldValue > 0 From 4e0f899159aebc9b5de32b04faefc7f98f8cc654 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 May 2022 18:13:30 +0900 Subject: [PATCH 24/27] Rename value changed variable --- osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs b/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs index 3ca443cc85..081610a62b 100644 --- a/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs @@ -76,15 +76,15 @@ namespace osu.Game.Rulesets.Edit if (!DistanceSpacingMultiplier.Disabled) { DistanceSpacingMultiplier.Value = EditorBeatmap.BeatmapInfo.DistanceSpacing; - DistanceSpacingMultiplier.BindValueChanged(v => + DistanceSpacingMultiplier.BindValueChanged(multiplier => { - distanceSpacingSlider.ContractedLabelText = $"D. S. ({v.NewValue:0.##x})"; - distanceSpacingSlider.ExpandedLabelText = $"Distance Spacing ({v.NewValue:0.##x})"; + distanceSpacingSlider.ContractedLabelText = $"D. S. ({multiplier.NewValue:0.##x})"; + distanceSpacingSlider.ExpandedLabelText = $"Distance Spacing ({multiplier.NewValue:0.##x})"; - if (v.NewValue != v.OldValue) - onScreenDisplay?.Display(new DistanceSpacingToast(v.NewValue.ToLocalisableString(@"0.##x"), v)); + if (multiplier.NewValue != multiplier.OldValue) + onScreenDisplay?.Display(new DistanceSpacingToast(multiplier.NewValue.ToLocalisableString(@"0.##x"), multiplier)); - EditorBeatmap.BeatmapInfo.DistanceSpacing = v.NewValue; + EditorBeatmap.BeatmapInfo.DistanceSpacing = multiplier.NewValue; }, true); } } From a759e641ebf17f71cbd825491fb34fc95045f382 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 4 May 2022 11:56:29 +0200 Subject: [PATCH 25/27] Enforce composer aspect ratio to avoid depending on ambient window size --- osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs index 231a1f408f..fb179a448c 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs @@ -71,6 +71,11 @@ namespace osu.Game.Tests.Visual.Editing Child = editorBeatmapContainer = new EditorBeatmapContainer(Beatmap.Value) { Child = hitObjectComposer = new OsuHitObjectComposer(new OsuRuleset()) + { + // force the composer to fully overlap the playfield area by setting a 4:3 aspect ratio. + FillMode = FillMode.Fit, + FillAspectRatio = 4 / 3f + } }; }); } From 6380216263c0cff2c46417322362da11631aa63e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 4 May 2022 11:56:44 +0200 Subject: [PATCH 26/27] Nudge click location to avoid placement failures --- osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs index fb179a448c..c9d44fdab7 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs @@ -142,7 +142,7 @@ namespace osu.Game.Tests.Visual.Editing // and the playfield/composer. var scrollArea = hitObjectComposer.ChildrenOfType().First().ScreenSpaceDrawQuad; var playfield = hitObjectComposer.Playfield.ScreenSpaceDrawQuad; - InputManager.MoveMouseTo(new Vector2(scrollArea.TopLeft.X, playfield.Centre.Y)); + InputManager.MoveMouseTo(new Vector2(scrollArea.TopLeft.X + 1, playfield.Centre.Y)); }); AddAssert("no circles placed", () => editorBeatmap.HitObjects.Count == 0); From 3eead5a6a39c3add1966990f280d655c2d71f3c2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 May 2022 19:40:08 +0900 Subject: [PATCH 27/27] Rename `FlushAnimation` to `FlushPendingSelections` to better match purpose --- osu.Game/Overlays/Mods/ModColumn.cs | 4 ++-- osu.Game/Overlays/Mods/ModSection.cs | 4 ++-- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 2 +- osu.Game/Overlays/Mods/ModSelectScreen.cs | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs index f9332dd28b..a792c0a81e 100644 --- a/osu.Game/Overlays/Mods/ModColumn.cs +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -435,9 +435,9 @@ namespace osu.Game.Overlays.Mods } /// - /// Play out all remaining animations immediately to leave mods in a good (final) state. + /// Run any delayed selections (due to animation) immediately to leave mods in a good (final) state. /// - public void FlushAnimation() + public void FlushPendingSelections() { while (pendingSelectionOperations.TryDequeue(out var dequeuedAction)) dequeuedAction(); diff --git a/osu.Game/Overlays/Mods/ModSection.cs b/osu.Game/Overlays/Mods/ModSection.cs index 5bf8cddd0c..a70191a864 100644 --- a/osu.Game/Overlays/Mods/ModSection.cs +++ b/osu.Game/Overlays/Mods/ModSection.cs @@ -250,9 +250,9 @@ namespace osu.Game.Overlays.Mods protected virtual ModButton CreateModButton(Mod mod) => new ModButton(mod); /// - /// Play out all remaining animations immediately to leave mods in a good (final) state. + /// Run any delayed selections (due to animation) immediately to leave mods in a good (final) state. /// - public void FlushAnimation() + public void FlushPendingSelections() { while (pendingSelectionOperations.TryDequeue(out var dequeuedAction)) dequeuedAction(); diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 9ce79c25f7..cf57322594 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -369,7 +369,7 @@ namespace osu.Game.Overlays.Mods foreach (var section in ModSectionsContainer) { - section.FlushAnimation(); + section.FlushPendingSelections(); } FooterContainer.MoveToX(content_width, WaveContainer.DISAPPEAR_DURATION, Easing.InSine); diff --git a/osu.Game/Overlays/Mods/ModSelectScreen.cs b/osu.Game/Overlays/Mods/ModSelectScreen.cs index bb12143d7c..8060bca65f 100644 --- a/osu.Game/Overlays/Mods/ModSelectScreen.cs +++ b/osu.Game/Overlays/Mods/ModSelectScreen.cs @@ -308,7 +308,7 @@ namespace osu.Game.Overlays.Mods var column = columnFlow[i].Column; - column.FlushAnimation(); + column.FlushPendingSelections(); column.TopLevelContent .MoveToY(i % 2 == 0 ? -distance : distance, fade_out_duration, Easing.OutQuint) .FadeOut(fade_out_duration, Easing.OutQuint);