1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-14 12:33:01 +08:00

Merge branch 'master' into grid-momentary-shortcuts

This commit is contained in:
Bartłomiej Dach 2022-10-25 22:17:08 +02:00 committed by GitHub
commit 48057412f5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 331 additions and 29 deletions

View File

@ -0,0 +1,22 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Game.Rulesets.Osu.Mods;
namespace osu.Game.Rulesets.Osu.Tests.Mods
{
public class TestSceneOsuModFreezeFrame : OsuModTestScene
{
[Test]
public void TestFreezeFrame()
{
CreateModTest(new ModTestData
{
Mod = new OsuModFreezeFrame(),
PassCondition = () => true,
Autoplay = false,
});
}
}
}

View File

@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
public override IconUsage? Icon { get; } = FontAwesome.Regular.Circle; public override IconUsage? Icon { get; } = FontAwesome.Regular.Circle;
public override Type[] IncompatibleMods => new[] { typeof(IHidesApproachCircles) }; public override Type[] IncompatibleMods => new[] { typeof(IHidesApproachCircles), typeof(OsuModFreezeFrame) };
[SettingSource("Initial size", "Change the initial size of the approach circle, relative to hit circles.", 0)] [SettingSource("Initial size", "Change the initial size of the approach circle, relative to hit circles.", 0)]
public BindableFloat Scale { get; } = new BindableFloat(4) public BindableFloat Scale { get; } = new BindableFloat(4)

View File

@ -0,0 +1,89 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModFreezeFrame : Mod, IApplicableToDrawableHitObject, IApplicableToBeatmap
{
public override string Name => "Freeze Frame";
public override string Acronym => "FR";
public override double ScoreMultiplier => 1;
public override LocalisableString Description => "Burn the notes into your memory.";
//Alters the transforms of the approach circles, breaking the effects of these mods.
public override Type[] IncompatibleMods => new[] { typeof(OsuModApproachDifferent) };
public override ModType Type => ModType.Fun;
//mod breaks normal approach circle preempt
private double originalPreempt;
public void ApplyToBeatmap(IBeatmap beatmap)
{
var firstHitObject = beatmap.HitObjects.OfType<OsuHitObject>().FirstOrDefault();
if (firstHitObject == null)
return;
double lastNewComboTime = 0;
originalPreempt = firstHitObject.TimePreempt;
foreach (var obj in beatmap.HitObjects.OfType<OsuHitObject>())
{
if (obj.NewCombo) { lastNewComboTime = obj.StartTime; }
applyFadeInAdjustment(obj);
}
void applyFadeInAdjustment(OsuHitObject osuObject)
{
osuObject.TimePreempt += osuObject.StartTime - lastNewComboTime;
foreach (var nested in osuObject.NestedHitObjects.OfType<OsuHitObject>())
{
switch (nested)
{
//SliderRepeat wont layer correctly if preempt is changed.
case SliderRepeat:
break;
default:
applyFadeInAdjustment(nested);
break;
}
}
}
}
public void ApplyToDrawableHitObject(DrawableHitObject drawableObject)
{
drawableObject.ApplyCustomUpdateState += (drawableHitObject, _) =>
{
if (drawableHitObject is not DrawableHitCircle drawableHitCircle) return;
var hitCircle = drawableHitCircle.HitObject;
var approachCircle = drawableHitCircle.ApproachCircle;
// Reapply scale, ensuring the AR isn't changed due to the new preempt.
approachCircle.ClearTransforms(targetMember: nameof(approachCircle.Scale));
approachCircle.ScaleTo(4 * (float)(hitCircle.TimePreempt / originalPreempt));
using (drawableHitCircle.ApproachCircle.BeginAbsoluteSequence(hitCircle.StartTime - hitCircle.TimePreempt))
approachCircle.ScaleTo(1, hitCircle.TimePreempt).Then().Expire();
};
}
}
}

View File

@ -201,7 +201,8 @@ namespace osu.Game.Rulesets.Osu
new OsuModMuted(), new OsuModMuted(),
new OsuModNoScope(), new OsuModNoScope(),
new MultiMod(new OsuModMagnetised(), new OsuModRepel()), new MultiMod(new OsuModMagnetised(), new OsuModRepel()),
new ModAdaptiveSpeed() new ModAdaptiveSpeed(),
new OsuModFreezeFrame()
}; };
case ModType.System: case ModType.System:

View File

