Skip to content

Commit 181c7d1

Browse files
authored
Merge pull request #20901 from peppy/add-editor-object-clone
Add ability to clone objects in the editor
2 parents 2ddb94d + f5ca447 commit 181c7d1

File tree

5 files changed

+75
-5
lines changed

5 files changed

+75
-5
lines changed

osu.Game.Tests/Visual/Editing/TestSceneEditorClipboard.cs

+31
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,20 @@ public void TestCopyPaste(bool deselectAfterCopy)
155155
AddUntilStep("composer selection box is visible", () => Editor.ChildrenOfType<HitObjectComposer>().First().ChildrenOfType<EditorSelectionHandler>().First().Alpha > 0);
156156
}
157157

158+
[Test]
159+
public void TestClone()
160+
{
161+
var addedObject = new HitCircle { StartTime = 1000 };
162+
AddStep("add hitobject", () => EditorBeatmap.Add(addedObject));
163+
AddStep("select added object", () => EditorBeatmap.SelectedHitObjects.Add(addedObject));
164+
165+
AddAssert("is one object", () => EditorBeatmap.HitObjects.Count == 1);
166+
AddStep("clone", () => Editor.Clone());
167+
AddAssert("is two objects", () => EditorBeatmap.HitObjects.Count == 2);
168+
AddStep("clone", () => Editor.Clone());
169+
AddAssert("is three objects", () => EditorBeatmap.HitObjects.Count == 3);
170+
}
171+
158172
[Test]
159173
public void TestCutNothing()
160174
{
@@ -175,5 +189,22 @@ public void TestPasteNothing()
175189
AddStep("paste hitobject", () => Editor.Paste());
176190
AddAssert("are no objects", () => EditorBeatmap.HitObjects.Count == 0);
177191
}
192+
193+
[Test]
194+
public void TestCloneNothing()
195+
{
196+
// Add arbitrary object and copy to clipboard.
197+
// This is tested to ensure that clone doesn't incorrectly read from the clipboard when no selection is made.
198+
var addedObject = new HitCircle { StartTime = 1000 };
199+
AddStep("add hitobject", () => EditorBeatmap.Add(addedObject));
200+
AddStep("select added object", () => EditorBeatmap.SelectedHitObjects.Add(addedObject));
201+
AddStep("copy hitobject", () => Editor.Copy());
202+
203+
AddStep("deselect all objects", () => EditorBeatmap.SelectedHitObjects.Clear());
204+
205+
AddAssert("is one object", () => EditorBeatmap.HitObjects.Count == 1);
206+
AddStep("clone", () => Editor.Clone());
207+
AddAssert("still one object", () => EditorBeatmap.HitObjects.Count == 1);
208+
}
178209
}
179210
}

osu.Game/Input/Bindings/GlobalActionContainer.cs

+11-4
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,17 @@ protected override void LoadComplete()
3131
parentInputManager = GetContainingInputManager();
3232
}
3333

34-
// IMPORTANT: Do not change the order of key bindings in this list.
35-
// It is used to decide the order of precedence (see note in DatabasedKeyBindingContainer).
34+
// IMPORTANT: Take care when changing order of the items in the enumerable.
35+
// It is used to decide the order of precedence, with the earlier items having higher precedence.
3636
public override IEnumerable<IKeyBinding> DefaultKeyBindings => GlobalKeyBindings
37-
.Concat(OverlayKeyBindings)
3837
.Concat(EditorKeyBindings)
3938
.Concat(InGameKeyBindings)
4039
.Concat(SongSelectKeyBindings)
41-
.Concat(AudioControlKeyBindings);
40+
.Concat(AudioControlKeyBindings)
41+
// Overlay bindings may conflict with more local cases like the editor so they are checked last.
42+
// It has generally been agreed on that local screens like the editor should have priority,
43+
// based on such usages potentially requiring a lot more key bindings that may be "shared" with global ones.
44+
.Concat(OverlayKeyBindings);
4245

4346
public IEnumerable<KeyBinding> GlobalKeyBindings => new[]
4447
{
@@ -87,6 +90,7 @@ protected override void LoadComplete()
8790
new KeyBinding(new[] { InputKey.F3 }, GlobalAction.EditorTimingMode),
8891
new KeyBinding(new[] { InputKey.F4 }, GlobalAction.EditorSetupMode),
8992
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.A }, GlobalAction.EditorVerifyMode),
93+
new KeyBinding(new[] { InputKey.Control, InputKey.D }, GlobalAction.EditorCloneSelection),
9094
new KeyBinding(new[] { InputKey.J }, GlobalAction.EditorNudgeLeft),
9195
new KeyBinding(new[] { InputKey.K }, GlobalAction.EditorNudgeRight),
9296
new KeyBinding(new[] { InputKey.G }, GlobalAction.EditorCycleGridDisplayMode),
@@ -343,5 +347,8 @@ public enum GlobalAction
343347

