Merge pull request #13908 from peppy/editor-disallow-placement-when-untimed

Fix editor composer allowing object placement without timing present
This commit is contained in:
Dan Balasescu 2021-07-19 18:37:19 +09:00 committed by GitHub
commit 473011070f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 141 additions and 54 deletions

View File

@ -13,8 +13,8 @@ namespace osu.Game.Tests.Visual.Editing
{
public TestSceneEditorComposeRadioButtons()
{
RadioButtonCollection collection;
Add(collection = new RadioButtonCollection
EditorRadioButtonCollection collection;
Add(collection = new EditorRadioButtonCollection
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,

View File

@ -2,17 +2,24 @@
// 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;
using osu.Framework.Timing;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu;
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.Compose.Components;
using osuTK;
namespace osu.Game.Tests.Visual.Editing
@ -20,37 +27,89 @@ namespace osu.Game.Tests.Visual.Editing
[TestFixture]
public class TestSceneHitObjectComposer : EditorClockTestScene
{
[BackgroundDependencyLoader]
private void load()
private OsuHitObjectComposer hitObjectComposer;
private EditorBeatmapContainer editorBeatmapContainer;
private EditorBeatmap editorBeatmap => editorBeatmapContainer.EditorBeatmap;
[SetUpSteps]
public void SetUpSteps()
{
Beatmap.Value = CreateWorkingBeatmap(new Beatmap
AddStep("create beatmap", () =>
{
HitObjects = new List<HitObject>
Beatmap.Value = CreateWorkingBeatmap(new Beatmap
{
new HitCircle { Position = new Vector2(256, 192), Scale = 0.5f },
new HitCircle { Position = new Vector2(344, 148), Scale = 0.5f },
new Slider
HitObjects = new List<HitObject>
{
Position = new Vector2(128, 256),
Path = new SliderPath(PathType.Linear, new[]
new HitCircle { Position = new Vector2(256, 192), Scale = 0.5f },
new HitCircle { Position = new Vector2(344, 148), Scale = 0.5f },
new Slider
{
Vector2.Zero,
new Vector2(216, 0),
}),
Scale = 0.5f,
}
},
Position = new Vector2(128, 256),
Path = new SliderPath(PathType.Linear, new[]
{
Vector2.Zero,
new Vector2(216, 0),
}),
Scale = 0.5f,
}
},
});
});
var editorBeatmap = new EditorBeatmap(Beatmap.Value.GetPlayableBeatmap(new OsuRuleset().RulesetInfo));
AddStep("Create composer", () =>
{
Child = editorBeatmapContainer = new EditorBeatmapContainer(Beatmap.Value)
{
Child = hitObjectComposer = new OsuHitObjectComposer(new OsuRuleset())
};
});
}
var clock = new DecoupleableInterpolatingFramedClock { IsCoupled = false };
Dependencies.CacheAs<IAdjustableClock>(clock);
Dependencies.CacheAs<IFrameBasedClock>(clock);
Dependencies.CacheAs(editorBeatmap);
Dependencies.CacheAs<IBeatSnapProvider>(editorBeatmap);
[Test]
public void TestPlacementOnlyWorksWithTiming()
{
AddStep("clear all control points", () => editorBeatmap.ControlPointInfo.Clear());
Child = new OsuHitObjectComposer(new OsuRuleset());
AddAssert("Tool is selection", () => hitObjectComposer.ChildrenOfType<ComposeBlueprintContainer>().First().CurrentTool is SelectTool);
AddAssert("Hitcircle button not clickable", () => !hitObjectComposer.ChildrenOfType<EditorRadioButton>().First(d => d.Button.Label == "HitCircle").Enabled.Value);
AddStep("Add timing point", () => editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint()));
AddAssert("Hitcircle button is clickable", () => hitObjectComposer.ChildrenOfType<EditorRadioButton>().First(d => d.Button.Label == "HitCircle").Enabled.Value);
AddStep("Change to hitcircle", () => hitObjectComposer.ChildrenOfType<EditorRadioButton>().First(d => d.Button.Label == "HitCircle").Click());
AddAssert("Tool changed", () => hitObjectComposer.ChildrenOfType<ComposeBlueprintContainer>().First().CurrentTool is HitCircleCompositionTool);
}
public class EditorBeatmapContainer : Container
{
private readonly WorkingBeatmap working;
public EditorBeatmap EditorBeatmap { get; private set; }
public EditorBeatmapContainer(WorkingBeatmap working)
{
this.working = working;
RelativeSizeAxes = Axes.Both;
}
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
EditorBeatmap = new EditorBeatmap(working.GetPlayableBeatmap(new OsuRuleset().RulesetInfo));
dependencies.CacheAs(EditorBeatmap);
dependencies.CacheAs<IBeatSnapProvider>(EditorBeatmap);
return dependencies;
}
protected override void LoadComplete()
{
base.LoadComplete();
Add(EditorBeatmap);
}
}
}
}

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
@ -63,10 +64,12 @@ namespace osu.Game.Rulesets.Edit
private InputManager inputManager;
private RadioButtonCollection toolboxCollection;
private EditorRadioButtonCollection toolboxCollection;
private FillFlowContainer togglesCollection;
private IBindable<bool> hasTiming;
protected HitObjectComposer(Ruleset ruleset)
{
Ruleset = ruleset;
@ -126,7 +129,7 @@ namespace osu.Game.Rulesets.Edit
{
new ToolboxGroup("toolbox (1-9)")
{
Child = toolboxCollection = new RadioButtonCollection { RelativeSizeAxes = Axes.X }
Child = toolboxCollection = new EditorRadioButtonCollection { RelativeSizeAxes = Axes.X }
},
new ToolboxGroup("toggles (Q~P)")
{
@ -160,6 +163,14 @@ namespace osu.Game.Rulesets.Edit
base.LoadComplete();
inputManager = GetContainingInputManager();
hasTiming = EditorBeatmap.HasTiming.GetBoundCopy();
hasTiming.BindValueChanged(timing =>
{
// it's important this is performed before the similar code in EditorRadioButton disables the button.
if (!timing.NewValue)
setSelectTool();
});
}
public override Playfield Playfield => drawableRulesetWrapper.Playfield;
@ -219,7 +230,8 @@ namespace osu.Game.Rulesets.Edit
if (item != null)
{
item.Select();
if (!item.Selected.Disabled)
item.Select();
return true;
}
}

View File

@ -5,9 +5,11 @@ using System;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
@ -16,26 +18,30 @@ using osuTK.Graphics;
namespace osu.Game.Screens.Edit.Components.RadioButtons
{
public class DrawableRadioButton : OsuButton
public class EditorRadioButton : OsuButton, IHasTooltip
{
/// <summary>
/// Invoked when this <see cref="DrawableRadioButton"/> has been selected.
/// Invoked when this <see cref="EditorRadioButton"/> has been selected.
/// </summary>
public Action<RadioButton> Selected;
public readonly RadioButton Button;
private Color4 defaultBackgroundColour;
private Color4 defaultBubbleColour;
private Color4 selectedBackgroundColour;
private Color4 selectedBubbleColour;
private Drawable icon;
private readonly RadioButton button;
public DrawableRadioButton(RadioButton button)
[Resolved(canBeNull: true)]
private EditorBeatmap editorBeatmap { get; set; }
public EditorRadioButton(RadioButton button)
{
this.button = button;
Button = button;
Text = button.Item.ToString();
Text = button.Label;
Action = button.Select;
RelativeSizeAxes = Axes.X;
@ -57,7 +63,7 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons
Colour = Color4.Black.Opacity(0.5f)
};
Add(icon = (button.CreateIcon?.Invoke() ?? new Circle()).With(b =>
Add(icon = (Button.CreateIcon?.Invoke() ?? new Circle()).With(b =>
{
b.Blending = BlendingParameters.Additive;
b.Anchor = Anchor.CentreLeft;
@ -71,13 +77,16 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons
{
base.LoadComplete();
button.Selected.ValueChanged += selected =>
Button.Selected.ValueChanged += selected =>
{
updateSelectionState();
if (selected.NewValue)
Selected?.Invoke(button);
Selected?.Invoke(Button);
};
editorBeatmap?.HasTiming.BindValueChanged(hasTiming => Button.Selected.Disabled = !hasTiming.NewValue, true);
Button.Selected.BindDisabledChanged(disabled => Enabled.Value = !disabled, true);
updateSelectionState();
}
@ -86,8 +95,8 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons
if (!IsLoaded)
return;
BackgroundColour = button.Selected.Value ? selectedBackgroundColour : defaultBackgroundColour;
icon.Colour = button.Selected.Value ? selectedBubbleColour : defaultBubbleColour;
BackgroundColour = Button.Selected.Value ? selectedBackgroundColour : defaultBackgroundColour;
icon.Colour = Button.Selected.Value ? selectedBubbleColour : defaultBubbleColour;
}
protected override SpriteText CreateText() => new OsuSpriteText
@ -97,5 +106,7 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons
Anchor = Anchor.CentreLeft,
X = 40f
};
public LocalisableString TooltipText => Enabled.Value ? string.Empty : "Add at least one timing point first!";
}
}

View File

@ -9,7 +9,7 @@ using osuTK;
namespace osu.Game.Screens.Edit.Components.RadioButtons
{
public class RadioButtonCollection : CompositeDrawable
public class EditorRadioButtonCollection : CompositeDrawable
{
private IReadOnlyList<RadioButton> items;
@ -28,13 +28,13 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons
}
}
private readonly FlowContainer<DrawableRadioButton> buttonContainer;
private readonly FlowContainer<EditorRadioButton> buttonContainer;
public RadioButtonCollection()
public EditorRadioButtonCollection()
{
AutoSizeAxes = Axes.Y;
InternalChild = buttonContainer = new FillFlowContainer<DrawableRadioButton>
InternalChild = buttonContainer = new FillFlowContainer<EditorRadioButton>
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
@ -58,7 +58,7 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons
currentlySelected = null;
};
buttonContainer.Add(new DrawableRadioButton(button));
buttonContainer.Add(new EditorRadioButton(button));
}
}
}

View File

@ -17,7 +17,7 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons
/// <summary>
/// The item related to this button.
/// </summary>
public object Item;
public string Label;
/// <summary>
/// A function which creates a drawable icon to represent this item. If null, a sane default should be used.
@ -26,21 +26,14 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons
private readonly Action action;
public RadioButton(object item, Action action, Func<Drawable> createIcon = null)
public RadioButton(string label, Action action, Func<Drawable> createIcon = null)
{
Item = item;
Label = label;
CreateIcon = createIcon;
this.action = action;
Selected = new BindableBool();
}
public RadioButton(string item)
: this(item, null)
{
Item = item;
action = null;
}
/// <summary>
/// Selects this <see cref="RadioButton"/>.
/// </summary>

View File

@ -46,12 +46,22 @@ namespace osu.Game.Screens.Edit
public readonly IBeatmap PlayableBeatmap;
/// <summary>
/// Whether at least one timing control point is present and providing timing information.
/// </summary>
public IBindable<bool> HasTiming => hasTiming;
private readonly Bindable<bool> hasTiming = new Bindable<bool>();
[CanBeNull]
public readonly ISkin BeatmapSkin;
[Resolved]
private BindableBeatDivisor beatDivisor { get; set; }
[Resolved]
private EditorClock editorClock { get; set; }
private readonly IBeatmapProcessor beatmapProcessor;
private readonly Dictionary<HitObject, Bindable<double>> startTimeBindables = new Dictionary<HitObject, Bindable<double>>();
@ -238,6 +248,8 @@ namespace osu.Game.Screens.Edit
if (batchPendingUpdates.Count > 0)
UpdateState();
hasTiming.Value = !ReferenceEquals(ControlPointInfo.TimingPointAt(editorClock.CurrentTime), TimingControlPoint.DEFAULT);
}
protected override void UpdateState()