@ -106,6 +106,49 @@ namespace osu.Game.Tests.Visual.Editing
assertBeatSnap(16); assertBeatSnap(16);
} }
[Test]
public void TestKeyboardNavigation()
{
pressKey(1);
assertBeatSnap(1);
assertPreset(BeatDivisorType.Common);
pressKey(2);
assertBeatSnap(2);
assertPreset(BeatDivisorType.Common);
pressKey(3);
assertBeatSnap(3);
assertPreset(BeatDivisorType.Triplets);
pressKey(4);
assertBeatSnap(4);
assertPreset(BeatDivisorType.Common);
pressKey(5);
assertBeatSnap(5);
assertPreset(BeatDivisorType.Custom, 5);
pressKey(6);
assertBeatSnap(6);
assertPreset(BeatDivisorType.Triplets);
pressKey(7);
assertBeatSnap(7);
assertPreset(BeatDivisorType.Custom, 7);
pressKey(8);
assertBeatSnap(8);
assertPreset(BeatDivisorType.Common);
void pressKey(int key) => AddStep($"press shift+{key}", () =>
{
InputManager.PressKey(Key.ShiftLeft);
InputManager.Key(Key.Number0 + key);
InputManager.ReleaseKey(Key.ShiftLeft);
});
}
[Test] [Test]
public void TestBeatPresetNavigation() public void TestBeatPresetNavigation()
{ {

View File

@ -0,0 +1,32 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Editing
{
/// <summary>
/// Test editor hotkeys at a high level to ensure they all work well together.
/// </summary>
public class TestSceneEditorBindings : EditorTestScene
{
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
[Test]
public void TestBeatDivisorChangeHotkeys()
{
AddStep("hold shift", () => InputManager.PressKey(Key.LShift));
AddStep("press 4", () => InputManager.Key(Key.Number4));
AddAssert("snap updated to 4", () => EditorBeatmap.BeatmapInfo.BeatDivisor, () => Is.EqualTo(4));
AddStep("press 6", () => InputManager.Key(Key.Number6));
AddAssert("snap updated to 6", () => EditorBeatmap.BeatmapInfo.BeatDivisor, () => Is.EqualTo(6));
AddStep("release shift", () => InputManager.ReleaseKey(Key.LShift));
}
}
}

View File

@ -155,6 +155,20 @@ namespace osu.Game.Tests.Visual.Editing
AddUntilStep("composer selection box is visible", () => Editor.ChildrenOfType<HitObjectComposer>().First().ChildrenOfType<EditorSelectionHandler>().First().Alpha > 0); AddUntilStep("composer selection box is visible", () => Editor.ChildrenOfType<HitObjectComposer>().First().ChildrenOfType<EditorSelectionHandler>().First().Alpha > 0);
} }
[Test]
public void TestClone()
{
var addedObject = new HitCircle { StartTime = 1000 };
AddStep("add hitobject", () => EditorBeatmap.Add(addedObject));
AddStep("select added object", () => EditorBeatmap.SelectedHitObjects.Add(addedObject));
AddAssert("is one object", () => EditorBeatmap.HitObjects.Count == 1);
AddStep("clone", () => Editor.Clone());
AddAssert("is two objects", () => EditorBeatmap.HitObjects.Count == 2);
AddStep("clone", () => Editor.Clone());
AddAssert("is three objects", () => EditorBeatmap.HitObjects.Count == 3);
}
[Test] [Test]
public void TestCutNothing() public void TestCutNothing()
{ {
@ -175,5 +189,22 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("paste hitobject", () => Editor.Paste()); AddStep("paste hitobject", () => Editor.Paste());
AddAssert("are no objects", () => EditorBeatmap.HitObjects.Count == 0); AddAssert("are no objects", () => EditorBeatmap.HitObjects.Count == 0);
} }
[Test]
public void TestCloneNothing()
{
// Add arbitrary object and copy to clipboard.
// This is tested to ensure that clone doesn't incorrectly read from the clipboard when no selection is made.
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("deselect all objects", () => EditorBeatmap.SelectedHitObjects.Clear());
AddAssert("is one object", () => EditorBeatmap.HitObjects.Count == 1);
AddStep("clone", () => Editor.Clone());
AddAssert("still one object", () => EditorBeatmap.HitObjects.Count == 1);
}
} }
} }

View File

