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