diff --git a/osu.Android.props b/osu.Android.props index a2686c380e..6cbb4b2e68 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game.Tests/Beatmaps/TestSceneEditorBeatmap.cs b/osu.Game.Tests/Beatmaps/TestSceneEditorBeatmap.cs index b7b48ec06a..bf5b517603 100644 --- a/osu.Game.Tests/Beatmaps/TestSceneEditorBeatmap.cs +++ b/osu.Game.Tests/Beatmaps/TestSceneEditorBeatmap.cs @@ -23,15 +23,19 @@ namespace osu.Game.Tests.Beatmaps [Test] public void TestHitObjectAddEvent() { - var editorBeatmap = new EditorBeatmap(new OsuBeatmap()); - - HitObject addedObject = null; - editorBeatmap.HitObjectAdded += h => addedObject = h; - var hitCircle = new HitCircle(); - editorBeatmap.Add(hitCircle); - Assert.That(addedObject, Is.EqualTo(hitCircle)); + HitObject addedObject = null; + EditorBeatmap editorBeatmap = null; + + AddStep("add beatmap", () => + { + Child = editorBeatmap = new EditorBeatmap(new OsuBeatmap()); + editorBeatmap.HitObjectAdded += h => addedObject = h; + }); + + AddStep("add hitobject", () => editorBeatmap.Add(hitCircle)); + AddAssert("received add event", () => addedObject == hitCircle); } /// @@ -41,13 +45,15 @@ namespace osu.Game.Tests.Beatmaps public void HitObjectRemoveEvent() { var hitCircle = new HitCircle(); - var editorBeatmap = new EditorBeatmap(new OsuBeatmap { HitObjects = { hitCircle } }); - HitObject removedObject = null; - editorBeatmap.HitObjectRemoved += h => removedObject = h; - - editorBeatmap.Remove(hitCircle); - Assert.That(removedObject, Is.EqualTo(hitCircle)); + EditorBeatmap editorBeatmap = null; + AddStep("add beatmap", () => + { + Child = editorBeatmap = new EditorBeatmap(new OsuBeatmap { HitObjects = { hitCircle } }); + editorBeatmap.HitObjectRemoved += h => removedObject = h; + }); + AddStep("remove hitobject", () => editorBeatmap.Remove(editorBeatmap.HitObjects.First())); + AddAssert("received remove event", () => removedObject == hitCircle); } /// @@ -147,6 +153,7 @@ namespace osu.Game.Tests.Beatmaps public void TestResortWhenStartTimeChanged() { var hitCircle = new HitCircle { StartTime = 1000 }; + var editorBeatmap = new EditorBeatmap(new OsuBeatmap { HitObjects = diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorChangeStates.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorChangeStates.cs index dfe1e434dc..ab53f4fd93 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorChangeStates.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorChangeStates.cs @@ -1,41 +1,27 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Linq; using NUnit.Framework; -using osu.Framework.Testing; using osu.Game.Rulesets; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Screens.Edit; namespace osu.Game.Tests.Visual.Editing { public class TestSceneEditorChangeStates : EditorTestScene { - private EditorBeatmap editorBeatmap; - protected override Ruleset CreateEditorRuleset() => new OsuRuleset(); - protected new TestEditor Editor => (TestEditor)base.Editor; - - public override void SetUpSteps() - { - base.SetUpSteps(); - - AddStep("get beatmap", () => editorBeatmap = Editor.ChildrenOfType().Single()); - } - [Test] public void TestSelectedObjects() { HitCircle obj = null; - AddStep("add hitobject", () => editorBeatmap.Add(obj = new HitCircle { StartTime = 1000 })); - AddStep("select hitobject", () => editorBeatmap.SelectedHitObjects.Add(obj)); - AddAssert("confirm 1 selected", () => editorBeatmap.SelectedHitObjects.Count == 1); - AddStep("deselect hitobject", () => editorBeatmap.SelectedHitObjects.Remove(obj)); - AddAssert("confirm 0 selected", () => editorBeatmap.SelectedHitObjects.Count == 0); + AddStep("add hitobject", () => EditorBeatmap.Add(obj = new HitCircle { StartTime = 1000 })); + AddStep("select hitobject", () => EditorBeatmap.SelectedHitObjects.Add(obj)); + AddAssert("confirm 1 selected", () => EditorBeatmap.SelectedHitObjects.Count == 1); + AddStep("deselect hitobject", () => EditorBeatmap.SelectedHitObjects.Remove(obj)); + AddAssert("confirm 0 selected", () => EditorBeatmap.SelectedHitObjects.Count == 0); } [Test] @@ -43,11 +29,11 @@ namespace osu.Game.Tests.Visual.Editing { int hitObjectCount = 0; - AddStep("get initial state", () => hitObjectCount = editorBeatmap.HitObjects.Count); + AddStep("get initial state", () => hitObjectCount = EditorBeatmap.HitObjects.Count); addUndoSteps(); - AddAssert("no change occurred", () => hitObjectCount == editorBeatmap.HitObjects.Count); + AddAssert("no change occurred", () => hitObjectCount == EditorBeatmap.HitObjects.Count); AddAssert("no unsaved changes", () => !Editor.HasUnsavedChanges); } @@ -56,11 +42,11 @@ namespace osu.Game.Tests.Visual.Editing { int hitObjectCount = 0; - AddStep("get initial state", () => hitObjectCount = editorBeatmap.HitObjects.Count); + AddStep("get initial state", () => hitObjectCount = EditorBeatmap.HitObjects.Count); addRedoSteps(); - AddAssert("no change occurred", () => hitObjectCount == editorBeatmap.HitObjects.Count); + AddAssert("no change occurred", () => hitObjectCount == EditorBeatmap.HitObjects.Count); AddAssert("no unsaved changes", () => !Editor.HasUnsavedChanges); } @@ -73,11 +59,11 @@ namespace osu.Game.Tests.Visual.Editing AddStep("bind removal", () => { - editorBeatmap.HitObjectAdded += h => addedObject = h; - editorBeatmap.HitObjectRemoved += h => removedObject = h; + EditorBeatmap.HitObjectAdded += h => addedObject = h; + EditorBeatmap.HitObjectRemoved += h => removedObject = h; }); - AddStep("add hitobject", () => editorBeatmap.Add(expectedObject = new HitCircle { StartTime = 1000 })); + AddStep("add hitobject", () => EditorBeatmap.Add(expectedObject = new HitCircle { StartTime = 1000 })); AddAssert("hitobject added", () => addedObject == expectedObject); AddAssert("unsaved changes", () => Editor.HasUnsavedChanges); @@ -95,11 +81,11 @@ namespace osu.Game.Tests.Visual.Editing AddStep("bind removal", () => { - editorBeatmap.HitObjectAdded += h => addedObject = h; - editorBeatmap.HitObjectRemoved += h => removedObject = h; + EditorBeatmap.HitObjectAdded += h => addedObject = h; + EditorBeatmap.HitObjectRemoved += h => removedObject = h; }); - AddStep("add hitobject", () => editorBeatmap.Add(expectedObject = new HitCircle { StartTime = 1000 })); + AddStep("add hitobject", () => EditorBeatmap.Add(expectedObject = new HitCircle { StartTime = 1000 })); addUndoSteps(); AddStep("reset variables", () => @@ -117,7 +103,7 @@ namespace osu.Game.Tests.Visual.Editing [Test] public void TestAddObjectThenSaveHasNoUnsavedChanges() { - AddStep("add hitobject", () => editorBeatmap.Add(new HitCircle { StartTime = 1000 })); + AddStep("add hitobject", () => EditorBeatmap.Add(new HitCircle { StartTime = 1000 })); AddAssert("unsaved changes", () => Editor.HasUnsavedChanges); AddStep("save changes", () => Editor.Save()); @@ -133,12 +119,12 @@ namespace osu.Game.Tests.Visual.Editing AddStep("bind removal", () => { - editorBeatmap.HitObjectAdded += h => addedObject = h; - editorBeatmap.HitObjectRemoved += h => removedObject = h; + EditorBeatmap.HitObjectAdded += h => addedObject = h; + EditorBeatmap.HitObjectRemoved += h => removedObject = h; }); - AddStep("add hitobject", () => editorBeatmap.Add(expectedObject = new HitCircle { StartTime = 1000 })); - AddStep("remove object", () => editorBeatmap.Remove(expectedObject)); + AddStep("add hitobject", () => EditorBeatmap.Add(expectedObject = new HitCircle { StartTime = 1000 })); + AddStep("remove object", () => EditorBeatmap.Remove(expectedObject)); AddStep("reset variables", () => { addedObject = null; @@ -160,12 +146,12 @@ namespace osu.Game.Tests.Visual.Editing AddStep("bind removal", () => { - editorBeatmap.HitObjectAdded += h => addedObject = h; - editorBeatmap.HitObjectRemoved += h => removedObject = h; + EditorBeatmap.HitObjectAdded += h => addedObject = h; + EditorBeatmap.HitObjectRemoved += h => removedObject = h; }); - AddStep("add hitobject", () => editorBeatmap.Add(expectedObject = new HitCircle { StartTime = 1000 })); - AddStep("remove object", () => editorBeatmap.Remove(expectedObject)); + AddStep("add hitobject", () => EditorBeatmap.Add(expectedObject = new HitCircle { StartTime = 1000 })); + AddStep("remove object", () => EditorBeatmap.Remove(expectedObject)); addUndoSteps(); AddStep("reset variables", () => @@ -183,18 +169,5 @@ namespace osu.Game.Tests.Visual.Editing private void addUndoSteps() => AddStep("undo", () => Editor.Undo()); private void addRedoSteps() => AddStep("redo", () => Editor.Redo()); - - protected override Editor CreateEditor() => new TestEditor(); - - protected class TestEditor : Editor - { - public new void Undo() => base.Undo(); - - public new void Redo() => base.Redo(); - - public new void Save() => base.Save(); - - public new bool HasUnsavedChanges => base.HasUnsavedChanges; - } } } diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorClipboard.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorClipboard.cs new file mode 100644 index 0000000000..29046c82a6 --- /dev/null +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorClipboard.cs @@ -0,0 +1,154 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Tests.Beatmaps; +using osuTK; + +namespace osu.Game.Tests.Visual.Editing +{ + public class TestSceneEditorClipboard : EditorTestScene + { + protected override Ruleset CreateEditorRuleset() => new OsuRuleset(); + + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false); + + [Test] + public void TestCutRemovesObjects() + { + var addedObject = new HitCircle { StartTime = 1000 }; + + AddStep("add hitobject", () => EditorBeatmap.Add(addedObject)); + + AddStep("select added object", () => EditorBeatmap.SelectedHitObjects.Add(addedObject)); + + AddStep("cut hitobject", () => Editor.Cut()); + + AddAssert("no hitobjects in beatmap", () => EditorBeatmap.HitObjects.Count == 0); + } + + [TestCase(1000)] + [TestCase(2000)] + public void TestCutPaste(double newTime) + { + var addedObject = new HitCircle { StartTime = 1000 }; + + AddStep("add hitobject", () => EditorBeatmap.Add(addedObject)); + + AddStep("select added object", () => EditorBeatmap.SelectedHitObjects.Add(addedObject)); + + AddStep("cut hitobject", () => Editor.Cut()); + + AddStep("move forward in time", () => EditorClock.Seek(newTime)); + + AddStep("paste hitobject", () => Editor.Paste()); + + AddAssert("is one object", () => EditorBeatmap.HitObjects.Count == 1); + + AddAssert("new object selected", () => EditorBeatmap.SelectedHitObjects.Single().StartTime == newTime); + } + + [Test] + public void TestCutPasteSlider() + { + var addedObject = new Slider + { + StartTime = 1000, + Path = new SliderPath + { + ControlPoints = + { + new PathControlPoint(), + new PathControlPoint(new Vector2(100, 0), PathType.Bezier) + } + } + }; + + AddStep("add hitobject", () => EditorBeatmap.Add(addedObject)); + + AddStep("select added object", () => EditorBeatmap.SelectedHitObjects.Add(addedObject)); + + AddStep("cut hitobject", () => Editor.Cut()); + + AddStep("paste hitobject", () => Editor.Paste()); + + AddAssert("is one object", () => EditorBeatmap.HitObjects.Count == 1); + + AddAssert("path matches", () => + { + var path = ((Slider)EditorBeatmap.HitObjects.Single()).Path; + return path.ControlPoints.Count == 2 && path.ControlPoints.SequenceEqual(addedObject.Path.ControlPoints); + }); + } + + [Test] + public void TestCutPasteSpinner() + { + var addedObject = new Spinner + { + StartTime = 1000, + Duration = 5000 + }; + + AddStep("add hitobject", () => EditorBeatmap.Add(addedObject)); + + AddStep("select added object", () => EditorBeatmap.SelectedHitObjects.Add(addedObject)); + + AddStep("cut hitobject", () => Editor.Cut()); + + AddStep("paste hitobject", () => Editor.Paste()); + + AddAssert("is one object", () => EditorBeatmap.HitObjects.Count == 1); + + AddAssert("duration matches", () => ((Spinner)EditorBeatmap.HitObjects.Single()).Duration == 5000); + } + + [Test] + public void TestCopyPaste() + { + var addedObject = new HitCircle { StartTime = 1000 }; + + AddStep("add hitobject", () => EditorBeatmap.Add(addedObject)); + + AddStep("select added object", () => EditorBeatmap.SelectedHitObjects.Add(addedObject)); + + AddStep("copy hitobject", () => Editor.Copy()); + + AddStep("move forward in time", () => EditorClock.Seek(2000)); + + AddStep("paste hitobject", () => Editor.Paste()); + + AddAssert("are two objects", () => EditorBeatmap.HitObjects.Count == 2); + + AddAssert("new object selected", () => EditorBeatmap.SelectedHitObjects.Single().StartTime == 2000); + } + + [Test] + public void TestCutNothing() + { + AddStep("cut hitobject", () => Editor.Cut()); + AddAssert("are no objects", () => EditorBeatmap.HitObjects.Count == 0); + } + + [Test] + public void TestCopyNothing() + { + AddStep("copy hitobject", () => Editor.Copy()); + AddAssert("are no objects", () => EditorBeatmap.HitObjects.Count == 0); + } + + [Test] + public void TestPasteNothing() + { + AddStep("paste hitobject", () => Editor.Paste()); + AddAssert("are no objects", () => EditorBeatmap.HitObjects.Count == 0); + } + } +} diff --git a/osu.Game/Screens/Edit/ClipboardContent.cs b/osu.Game/Screens/Edit/ClipboardContent.cs new file mode 100644 index 0000000000..b2edbedccc --- /dev/null +++ b/osu.Game/Screens/Edit/ClipboardContent.cs @@ -0,0 +1,27 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; +using osu.Game.IO.Serialization; +using osu.Game.IO.Serialization.Converters; +using osu.Game.Rulesets.Objects; + +namespace osu.Game.Screens.Edit +{ + public class ClipboardContent : IJsonSerializable + { + [JsonConverter(typeof(TypedListConverter))] + public IList HitObjects; + + public ClipboardContent() + { + } + + public ClipboardContent(EditorBeatmap editorBeatmap) + { + HitObjects = editorBeatmap.SelectedHitObjects.ToList(); + } + } +} diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index b7b222d87b..bf1e18771f 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -271,6 +271,9 @@ namespace osu.Game.Screens.Edit.Compose.Components blueprint.Selected += onBlueprintSelected; blueprint.Deselected += onBlueprintDeselected; + if (beatmap.SelectedHitObjects.Contains(hitObject)) + blueprint.Select(); + SelectionBlueprints.Add(blueprint); } diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index ce34c1dac0..71340041f0 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -3,6 +3,8 @@ using System; using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -22,6 +24,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; +using osu.Game.IO.Serialization; using osu.Game.Online.API; using osu.Game.Overlays; using osu.Game.Rulesets.Edit; @@ -131,9 +134,14 @@ namespace osu.Game.Screens.Edit updateLastSavedHash(); EditorMenuBar menuBar; + OsuMenuItem undoMenuItem; OsuMenuItem redoMenuItem; + EditorMenuItem cutMenuItem; + EditorMenuItem copyMenuItem; + EditorMenuItem pasteMenuItem; + var fileMenuItems = new List { new EditorMenuItem("Save", MenuItemType.Standard, Save) @@ -183,7 +191,11 @@ namespace osu.Game.Screens.Edit Items = new[] { undoMenuItem = new EditorMenuItem("Undo", MenuItemType.Standard, Undo), - redoMenuItem = new EditorMenuItem("Redo", MenuItemType.Standard, Redo) + redoMenuItem = new EditorMenuItem("Redo", MenuItemType.Standard, Redo), + new EditorMenuItemSpacer(), + cutMenuItem = new EditorMenuItem("Cut", MenuItemType.Standard, Cut), + copyMenuItem = new EditorMenuItem("Copy", MenuItemType.Standard, Copy), + pasteMenuItem = new EditorMenuItem("Paste", MenuItemType.Standard, Paste), } } } @@ -244,6 +256,16 @@ namespace osu.Game.Screens.Edit changeHandler.CanUndo.BindValueChanged(v => undoMenuItem.Action.Disabled = !v.NewValue, true); changeHandler.CanRedo.BindValueChanged(v => redoMenuItem.Action.Disabled = !v.NewValue, true); + editorBeatmap.SelectedHitObjects.BindCollectionChanged((_, __) => + { + var hasObjects = editorBeatmap.SelectedHitObjects.Count > 0; + + cutMenuItem.Action.Disabled = !hasObjects; + copyMenuItem.Action.Disabled = !hasObjects; + }, true); + + clipboard.BindValueChanged(content => pasteMenuItem.Action.Disabled = string.IsNullOrEmpty(content.NewValue)); + menuBar.Mode.ValueChanged += onModeChanged; bottomBackground.Colour = colours.Gray2; @@ -270,6 +292,18 @@ namespace osu.Game.Screens.Edit { switch (action.ActionType) { + case PlatformActionType.Cut: + Cut(); + return true; + + case PlatformActionType.Copy: + Copy(); + return true; + + case PlatformActionType.Paste: + Paste(); + return true; + case PlatformActionType.Undo: Undo(); return true; @@ -394,6 +428,47 @@ namespace osu.Game.Screens.Edit this.Exit(); } + private readonly Bindable clipboard = new Bindable(); + + protected void Cut() + { + Copy(); + foreach (var h in editorBeatmap.SelectedHitObjects.ToArray()) + editorBeatmap.Remove(h); + } + + protected void Copy() + { + if (editorBeatmap.SelectedHitObjects.Count == 0) + return; + + clipboard.Value = new ClipboardContent(editorBeatmap).Serialize(); + } + + protected void Paste() + { + if (string.IsNullOrEmpty(clipboard.Value)) + return; + + var objects = clipboard.Value.Deserialize().HitObjects; + + Debug.Assert(objects.Any()); + + double timeOffset = clock.CurrentTime - objects.Min(o => o.StartTime); + + foreach (var h in objects) + h.StartTime += timeOffset; + + changeHandler.BeginChange(); + + editorBeatmap.SelectedHitObjects.Clear(); + + editorBeatmap.AddRange(objects); + editorBeatmap.SelectedHitObjects.AddRange(objects); + + changeHandler.EndChange(); + } + protected void Undo() => changeHandler.RestoreState(-1); protected void Redo() => changeHandler.RestoreState(1); diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index 061009e519..3a9bd85b0f 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -9,7 +9,6 @@ using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Timing; @@ -68,41 +67,6 @@ namespace osu.Game.Screens.Edit trackStartTime(obj); } - private readonly HashSet pendingUpdates = new HashSet(); - private ScheduledDelegate scheduledUpdate; - - /// - /// Updates a , invoking and re-processing the beatmap. - /// - /// The to update. - public void UpdateHitObject([NotNull] HitObject hitObject) => updateHitObject(hitObject, false); - - private void updateHitObject([CanBeNull] HitObject hitObject, bool silent) - { - scheduledUpdate?.Cancel(); - - if (hitObject != null) - pendingUpdates.Add(hitObject); - - scheduledUpdate = Schedule(() => - { - beatmapProcessor?.PreProcess(); - - foreach (var obj in pendingUpdates) - obj.ApplyDefaults(ControlPointInfo, BeatmapInfo.BaseDifficulty); - - beatmapProcessor?.PostProcess(); - - if (!silent) - { - foreach (var obj in pendingUpdates) - HitObjectUpdated?.Invoke(obj); - } - - pendingUpdates.Clear(); - }); - } - public BeatmapInfo BeatmapInfo { get => PlayableBeatmap.BeatmapInfo; @@ -125,6 +89,8 @@ namespace osu.Game.Screens.Edit private IList mutableHitObjects => (IList)PlayableBeatmap.HitObjects; + private readonly HashSet pendingUpdates = new HashSet(); + /// /// Adds a collection of s to this . /// @@ -160,14 +126,27 @@ namespace osu.Game.Screens.Edit mutableHitObjects.Insert(index, hitObject); + // must be run after any change to hitobject ordering + beatmapProcessor?.PreProcess(); + processHitObject(hitObject); + beatmapProcessor?.PostProcess(); + HitObjectAdded?.Invoke(hitObject); - updateHitObject(hitObject, true); + } + + /// + /// Updates a , invoking and re-processing the beatmap. + /// + /// The to update. + public void UpdateHitObject([NotNull] HitObject hitObject) + { + pendingUpdates.Add(hitObject); } /// /// Removes a from this . /// - /// The to add. + /// The to remove. /// True if the has been removed, false otherwise. public bool Remove(HitObject hitObject) { @@ -199,11 +178,14 @@ namespace osu.Game.Screens.Edit var bindable = startTimeBindables[hitObject]; bindable.UnbindAll(); - startTimeBindables.Remove(hitObject); - HitObjectRemoved?.Invoke(hitObject); - updateHitObject(null, true); + // must be run after any change to hitobject ordering + beatmapProcessor?.PreProcess(); + processHitObject(hitObject); + beatmapProcessor?.PostProcess(); + + HitObjectRemoved?.Invoke(hitObject); } /// @@ -211,20 +193,33 @@ namespace osu.Game.Screens.Edit /// public void Clear() { - var removed = HitObjects.ToList(); - - mutableHitObjects.Clear(); - - foreach (var b in startTimeBindables) - b.Value.UnbindAll(); - startTimeBindables.Clear(); - - foreach (var h in removed) - HitObjectRemoved?.Invoke(h); - - updateHitObject(null, true); + foreach (var h in HitObjects.ToArray()) + Remove(h); } + protected override void Update() + { + base.Update(); + + // debounce updates as they are common and may come from input events, which can run needlessly many times per update frame. + if (pendingUpdates.Count > 0) + { + beatmapProcessor?.PreProcess(); + + foreach (var hitObject in pendingUpdates) + { + processHitObject(hitObject); + HitObjectUpdated?.Invoke(hitObject); + } + + pendingUpdates.Clear(); + + beatmapProcessor?.PostProcess(); + } + } + + private void processHitObject(HitObject hitObject) => hitObject.ApplyDefaults(ControlPointInfo, BeatmapInfo.BaseDifficulty); + private void trackStartTime(HitObject hitObject) { startTimeBindables[hitObject] = hitObject.StartTimeBindable.GetBoundCopy(); diff --git a/osu.Game/Tests/Beatmaps/TestBeatmap.cs b/osu.Game/Tests/Beatmaps/TestBeatmap.cs index a375a17bcf..87b77f4616 100644 --- a/osu.Game/Tests/Beatmaps/TestBeatmap.cs +++ b/osu.Game/Tests/Beatmaps/TestBeatmap.cs @@ -15,14 +15,16 @@ namespace osu.Game.Tests.Beatmaps { public class TestBeatmap : Beatmap { - public TestBeatmap(RulesetInfo ruleset) + public TestBeatmap(RulesetInfo ruleset, bool withHitObjects = true) { var baseBeatmap = CreateBeatmap(); BeatmapInfo = baseBeatmap.BeatmapInfo; ControlPointInfo = baseBeatmap.ControlPointInfo; Breaks = baseBeatmap.Breaks; - HitObjects = baseBeatmap.HitObjects; + + if (withHitObjects) + HitObjects = baseBeatmap.HitObjects; BeatmapInfo.Ruleset = ruleset; BeatmapInfo.RulesetID = ruleset.ID ?? 0; diff --git a/osu.Game/Tests/Visual/EditorTestScene.cs b/osu.Game/Tests/Visual/EditorTestScene.cs index cd08f4712a..8f76f247cf 100644 --- a/osu.Game/Tests/Visual/EditorTestScene.cs +++ b/osu.Game/Tests/Visual/EditorTestScene.cs @@ -14,7 +14,11 @@ namespace osu.Game.Tests.Visual { public abstract class EditorTestScene : ScreenTestScene { - protected Editor Editor { get; private set; } + protected EditorBeatmap EditorBeatmap; + + protected TestEditor Editor { get; private set; } + + protected EditorClock EditorClock { get; private set; } [BackgroundDependencyLoader] private void load() @@ -29,6 +33,8 @@ namespace osu.Game.Tests.Visual AddStep("load editor", () => LoadScreen(Editor = CreateEditor())); AddUntilStep("wait for editor to load", () => Editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true && Editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); + AddStep("get beatmap", () => EditorBeatmap = Editor.ChildrenOfType().Single()); + AddStep("get clock", () => EditorClock = Editor.ChildrenOfType().Single()); } /// @@ -39,6 +45,23 @@ namespace osu.Game.Tests.Visual protected sealed override Ruleset CreateRuleset() => CreateEditorRuleset(); - protected virtual Editor CreateEditor() => new Editor(); + protected virtual TestEditor CreateEditor() => new TestEditor(); + + protected class TestEditor : Editor + { + public new void Undo() => base.Undo(); + + public new void Redo() => base.Redo(); + + public new void Save() => base.Save(); + + public new void Cut() => base.Cut(); + + public new void Copy() => base.Copy(); + + public new void Paste() => base.Paste(); + + public new bool HasUnsavedChanges => base.HasUnsavedChanges; + } } } diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 48582ae29d..8d23a32c3c 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -24,7 +24,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 0eed2fa911..d00b174195 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -80,7 +80,7 @@ - +