@ -31,14 +31,17 @@ namespace osu.Game.Input.Bindings
parentInputManager = GetContainingInputManager(); parentInputManager = GetContainingInputManager();
} }
// IMPORTANT: Do not change the order of key bindings in this list. // IMPORTANT: Take care when changing order of the items in the enumerable.
// It is used to decide the order of precedence (see note in DatabasedKeyBindingContainer). // It is used to decide the order of precedence, with the earlier items having higher precedence.
public override IEnumerable<IKeyBinding> DefaultKeyBindings => GlobalKeyBindings public override IEnumerable<IKeyBinding> DefaultKeyBindings => GlobalKeyBindings
.Concat(OverlayKeyBindings)
.Concat(EditorKeyBindings) .Concat(EditorKeyBindings)
.Concat(InGameKeyBindings) .Concat(InGameKeyBindings)
.Concat(SongSelectKeyBindings) .Concat(SongSelectKeyBindings)
.Concat(AudioControlKeyBindings); .Concat(AudioControlKeyBindings)
// Overlay bindings may conflict with more local cases like the editor so they are checked last.
// It has generally been agreed on that local screens like the editor should have priority,
// based on such usages potentially requiring a lot more key bindings that may be "shared" with global ones.
.Concat(OverlayKeyBindings);
public IEnumerable<KeyBinding> GlobalKeyBindings => new[] public IEnumerable<KeyBinding> GlobalKeyBindings => new[]
{ {
@ -87,6 +90,7 @@ namespace osu.Game.Input.Bindings
new KeyBinding(new[] { InputKey.F3 }, GlobalAction.EditorTimingMode), new KeyBinding(new[] { InputKey.F3 }, GlobalAction.EditorTimingMode),
new KeyBinding(new[] { InputKey.F4 }, GlobalAction.EditorSetupMode), new KeyBinding(new[] { InputKey.F4 }, GlobalAction.EditorSetupMode),
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.A }, GlobalAction.EditorVerifyMode), new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.A }, GlobalAction.EditorVerifyMode),
new KeyBinding(new[] { InputKey.Control, InputKey.D }, GlobalAction.EditorCloneSelection),
new KeyBinding(new[] { InputKey.J }, GlobalAction.EditorNudgeLeft), new KeyBinding(new[] { InputKey.J }, GlobalAction.EditorNudgeLeft),
new KeyBinding(new[] { InputKey.K }, GlobalAction.EditorNudgeRight), new KeyBinding(new[] { InputKey.K }, GlobalAction.EditorNudgeRight),
new KeyBinding(new[] { InputKey.G }, GlobalAction.EditorCycleGridDisplayMode), new KeyBinding(new[] { InputKey.G }, GlobalAction.EditorCycleGridDisplayMode),
@ -343,5 +347,8 @@ namespace osu.Game.Input.Bindings
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleProfile))] [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleProfile))]
ToggleProfile, ToggleProfile,
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorCloneSelection))]
EditorCloneSelection
} }
} }

View File

@ -184,6 +184,11 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString EditorTapForBPM => new TranslatableString(getKey(@"editor_tap_for_bpm"), @"Tap for BPM"); public static LocalisableString EditorTapForBPM => new TranslatableString(getKey(@"editor_tap_for_bpm"), @"Tap for BPM");
/// <summary>
/// "Clone selection"
/// </summary>
public static LocalisableString EditorCloneSelection => new TranslatableString(getKey(@"editor_clone_selection"), @"Clone selection");
/// <summary> /// <summary>
/// "Cycle grid display mode" /// "Cycle grid display mode"
/// </summary> /// </summary>

View File