344348
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleProfile))]
345349
ToggleProfile,
350+
351+
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorCloneSelection))]
352+
EditorCloneSelection
346353
}
347354
}

osu.Game/Localisation/GlobalActionKeyBindingStrings.cs

+5
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,11 @@ public static class GlobalActionKeyBindingStrings
184184
/// </summary>
185185
public static LocalisableString EditorTapForBPM => new TranslatableString(getKey(@"editor_tap_for_bpm"), @"Tap for BPM");
186186

187+
/// <summary>
188+
/// "Clone selection"
189+
/// </summary>
190+
public static LocalisableString EditorCloneSelection => new TranslatableString(getKey(@"editor_clone_selection"), @"Clone selection");
191+
187192
/// <summary>
188193
/// "Cycle grid display mode"
189194
/// </summary>

osu.Game/Screens/Edit/Editor.cs

+26-1
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,7 @@ private void load(OsuConfigManager config)
304304
cutMenuItem = new EditorMenuItem("Cut", MenuItemType.Standard, Cut),
305305
copyMenuItem = new EditorMenuItem("Copy", MenuItemType.Standard, Copy),
306306
pasteMenuItem = new EditorMenuItem("Paste", MenuItemType.Standard, Paste),
307+
cloneMenuItem = new EditorMenuItem("Clone", MenuItemType.Standard, Clone),
307308
}
308309
},
309310
new MenuItem("View")
@@ -575,6 +576,10 @@ public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
575576
this.Exit();
576577
return true;
577578

579+
case GlobalAction.EditorCloneSelection:
580+
Clone();
581+
return true;
582+
578583
case GlobalAction.EditorComposeMode:
579584
Mode.Value = EditorScreenMode.Compose;
580585
return true;
@@ -741,6 +746,7 @@ private void updateInProgress(ValueChangedEvent<bool> obj)
741746

742747
private EditorMenuItem cutMenuItem;
743748
private EditorMenuItem copyMenuItem;
749+
private EditorMenuItem cloneMenuItem;
744750
private EditorMenuItem pasteMenuItem;
745751

746752
private readonly BindableWithCurrent<bool> canCut = new BindableWithCurrent<bool>();
@@ -750,7 +756,11 @@ private void updateInProgress(ValueChangedEvent<bool> obj)
750756
private void setUpClipboardActionAvailability()
751757
{
752758
canCut.Current.BindValueChanged(cut => cutMenuItem.Action.Disabled = !cut.NewValue, true);
753-
canCopy.Current.BindValueChanged(copy => copyMenuItem.Action.Disabled = !copy.NewValue, true);
759+
canCopy.Current.BindValueChanged(copy =>
760+
{
761+
copyMenuItem.Action.Disabled = !copy.NewValue;
762+
cloneMenuItem.Action.Disabled = !copy.NewValue;
763+
}, true);
754764
canPaste.Current.BindValueChanged(paste => pasteMenuItem.Action.Disabled = !paste.NewValue, true);
755765
}
756766

@@ -765,6 +775,21 @@ private void rebindClipboardBindables()
765775

766776
protected void Copy() => currentScreen?.Copy();
767777

778+
protected void Clone()
779+
{
780+
// Avoid attempting to clone if copying is not available (as it may result in pasting something unexpected).
781+
if (!canCopy.Value)
782+
return;
783+
784+
// This is an initial implementation just to get an idea of how people used this function.
785+
// There are a couple of differences from osu!stable's implementation which will require more work to match:
786+
// - The "clipboard" is not populated during the duplication process.
787+
// - The duplicated hitobjects are inserted after the original pattern (add one beat_length and then quantize using beat snap).
788+
// - The duplicated hitobjects are selected (but this is also applied for all paste operations so should be changed there).
789+
Copy();
790+
Paste();
791+
}
792+
768793
protected void Paste() => currentScreen?.Paste();
769794

770795
#endregion

osu.Game/Tests/Visual/EditorTestScene.cs

+2
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ protected class TestEditor : Editor
110110

111111
public new void Paste() => base.Paste();
112112

113+
public new void Clone() => base.Clone();
114+
113115
public new void SwitchToDifficulty(BeatmapInfo beatmapInfo) => base.SwitchToDifficulty(beatmapInfo);
114116

115117
public new void CreateNewDifficulty(RulesetInfo rulesetInfo) => base.CreateNewDifficulty(rulesetInfo);

0 commit comments

Comments
 (0)