diff --git a/osu.Game.Rulesets.Karaoke.Tests/Screens/Edit/Beatmap/Lyrics/Content/TestSceneInteractableLyric.cs b/osu.Game.Rulesets.Karaoke.Tests/Screens/Edit/Beatmap/Lyrics/Content/TestSceneInteractableLyric.cs new file mode 100644 index 000000000..84d5d11b4 --- /dev/null +++ b/osu.Game.Rulesets.Karaoke.Tests/Screens/Edit/Beatmap/Lyrics/Content/TestSceneInteractableLyric.cs @@ -0,0 +1,390 @@ +// Copyright (c) andy840119 . Licensed under the GPL Licence. +// 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.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Testing; +using osu.Game.Graphics; +using osu.Game.Rulesets.Karaoke.Beatmaps; +using osu.Game.Rulesets.Karaoke.Configuration; +using osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics; +using osu.Game.Rulesets.Karaoke.Objects; +using osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps; +using osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics; +using osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Components.Lyrics; +using osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States; +using osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes; +using osu.Game.Rulesets.Karaoke.Tests.Helper; +using osu.Game.Screens.Edit; +using osu.Game.Tests.Visual; +using osuTK; + +namespace osu.Game.Rulesets.Karaoke.Tests.Screens.Edit.Beatmap.Lyrics.Content; + +public partial class TestSceneInteractableLyric : EditorClockTestScene +{ + private const int border = 36; + + private static readonly Lyric lyric = new() + { + Text = "カラオケ", + TimeTags = TestCaseTagHelper.ParseTimeTags(new[] { "[0,start]:1000#^ka", "[1,start]:2000#ra", "[2,start]:3000#o", "[3,start]:4000#ke", "[3,end]:5000" }), + RubyTags = TestCaseTagHelper.ParseRubyTags(new[] { "[0]:か", "[1]:ら", "[2]:お", "[3]:け" }), + }; + + [Resolved] + private OsuColour colour { get; set; } = null!; + + [Cached(typeof(EditorBeatmap))] + private readonly EditorBeatmap editorBeatmap = new(new KaraokeBeatmap + { + BeatmapInfo = + { + Ruleset = new KaraokeRuleset().RulesetInfo, + }, + }); + + public TestSceneInteractableLyric() + { + editorBeatmap.Add(lyric); + editorBeatmap.SelectedHitObjects.Add(lyric); + } + + [Test] + public void TestGridLayer() + { + AddToggleStep("Add/remove the grid layer", value => + { + if (value) + { + addLoader(new LayerLoader + { + OnLoad = layer => + { + layer.Spacing = 10; + }, + }); + } + else + { + removeLoader(); + } + }); + + AddSliderStep("Change the spacing of the grid", 0, 100, 10, value => + { + changeLayerProperty(layer => + { + layer.Spacing = value; + }); + }); + } + + [Test] + public void TestEditLyricLayer() + { + AddToggleStep("Add/remove the edit lyric layer", value => + { + if (value) + { + addLoader(); + } + else + { + removeLoader(); + } + }); + } + + [Test] + public void TestTimeTagLayer() + { + AddToggleStep("Add/remove the time-tag layer", value => + { + if (value) + { + addLoader(); + } + else + { + removeLoader(); + } + }); + } + + [Test] + public void TestCaretLayer() + { + AddToggleStep("Add/remove the caret layer", value => + { + if (value) + { + addLoader(); + } + else + { + removeLoader(); + } + }); + + AddStep("View mode", () => switchMode(LyricEditorMode.View)); + AddStep("Typing mode", () => switchMode(LyricEditorMode.EditText, TextEditStep.Typing)); + AddStep("Cutting mode", () => switchMode(LyricEditorMode.EditText, TextEditStep.Split)); + AddStep("Edit ruby mode", () => switchMode(LyricEditorMode.EditRuby, RubyTagEditMode.Create)); + AddStep("Edit time-tag mode", () => switchMode(LyricEditorMode.EditTimeTag, TimeTagEditStep.Create)); + AddStep("Record time-tag mode", () => switchMode(LyricEditorMode.EditTimeTag, TimeTagEditStep.Recording)); + } + + [Test] + public void TestBlueprintLayer() + { + AddToggleStep("Add/remove the blueprint layer", value => + { + if (value) + { + addLoader(); + } + else + { + removeLoader(); + } + }); + + AddStep("Ruby blueprint", () => + { + switchMode(LyricEditorMode.EditRuby, RubyTagEditMode.Create); + switchEditRubyModeState(RubyTagEditMode.Modify); + }); + AddStep("Time-tag adjust blueprint", () => + { + switchMode(LyricEditorMode.EditTimeTag, TimeTagEditStep.Adjust); + }); + } + + [BackgroundDependencyLoader] + private void load() + { + Dependencies.CacheAs(new LyricsProvider().With(Add)); + Dependencies.CacheAs(new LyricsChangeHandler().With(Add)); + Dependencies.CacheAs(new LyricTextChangeHandler().With(Add)); + Dependencies.CacheAs(new LyricRubyTagsChangeHandler().With(Add)); + Dependencies.CacheAs(new LyricTimeTagsChangeHandler().With(Add)); + Dependencies.Cache(new KaraokeRulesetLyricEditorConfigManager()); + } + + #region Testing tools + + private void switchMode(LyricEditorMode mode, Enum? step = null) + { + var editorState = this.ChildrenOfType().First(); + editorState.SwitchMode(mode); + + if (step != null) + { + editorState.SwitchEditStep(step); + } + } + + private void switchEditRubyModeState(RubyTagEditMode mode) + { + var editRubyModeState = this.ChildrenOfType().First(); + editRubyModeState.BindableRubyTagEditMode.Value = mode; + } + + private readonly List loaders = new() + { + // note: lyric layer should always in the loader and never be removed. + new LayerLoader + { + OnLoad = layer => + { + layer.LyricPosition = new Vector2(border); + }, + }, + }; + + private LayerLoader? getLoader() + => loaders.FirstOrDefault(l => l.GetType().GenericTypeArguments.First() == typeof(TLayer)); + + private void addLoader(bool reloadView = true) where TLayer : Layer + { + addLoader(new LayerLoader(), reloadView); + } + + private void addLoader(LayerLoader instance, bool reloadView = true) where TLayer : Layer + { + if (getLoader() != null) + { + return; + } + + loaders.Add(instance); + + if (reloadView) + { + updateInteractableLyric(); + } + } + + private void removeLoader(bool reloadView = true) + { + var loader = getLoader(); + + if (loader != null) + { + loaders.Remove(loader); + } + + if (reloadView) + { + updateInteractableLyric(); + } + } + + private void changeLayerProperty(Action action, bool reloadView = true) where TLayer : Layer + { + if (getLoader() == null) + { + return; + } + + removeLoader(false); + addLoader(new LayerLoader + { + OnLoad = action, + }, false); + + if (reloadView) + { + updateInteractableLyric(); + } + } + + private void updateInteractableLyric() + { + RemoveAll(x => x is PopoverContainer, true); + Add(new PopoverContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Scale = new Vector2(2), + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colour.BlueDark, + }, + new MockLyricEditorState + { + AutoSizeAxes = Axes.Both, + Padding = new MarginPadding(48), + Child = createInteractableLyric(loaders.ToArray()), + }, + }, + }); + } + + private static InteractableLyric createInteractableLyric(LayerLoader[] loaders) + { + return new InteractableLyric(lyric) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + TextSizeChanged = (self, size) => + { + self.Width = size.X + border * 2; + self.Height = size.Y + border * 2; + }, + Loaders = reorderLoaders(loaders), + }; + } + + private static LayerLoader[] reorderLoaders(LayerLoader[] loaders) + { + // follow how LyricEditor to change the sort from input loaders + return loaders.OrderBy(x => + { + var type = x.GetType().GenericTypeArguments.First(); + return type.Name switch + { + nameof(GridLayer) => 0, + nameof(LyricLayer) => 1, + nameof(EditLyricLayer) => 2, + nameof(TimeTagLayer) => 3, + nameof(CaretLayer) => 4, + nameof(BlueprintLayer) => 5, + _ => throw new InvalidOperationException(), + }; + }).ToArray(); + } + + #endregion + + /// + /// Follow to create the component with necessary DI. + /// + [Cached(typeof(ILyricEditorState))] + private partial class MockLyricEditorState : Container, ILyricEditorState + { + private readonly Bindable bindableMode = new(); + private readonly Bindable bindableModeWithEditStep = new(); + + public IBindable BindableMode => bindableMode; + + public IBindable BindableModeWithEditStep => bindableModeWithEditStep; + public LyricEditorMode Mode => LyricEditorMode.View; + + [Cached] + private readonly LyricEditorColourProvider colourProvider = new(); + + [Cached(typeof(ILyricCaretState))] + private readonly LyricCaretState lyricCaretState = new(); + + [Cached(typeof(IEditRubyModeState))] + private readonly EditRubyModeState editRubyModeState = new(); + + [Cached(typeof(IEditTimeTagModeState))] + private readonly EditTimeTagModeState editTimeTagModeState = new(); + + /// + /// Add the DI into children here for prevent child is removed if call Children = [...] outside. + /// + protected override void LoadComplete() + { + base.LoadComplete(); + + // global state + AddInternal(lyricCaretState); + + // state for target mode only. + AddInternal(editRubyModeState); + AddInternal(editTimeTagModeState); + } + + public void SwitchMode(LyricEditorMode mode) + => bindableMode.Value = mode; + + public void SwitchEditStep(TEditStep editStep) where TEditStep : Enum + { + bindableModeWithEditStep.Value = new EditorModeWithEditStep + { + Mode = bindableMode.Value, + EditStep = editStep, + Default = false, + }; + } + + public void NavigateToFix(LyricEditorMode mode) + => bindableMode.Value = mode; + } +} diff --git a/osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Components/Lyrics/InteractableLyric.cs b/osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Components/Lyrics/InteractableLyric.cs index 9840c4761..e8354ac92 100644 --- a/osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Components/Lyrics/InteractableLyric.cs +++ b/osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Components/Lyrics/InteractableLyric.cs @@ -44,14 +44,17 @@ public InteractableLyric(Lyric lyric) }); } - public IEnumerable Layers + public IEnumerable Loaders { - get => InternalChildren.OfType(); init { - AddRangeInternal(value); + foreach (var loader in value) + { + var layer = loader.CreateLayer(lyric); + AddInternal(layer); + } - var lyricLayers = value.OfType().Single(); + var lyricLayers = layers.OfType().Single(); lyricLayers.SizeChanged = size => { TextSizeChanged?.Invoke(this, size); @@ -59,18 +62,20 @@ public IEnumerable Layers } } + private IEnumerable layers => InternalChildren.OfType(); + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { var baseDependencies = new DependencyContainer(base.CreateChildDependencies(parent)); - var lyricLayer = Layers.OfType().Single(); + var lyricLayer = layers.OfType().Single(); baseDependencies.CacheAs(lyricLayer); return baseDependencies; } public void TriggerDisallowEditEffect() { - Layers.ForEach(x => x.TriggerDisallowEditEffect(bindableMode.Value)); + layers.ForEach(x => x.TriggerDisallowEditEffect(bindableMode.Value)); } [BackgroundDependencyLoader] @@ -86,7 +91,7 @@ private void triggerWritableVersionChanged() // adjust the style. bool editable = lockReason == null; - Layers.ForEach(x => x.UpdateDisableEditState(editable)); + layers.ForEach(x => x.UpdateDisableEditState(editable)); } public LocalisableString TooltipText => lockReason ?? string.Empty; diff --git a/osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Components/Lyrics/LayerLoader.cs b/osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Components/Lyrics/LayerLoader.cs new file mode 100644 index 000000000..cff34b7ed --- /dev/null +++ b/osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Components/Lyrics/LayerLoader.cs @@ -0,0 +1,25 @@ +// Copyright (c) andy840119 . Licensed under the GPL Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Game.Rulesets.Karaoke.Objects; +using osu.Game.Rulesets.Karaoke.Utils; + +namespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Components.Lyrics; + +public class LayerLoader : LayerLoader where TLayer : Layer +{ + public Action? OnLoad { get; init; } + + public override Layer CreateLayer(Lyric lyric) + { + var layer = ActivatorUtils.CreateInstance(lyric); + OnLoad?.Invoke(layer); + return layer; + } +} + +public abstract class LayerLoader +{ + public abstract Layer CreateLayer(Lyric lyric); +} diff --git a/osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Compose/EditLyricDetailRow.cs b/osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Compose/EditLyricDetailRow.cs index 30bf28674..ec7cfe349 100644 --- a/osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Compose/EditLyricDetailRow.cs +++ b/osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Compose/EditLyricDetailRow.cs @@ -37,11 +37,11 @@ protected override Drawable CreateContent(Lyric lyric) { self.Height = size.Y; }, - Layers = new Layer[] + Loaders = new LayerLoader[] { - new LyricLayer(lyric), - new InteractLyricLayer(lyric), - new TimeTagLayer(lyric), + new LayerLoader(), + new LayerLoader(), + new LayerLoader(), }, }; } diff --git a/osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Compose/LyricEditor.cs b/osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Compose/LyricEditor.cs index f5145abeb..cfbbdf64d 100644 --- a/osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Compose/LyricEditor.cs +++ b/osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Compose/LyricEditor.cs @@ -55,20 +55,26 @@ public LyricEditor() self.Width = size.X + border * 2; self.Height = size.Y + border * 2; }, - Layers = new Layer[] + Loaders = new LayerLoader[] { - new GridLayer(lyric) + new LayerLoader { - Spacing = 10, + OnLoad = layer => + { + layer.Spacing = 10; + }, }, - new LyricLayer(lyric) + new LayerLoader { - LyricPosition = new Vector2(border), + OnLoad = layer => + { + layer.LyricPosition = new Vector2(border); + }, }, - new EditLyricLayer(lyric), - new TimeTagLayer(lyric), - new CaretLayer(lyric), - new BlueprintLayer(lyric), + new LayerLoader(), + new LayerLoader(), + new LayerLoader(), + new LayerLoader(), }, }); }); diff --git a/osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/List/EditLyricPreviewRow.cs b/osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/List/EditLyricPreviewRow.cs index cf1f7aa5d..fe661be18 100644 --- a/osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/List/EditLyricPreviewRow.cs +++ b/osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/List/EditLyricPreviewRow.cs @@ -36,13 +36,13 @@ protected override Drawable CreateContent(Lyric lyric) { self.Height = size.Y; }, - Layers = new Layer[] + Loaders = new LayerLoader[] { - new LyricLayer(lyric), - new EditLyricLayer(lyric), - new TimeTagLayer(lyric), - new CaretLayer(lyric), - new BlueprintLayer(lyric), + new LayerLoader(), + new LayerLoader(), + new LayerLoader(), + new LayerLoader(), + new LayerLoader(), }, }; }