@ -238,7 +238,7 @@ namespace osu.Game.Rulesets.Edit
protected override bool OnKeyDown(KeyDownEvent e) protected override bool OnKeyDown(KeyDownEvent e)
{ {
if (e.ControlPressed || e.AltPressed || e.SuperPressed) if (e.ControlPressed || e.AltPressed || e.SuperPressed || e.ShiftPressed)
return false; return false;
if (checkLeftToggleFromKey(e.Key, out int leftIndex)) if (checkLeftToggleFromKey(e.Key, out int leftIndex))

View File

@ -199,8 +199,8 @@ namespace osu.Game.Rulesets.Objects.Legacy
if (stringAddBank == @"none") if (stringAddBank == @"none")
stringAddBank = null; stringAddBank = null;
bankInfo.Normal = stringBank; bankInfo.BankForNormal = stringBank;
bankInfo.Add = string.IsNullOrEmpty(stringAddBank) ? stringBank : stringAddBank; bankInfo.BankForAdditions = string.IsNullOrEmpty(stringAddBank) ? stringBank : stringAddBank;
if (split.Length > 2) if (split.Length > 2)
bankInfo.CustomSampleBank = Parsing.ParseInt(split[2]); bankInfo.CustomSampleBank = Parsing.ParseInt(split[2]);
@ -447,32 +447,54 @@ namespace osu.Game.Rulesets.Objects.Legacy
var soundTypes = new List<HitSampleInfo> var soundTypes = new List<HitSampleInfo>
{ {
new LegacyHitSampleInfo(HitSampleInfo.HIT_NORMAL, bankInfo.Normal, bankInfo.Volume, bankInfo.CustomSampleBank, new LegacyHitSampleInfo(HitSampleInfo.HIT_NORMAL, bankInfo.BankForNormal, bankInfo.Volume, bankInfo.CustomSampleBank,
// if the sound type doesn't have the Normal flag set, attach it anyway as a layered sample. // if the sound type doesn't have the Normal flag set, attach it anyway as a layered sample.
// None also counts as a normal non-layered sample: https://osu.ppy.sh/help/wiki/osu!_File_Formats/Osu_(file_format)#hitsounds // None also counts as a normal non-layered sample: https://osu.ppy.sh/help/wiki/osu!_File_Formats/Osu_(file_format)#hitsounds
type != LegacyHitSoundType.None && !type.HasFlagFast(LegacyHitSoundType.Normal)) type != LegacyHitSoundType.None && !type.HasFlagFast(LegacyHitSoundType.Normal))
}; };
if (type.HasFlagFast(LegacyHitSoundType.Finish)) if (type.HasFlagFast(LegacyHitSoundType.Finish))
soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_FINISH, bankInfo.Add, bankInfo.Volume, bankInfo.CustomSampleBank)); soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_FINISH, bankInfo.BankForAdditions, bankInfo.Volume, bankInfo.CustomSampleBank));
if (type.HasFlagFast(LegacyHitSoundType.Whistle)) if (type.HasFlagFast(LegacyHitSoundType.Whistle))
soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_WHISTLE, bankInfo.Add, bankInfo.Volume, bankInfo.CustomSampleBank)); soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_WHISTLE, bankInfo.BankForAdditions, bankInfo.Volume, bankInfo.CustomSampleBank));
if (type.HasFlagFast(LegacyHitSoundType.Clap)) if (type.HasFlagFast(LegacyHitSoundType.Clap))
soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_CLAP, bankInfo.Add, bankInfo.Volume, bankInfo.CustomSampleBank)); soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_CLAP, bankInfo.BankForAdditions, bankInfo.Volume, bankInfo.CustomSampleBank));
return soundTypes; return soundTypes;
} }
private class SampleBankInfo private class SampleBankInfo
{ {
/// <summary>
/// An optional overriding filename which causes all bank/sample specifications to be ignored.
/// </summary>
public string Filename; public string Filename;
public string Normal; /// <summary>
public string Add; /// The bank identifier to use for the base ("hitnormal") sample.
/// Transferred to <see cref="HitSampleInfo.Bank"/> when appropriate.
/// </summary>
public string BankForNormal;
/// <summary>
/// The bank identifier to use for additions ("hitwhistle", "hitfinish", "hitclap").
/// Transferred to <see cref="HitSampleInfo.Bank"/> when appropriate.
/// </summary>
public string BankForAdditions;
/// <summary>
/// Hit sample volume (0-100).
/// See <see cref="HitSampleInfo.Volume"/>.
/// </summary>
public int Volume; public int Volume;
/// <summary>
/// The index of the custom sample bank. Is only used if 2 or above for "reasons".
/// This will add a suffix to lookups, allowing extended bank lookups (ie. "normal-hitnormal-2").
/// See <see cref="HitSampleInfo.Suffix"/>.
/// </summary>
public int CustomSampleBank; public int CustomSampleBank;
public SampleBankInfo Clone() => (SampleBankInfo)MemberwiseClone(); public SampleBankInfo Clone() => (SampleBankInfo)MemberwiseClone();
@ -503,7 +525,8 @@ namespace osu.Game.Rulesets.Objects.Legacy
public sealed override HitSampleInfo With(Optional<string> newName = default, Optional<string?> newBank = default, Optional<string?> newSuffix = default, Optional<int> newVolume = default) public sealed override HitSampleInfo With(Optional<string> newName = default, Optional<string?> newBank = default, Optional<string?> newSuffix = default, Optional<int> newVolume = default)
=> With(newName, newBank, newVolume); => With(newName, newBank, newVolume);
public virtual LegacyHitSampleInfo With(Optional<string> newName = default, Optional<string?> newBank = default, Optional<int> newVolume = default, Optional<int> newCustomSampleBank = default, public virtual LegacyHitSampleInfo With(Optional<string> newName = default, Optional<string?> newBank = default, Optional<int> newVolume = default,
Optional<int> newCustomSampleBank = default,
Optional<bool> newIsLayered = default) Optional<bool> newIsLayered = default)
=> new LegacyHitSampleInfo(newName.GetOr(Name), newBank.GetOr(Bank), newVolume.GetOr(Volume), newCustomSampleBank.GetOr(CustomSampleBank), newIsLayered.GetOr(IsLayered)); => new LegacyHitSampleInfo(newName.GetOr(Name), newBank.GetOr(Bank), newVolume.GetOr(Volume), newCustomSampleBank.GetOr(CustomSampleBank), newIsLayered.GetOr(IsLayered));
@ -537,7 +560,8 @@ namespace osu.Game.Rulesets.Objects.Legacy
Path.ChangeExtension(Filename, null) Path.ChangeExtension(Filename, null)
}; };
public sealed override LegacyHitSampleInfo With(Optional<string> newName = default, Optional<string?> newBank = default, Optional<int> newVolume = default, Optional<int> newCustomSampleBank = default, public sealed override LegacyHitSampleInfo With(Optional<string> newName = default, Optional<string?> newBank = default, Optional<int> newVolume = default,
Optional<int> newCustomSampleBank = default,
Optional<bool> newIsLayered = default) Optional<bool> newIsLayered = default)
=> new FileHitSampleInfo(Filename, newVolume.GetOr(Volume)); => new FileHitSampleInfo(Filename, newVolume.GetOr(Volume));

View File

@ -25,6 +25,26 @@ namespace osu.Game.Screens.Edit
BindValueChanged(_ => ensureValidDivisor()); BindValueChanged(_ => ensureValidDivisor());
} }
/// <summary>
/// Set a divisor, updating the valid divisor range appropriately.
/// </summary>
/// <param name="divisor">The intended divisor.</param>
public void SetArbitraryDivisor(int divisor)
{
// If the current valid divisor range doesn't contain the proposed value, attempt to find one which does.
if (!ValidDivisors.Value.Presets.Contains(divisor))
{
if (BeatDivisorPresetCollection.COMMON.Presets.Contains(divisor))
ValidDivisors.Value = BeatDivisorPresetCollection.COMMON;
else if (BeatDivisorPresetCollection.TRIPLETS.Presets.Contains(divisor))
ValidDivisors.Value = BeatDivisorPresetCollection.TRIPLETS;
else
ValidDivisors.Value = BeatDivisorPresetCollection.Custom(divisor);
}
Value = divisor;
}
private void updateBindableProperties() private void updateBindableProperties()
{ {
ensureValidDivisor(); ensureValidDivisor();

View File

@ -209,6 +209,17 @@ namespace osu.Game.Screens.Edit.Compose.Components
} }
} }
protected override bool OnKeyDown(KeyDownEvent e)
{
if (e.ShiftPressed && e.Key >= Key.Number1 && e.Key <= Key.Number9)
{
beatDivisor.SetArbitraryDivisor(e.Key - Key.Number0);
return true;
}
return base.OnKeyDown(e);
}
internal class DivisorDisplay : OsuAnimatedButton, IHasPopover internal class DivisorDisplay : OsuAnimatedButton, IHasPopover
{ {
public BindableBeatDivisor BeatDivisor { get; } = new BindableBeatDivisor(); public BindableBeatDivisor BeatDivisor { get; } = new BindableBeatDivisor();
@ -306,17 +317,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
return; return;
} }
if (!BeatDivisor.ValidDivisors.Value.Presets.Contains(divisor)) BeatDivisor.SetArbitraryDivisor(divisor);
{
if (BeatDivisorPresetCollection.COMMON.Presets.Contains(divisor))
BeatDivisor.ValidDivisors.Value = BeatDivisorPresetCollection.COMMON;
else if (BeatDivisorPresetCollection.TRIPLETS.Presets.Contains(divisor))
BeatDivisor.ValidDivisors.Value = BeatDivisorPresetCollection.TRIPLETS;
else
BeatDivisor.ValidDivisors.Value = BeatDivisorPresetCollection.Custom(divisor);
}
BeatDivisor.Value = divisor;
this.HidePopover(); this.HidePopover();
} }

View File

@ -304,6 +304,7 @@ namespace osu.Game.Screens.Edit
cutMenuItem = new EditorMenuItem("Cut", MenuItemType.Standard, Cut), cutMenuItem = new EditorMenuItem("Cut", MenuItemType.Standard, Cut),
copyMenuItem = new EditorMenuItem("Copy", MenuItemType.Standard, Copy), copyMenuItem = new EditorMenuItem("Copy", MenuItemType.Standard, Copy),
pasteMenuItem = new EditorMenuItem("Paste", MenuItemType.Standard, Paste), pasteMenuItem = new EditorMenuItem("Paste", MenuItemType.Standard, Paste),
cloneMenuItem = new EditorMenuItem("Clone", MenuItemType.Standard, Clone),
} }
}, },
new MenuItem("View") new MenuItem("View")
@ -575,6 +576,10 @@ namespace osu.Game.Screens.Edit
this.Exit(); this.Exit();
return true; return true;
case GlobalAction.EditorCloneSelection:
Clone();
return true;
case GlobalAction.EditorComposeMode: case GlobalAction.EditorComposeMode:
Mode.Value = EditorScreenMode.Compose; Mode.Value = EditorScreenMode.Compose;
return true; return true;
@ -741,6 +746,7 @@ namespace osu.Game.Screens.Edit
private EditorMenuItem cutMenuItem; private EditorMenuItem cutMenuItem;
private EditorMenuItem copyMenuItem; private EditorMenuItem copyMenuItem;
private EditorMenuItem cloneMenuItem;
private EditorMenuItem pasteMenuItem; private EditorMenuItem pasteMenuItem;
private readonly BindableWithCurrent<bool> canCut = new BindableWithCurrent<bool>(); private readonly BindableWithCurrent<bool> canCut = new BindableWithCurrent<bool>();
@ -750,7 +756,11 @@ namespace osu.Game.Screens.Edit
private void setUpClipboardActionAvailability() private void setUpClipboardActionAvailability()
{ {
canCut.Current.BindValueChanged(cut => cutMenuItem.Action.Disabled = !cut.NewValue, true); canCut.Current.BindValueChanged(cut => cutMenuItem.Action.Disabled = !cut.NewValue, true);
canCopy.Current.BindValueChanged(copy => copyMenuItem.Action.Disabled = !copy.NewValue, true); canCopy.Current.BindValueChanged(copy =>
{
copyMenuItem.Action.Disabled = !copy.NewValue;
cloneMenuItem.Action.Disabled = !copy.NewValue;
}, true);
canPaste.Current.BindValueChanged(paste => pasteMenuItem.Action.Disabled = !paste.NewValue, true); canPaste.Current.BindValueChanged(paste => pasteMenuItem.Action.Disabled = !paste.NewValue, true);
} }
@ -765,6 +775,21 @@ namespace osu.Game.Screens.Edit
protected void Copy() => currentScreen?.Copy(); protected void Copy() => currentScreen?.Copy();
protected void Clone()
{
// Avoid attempting to clone if copying is not available (as it may result in pasting something unexpected).
if (!canCopy.Value)
return;
// This is an initial implementation just to get an idea of how people used this function.
// There are a couple of differences from osu!stable's implementation which will require more work to match:
// - The "clipboard" is not populated during the duplication process.
// - The duplicated hitobjects are inserted after the original pattern (add one beat_length and then quantize using beat snap).
// - The duplicated hitobjects are selected (but this is also applied for all paste operations so should be changed there).
Copy();
Paste();
}
protected void Paste() => currentScreen?.Paste(); protected void Paste() => currentScreen?.Paste();
#endregion #endregion

View File

@ -110,6 +110,8 @@ namespace osu.Game.Tests.Visual
public new void Paste() => base.Paste(); public new void Paste() => base.Paste();
public new void Clone() => base.Clone();
public new void SwitchToDifficulty(BeatmapInfo beatmapInfo) => base.SwitchToDifficulty(beatmapInfo); public new void SwitchToDifficulty(BeatmapInfo beatmapInfo) => base.SwitchToDifficulty(beatmapInfo);
public new void CreateNewDifficulty(RulesetInfo rulesetInfo) => base.CreateNewDifficulty(rulesetInfo); public new void CreateNewDifficulty(RulesetInfo rulesetInfo) => base.CreateNewDifficulty(rulesetInfo);