1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-22 04:07:25 +08:00

Implement hitobject sound adjustment (#6762)

Implement hitobject sound adjustment

Co-authored-by: Dean Herbert <pe@ppy.sh>
This commit is contained in:
Dean Herbert 2019-11-12 16:40:15 +09:00 committed by GitHub
commit 3ab332e60b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 348 additions and 143 deletions

View File

@ -126,7 +126,7 @@ namespace osu.Game.Rulesets.Catch.Objects
public double Distance => Path.Distance;
public List<List<HitSampleInfo>> NodeSamples { get; set; } = new List<List<HitSampleInfo>>();
public List<IList<HitSampleInfo>> NodeSamples { get; set; } = new List<IList<HitSampleInfo>>();
public double? LegacyLastTickOffset { get; set; }
}

View File

@ -255,7 +255,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
/// </summary>
/// <param name="time">The time to retrieve the sample info list from.</param>
/// <returns></returns>
private List<HitSampleInfo> sampleInfoListAt(double time)
private IList<HitSampleInfo> sampleInfoListAt(double time)
{
var curveData = HitObject as IHasCurve;

View File

@ -472,7 +472,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// </summary>
/// <param name="time">The time to retrieve the sample info list from.</param>
/// <returns></returns>
private List<HitSampleInfo> sampleInfoListAt(double time)
private IList<HitSampleInfo> sampleInfoListAt(double time)
{
var curveData = HitObject as IHasCurve;

View File

@ -126,6 +126,67 @@ namespace osu.Game.Rulesets.Osu.Tests
AddAssert("body positioned correctly", () => slider.Position == slider.HitObject.StackedPosition);
}
[Test]
public void TestChangeSamplesWithNoNodeSamples()
{
DrawableSlider slider = null;
AddStep("create slider", () =>
{
slider = (DrawableSlider)createSlider(repeats: 1);
Add(slider);
});
AddStep("change samples", () => slider.HitObject.Samples = new[]
{
new HitSampleInfo { Name = HitSampleInfo.HIT_CLAP },
new HitSampleInfo { Name = HitSampleInfo.HIT_WHISTLE },
});
AddAssert("head samples updated", () => assertSamples(((Slider)slider.HitObject).HeadCircle));
AddAssert("tick samples not updated", () => ((Slider)slider.HitObject).NestedHitObjects.OfType<SliderTick>().All(assertTickSamples));
AddAssert("repeat samples updated", () => ((Slider)slider.HitObject).NestedHitObjects.OfType<RepeatPoint>().All(assertSamples));
AddAssert("tail has no samples", () => ((Slider)slider.HitObject).TailCircle.Samples.Count == 0);
bool assertTickSamples(SliderTick tick) => tick.Samples.Single().Name == "slidertick";
bool assertSamples(HitObject hitObject)
{
return hitObject.Samples.Any(s => s.Name == HitSampleInfo.HIT_CLAP)
&& hitObject.Samples.Any(s => s.Name == HitSampleInfo.HIT_WHISTLE);
}
}
[Test]
public void TestChangeSamplesWithNodeSamples()
{
DrawableSlider slider = null;
AddStep("create slider", () =>
{
slider = (DrawableSlider)createSlider(repeats: 1);
for (int i = 0; i < 2; i++)
((Slider)slider.HitObject).NodeSamples.Add(new List<HitSampleInfo> { new HitSampleInfo { Name = HitSampleInfo.HIT_FINISH } });
Add(slider);
});
AddStep("change samples", () => slider.HitObject.Samples = new[]
{
new HitSampleInfo { Name = HitSampleInfo.HIT_CLAP },
new HitSampleInfo { Name = HitSampleInfo.HIT_WHISTLE },
});
AddAssert("head samples not updated", () => assertSamples(((Slider)slider.HitObject).HeadCircle));
AddAssert("tick samples not updated", () => ((Slider)slider.HitObject).NestedHitObjects.OfType<SliderTick>().All(assertTickSamples));
AddAssert("repeat samples not updated", () => ((Slider)slider.HitObject).NestedHitObjects.OfType<RepeatPoint>().All(assertSamples));
AddAssert("tail has no samples", () => ((Slider)slider.HitObject).TailCircle.Samples.Count == 0);
bool assertTickSamples(SliderTick tick) => tick.Samples.Single().Name == "slidertick";
bool assertSamples(HitObject hitObject) => hitObject.Samples.All(s => s.Name != HitSampleInfo.HIT_CLAP && s.Name != HitSampleInfo.HIT_WHISTLE);
}
private Drawable testSimpleBig(int repeats = 0) => createSlider(2, repeats: repeats);
private Drawable testSimpleBigLargeStackOffset(int repeats = 0) => createSlider(2, repeats: repeats, stackHeight: 10);
@ -143,7 +204,6 @@ namespace osu.Game.Rulesets.Osu.Tests
new Vector2(52, -34)
}, 700),
RepeatCount = repeats,
NodeSamples = createEmptySamples(repeats),
StackHeight = 10
};
@ -174,7 +234,6 @@ namespace osu.Game.Rulesets.Osu.Tests
new Vector2(distance, 0),
}, distance),
RepeatCount = repeats,
NodeSamples = createEmptySamples(repeats),
StackHeight = stackHeight
};
@ -194,7 +253,6 @@ namespace osu.Game.Rulesets.Osu.Tests
new Vector2(400, 0)
}, 600),
RepeatCount = repeats,
NodeSamples = createEmptySamples(repeats)
};
return createDrawable(slider, 2, 3);
@ -218,7 +276,6 @@ namespace osu.Game.Rulesets.Osu.Tests
new Vector2(430, 0)
}),
RepeatCount = repeats,
NodeSamples = createEmptySamples(repeats)
};
return createDrawable(slider, 2, 3);
@ -241,7 +298,6 @@ namespace osu.Game.Rulesets.Osu.Tests
new Vector2(430, 0)
}),
RepeatCount = repeats,
NodeSamples = createEmptySamples(repeats)
};
return createDrawable(slider, 2, 3);
@ -265,7 +321,6 @@ namespace osu.Game.Rulesets.Osu.Tests
new Vector2(0, -200)
}),
RepeatCount = repeats,
NodeSamples = createEmptySamples(repeats)
};
return createDrawable(slider, 2, 3);
@ -275,7 +330,7 @@ namespace osu.Game.Rulesets.Osu.Tests
private Drawable createCatmull(int repeats = 0)
{
var repeatSamples = new List<List<HitSampleInfo>>();
var repeatSamples = new List<IList<HitSampleInfo>>();
for (int i = 0; i < repeats; i++)
repeatSamples.Add(new List<HitSampleInfo>());
@ -297,14 +352,6 @@ namespace osu.Game.Rulesets.Osu.Tests
return createDrawable(slider, 3, 1);
}
private List<List<HitSampleInfo>> createEmptySamples(int repeats)
{
var repeatSamples = new List<List<HitSampleInfo>>();
for (int i = 0; i < repeats; i++)
repeatSamples.Add(new List<HitSampleInfo>());
return repeatSamples;
}
private Drawable createDrawable(Slider slider, float circleSize, double speedMultiplier)
{
var cpi = new ControlPointInfo();

View File

@ -70,7 +70,7 @@ namespace osu.Game.Rulesets.Osu.Objects
/// </summary>
internal float LazyTravelDistance;
public List<List<HitSampleInfo>> NodeSamples { get; set; } = new List<List<HitSampleInfo>>();
public List<IList<HitSampleInfo>> NodeSamples { get; set; } = new List<IList<HitSampleInfo>>();
private int repeatCount;
@ -108,6 +108,12 @@ namespace osu.Game.Rulesets.Osu.Objects
public HitCircle HeadCircle;
public SliderTailCircle TailCircle;
public Slider()
{
SamplesBindable.ItemsAdded += _ => updateNestedSamples();
SamplesBindable.ItemsRemoved += _ => updateNestedSamples();
}
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
{
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
@ -128,20 +134,6 @@ namespace osu.Game.Rulesets.Osu.Objects
foreach (var e in
SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset))
{
var firstSample = Samples.Find(s => s.Name == HitSampleInfo.HIT_NORMAL)
?? Samples.FirstOrDefault(); // TODO: remove this when guaranteed sort is present for samples (https://github.com/ppy/osu/issues/1933)
var sampleList = new List<HitSampleInfo>();
if (firstSample != null)
{
sampleList.Add(new HitSampleInfo
{
Bank = firstSample.Bank,
Volume = firstSample.Volume,
Name = @"slidertick",
});
}
switch (e.Type)
{
case SliderEventType.Tick:
@ -153,7 +145,6 @@ namespace osu.Game.Rulesets.Osu.Objects
Position = Position + Path.PositionAt(e.PathProgress),
StackHeight = StackHeight,
Scale = Scale,
Samples = sampleList
});
break;
@ -163,7 +154,6 @@ namespace osu.Game.Rulesets.Osu.Objects
StartTime = e.Time,
Position = Position,
StackHeight = StackHeight,
Samples = getNodeSamples(0),
SampleControlPoint = SampleControlPoint,
});
break;
@ -189,11 +179,12 @@ namespace osu.Game.Rulesets.Osu.Objects
Position = Position + Path.PositionAt(e.PathProgress),
StackHeight = StackHeight,
Scale = Scale,
Samples = getNodeSamples(e.SpanIndex + 1)
});
break;
}
}
updateNestedSamples();
}
private void updateNestedPositions()
@ -205,7 +196,33 @@ namespace osu.Game.Rulesets.Osu.Objects
TailCircle.Position = EndPosition;
}
private List<HitSampleInfo> getNodeSamples(int nodeIndex) =>
private void updateNestedSamples()
{
var firstSample = Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL)
?? Samples.FirstOrDefault(); // TODO: remove this when guaranteed sort is present for samples (https://github.com/ppy/osu/issues/1933)
var sampleList = new List<HitSampleInfo>();
if (firstSample != null)
{
sampleList.Add(new HitSampleInfo
{
Bank = firstSample.Bank,
Volume = firstSample.Volume,
Name = @"slidertick",
});
}
foreach (var tick in NestedHitObjects.OfType<SliderTick>())
tick.Samples = sampleList;
foreach (var repeat in NestedHitObjects.OfType<RepeatPoint>())
repeat.Samples = getNodeSamples(repeat.RepeatIndex + 1);
if (HeadCircle != null)
HeadCircle.Samples = getNodeSamples(0);
}
private IList<HitSampleInfo> getNodeSamples(int nodeIndex) =>
nodeIndex < NodeSamples.Count ? NodeSamples[nodeIndex] : Samples;
public override Judgement CreateJudgement() => new OsuJudgement();

View File

@ -79,7 +79,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
var curveData = obj as IHasCurve;
// Old osu! used hit sounding to determine various hit type information
List<HitSampleInfo> samples = obj.Samples;
IList<HitSampleInfo> samples = obj.Samples;
bool strong = samples.Any(s => s.Name == HitSampleInfo.HIT_FINISH);
@ -117,13 +117,13 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
if (!isForCurrentRuleset && tickSpacing > 0 && osuDuration < 2 * speedAdjustedBeatLength)
{
List<List<HitSampleInfo>> allSamples = curveData != null ? curveData.NodeSamples : new List<List<HitSampleInfo>>(new[] { samples });
List<IList<HitSampleInfo>> allSamples = curveData != null ? curveData.NodeSamples : new List<IList<HitSampleInfo>>(new[] { samples });
int i = 0;
for (double j = obj.StartTime; j <= obj.StartTime + taikoDuration + tickSpacing / 8; j += tickSpacing)
{
List<HitSampleInfo> currentSamples = allSamples[i];
IList<HitSampleInfo> currentSamples = allSamples[i];
bool isRim = currentSamples.Any(s => s.Name == HitSampleInfo.HIT_CLAP || s.Name == HitSampleInfo.HIT_WHISTLE);
strong = currentSamples.Any(s => s.Name == HitSampleInfo.HIT_FINISH);

View File

@ -1,11 +1,12 @@
// 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 osu.Framework.Configuration.Tracking;
namespace osu.Game.Rulesets.Configuration
{
public interface IRulesetConfigManager : ITrackableConfigManager
public interface IRulesetConfigManager : ITrackableConfigManager, IDisposable
{
}
}

View File

@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
// Todo: Rulesets should be overriding the resources instead, but we need to figure out where/when to apply overrides first
protected virtual string SampleNamespace => null;
protected SkinnableSound Samples;
protected SkinnableSound Samples { get; private set; }
protected virtual IEnumerable<HitSampleInfo> GetSamples() => HitObject.Samples;
@ -78,6 +78,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
/// </summary>
public JudgementResult Result { get; private set; }
private BindableList<HitSampleInfo> samplesBindable;
private Bindable<double> startTimeBindable;
private Bindable<int> comboIndexBindable;
@ -108,22 +109,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
throw new InvalidOperationException($"{GetType().ReadableName()} must provide a {nameof(JudgementResult)} through {nameof(CreateResult)}.");
}
var samples = GetSamples().ToArray();
if (samples.Length > 0)
{
if (HitObject.SampleControlPoint == null)
{
throw new ArgumentNullException(nameof(HitObject.SampleControlPoint), $"{nameof(HitObject)}s must always have an attached {nameof(HitObject.SampleControlPoint)}."
+ $" This is an indication that {nameof(HitObject.ApplyDefaults)} has not been invoked on {this}.");
}
samples = samples.Select(s => HitObject.SampleControlPoint.ApplyTo(s)).ToArray();
foreach (var s in samples)
s.Namespace = SampleNamespace;
AddInternal(Samples = new SkinnableSound(samples));
}
loadSamples();
}
protected override void LoadComplete()
@ -141,10 +127,40 @@ namespace osu.Game.Rulesets.Objects.Drawables
comboIndexBindable.BindValueChanged(_ => updateAccentColour(), true);
}
samplesBindable = HitObject.SamplesBindable.GetBoundCopy();
samplesBindable.ItemsAdded += _ => loadSamples();
samplesBindable.ItemsRemoved += _ => loadSamples();
updateState(ArmedState.Idle, true);
onDefaultsApplied();
}
private void loadSamples()
{
if (Samples != null)
{
RemoveInternal(Samples);
Samples = null;
}
var samples = GetSamples().ToArray();
if (samples.Length <= 0)
return;
if (HitObject.SampleControlPoint == null)
{
throw new ArgumentNullException(nameof(HitObject.SampleControlPoint), $"{nameof(HitObject)}s must always have an attached {nameof(HitObject.SampleControlPoint)}."
+ $" This is an indication that {nameof(HitObject.ApplyDefaults)} has not been invoked on {this}.");
}
samples = samples.Select(s => HitObject.SampleControlPoint.ApplyTo(s)).ToArray();
foreach (var s in samples)
s.Namespace = SampleNamespace;
AddInternal(Samples = new SkinnableSound(samples));
}
private void onDefaultsApplied() => apply(HitObject);
private void apply(HitObject hitObject)

View File

@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Objects
set => StartTimeBindable.Value = value;
}
private List<HitSampleInfo> samples;
public readonly BindableList<HitSampleInfo> SamplesBindable = new BindableList<HitSampleInfo>();
/// <summary>
/// The samples to be played when this hit object is hit.
@ -54,10 +54,14 @@ namespace osu.Game.Rulesets.Objects
/// and can be treated as the default samples for the hit object.
/// </para>
/// </summary>
public List<HitSampleInfo> Samples
public IList<HitSampleInfo> Samples
{
get => samples ?? (samples = new List<HitSampleInfo>());
set => samples = value;
get => SamplesBindable;
set
{
SamplesBindable.Clear();
SamplesBindable.AddRange(value);
}
}
[JsonIgnore]

View File

@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch
}
protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, Vector2[] controlPoints, double? length, PathType pathType, int repeatCount,
List<List<HitSampleInfo>> nodeSamples)
List<IList<HitSampleInfo>> nodeSamples)
{
newCombo |= forceNewCombo;
comboOffset += extraComboOffset;

View File

@ -184,7 +184,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
}
// Generate the final per-node samples
var nodeSamples = new List<List<HitSampleInfo>>(nodes);
var nodeSamples = new List<IList<HitSampleInfo>>(nodes);
for (int i = 0; i < nodes; i++)
nodeSamples.Add(convertSoundType(nodeSoundTypes[i], nodeBankInfos[i]));
@ -282,7 +282,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
/// <param name="nodeSamples">The samples to be played when the slider nodes are hit. This includes the head and tail of the slider.</param>
/// <returns>The hit object.</returns>
protected abstract HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, Vector2[] controlPoints, double? length, PathType pathType, int repeatCount,
List<List<HitSampleInfo>> nodeSamples);
List<IList<HitSampleInfo>> nodeSamples);
/// <summary>
/// Creates a legacy Spinner-type hit object.

View File

@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
public double Distance => Path.Distance;
public List<List<HitSampleInfo>> NodeSamples { get; set; }
public List<IList<HitSampleInfo>> NodeSamples { get; set; }
public int RepeatCount { get; set; }
public double EndTime => StartTime + this.SpanCount() * Distance / Velocity;

View File

@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Mania
}
protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, Vector2[] controlPoints, double? length, PathType pathType, int repeatCount,
List<List<HitSampleInfo>> nodeSamples)
List<IList<HitSampleInfo>> nodeSamples)
{
return new ConvertSlider
{

View File

@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu
}
protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, Vector2[] controlPoints, double? length, PathType pathType, int repeatCount,
List<List<HitSampleInfo>> nodeSamples)
List<IList<HitSampleInfo>> nodeSamples)
{
newCombo |= forceNewCombo;
comboOffset += extraComboOffset;

View File

@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Taiko
}
protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, Vector2[] controlPoints, double? length, PathType pathType, int repeatCount,
List<List<HitSampleInfo>> nodeSamples)
List<IList<HitSampleInfo>> nodeSamples)
{
return new ConvertSlider
{

View File

@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Objects.Types
/// n-1: The last repeat.<br />
/// n: The last node.
/// </summary>
List<List<HitSampleInfo>> NodeSamples { get; }
List<IList<HitSampleInfo>> NodeSamples { get; }
}
public static class HasRepeatsExtensions

View File

@ -43,7 +43,7 @@ namespace osu.Game.Rulesets
// ensures any potential database operations are finalised before game destruction.
foreach (var c in configCache.Values)
(c as IDisposable)?.Dispose();
c?.Dispose();
}
}
}

View File

@ -17,6 +17,7 @@ using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osuTK;
using osuTK.Input;
namespace osu.Game.Screens.Edit.Compose.Components
{
@ -96,11 +97,14 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected override bool OnMouseDown(MouseDownEvent e)
{
beginClickSelection(e);
return true;
return e.Button == MouseButton.Left;
}
protected override bool OnClick(ClickEvent e)
{
if (e.Button == MouseButton.Right)
return false;
// Deselection should only occur if no selected blueprints are hovered
// A special case for when a blueprint was selected via this click is added since OnClick() may occur outside the hitobject and should not trigger deselection
if (endClickSelection() || selectionHandler.SelectedBlueprints.Any(b => b.IsHovered))
@ -112,6 +116,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected override bool OnDoubleClick(DoubleClickEvent e)
{
if (e.Button == MouseButton.Right)
return false;
SelectionBlueprint clickedBlueprint = selectionHandler.SelectedBlueprints.FirstOrDefault(b => b.IsHovered);
if (clickedBlueprint == null)
@ -125,7 +132,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
// Special case for when a drag happened instead of a click
Schedule(() => endClickSelection());
return true;
return e.Button == MouseButton.Left;
}
protected override bool OnMouseMove(MouseMoveEvent e)
@ -141,6 +148,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected override bool OnDragStart(DragStartEvent e)
{
if (e.Button == MouseButton.Right)
return false;
if (!beginSelectionMovement())
{
dragBox.UpdateDrag(e);
@ -152,6 +162,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected override bool OnDrag(DragEvent e)
{
if (e.Button == MouseButton.Right)
return false;
if (!moveCurrentSelection(e))
dragBox.UpdateDrag(e);
@ -160,6 +173,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected override bool OnDragEnd(DragEndEvent e)
{
if (e.Button == MouseButton.Right)
return false;
if (!finishSelectionMovement())
{
dragBox.FadeOut(250, Easing.OutQuint);

View File

@ -7,11 +7,15 @@ using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.States;
using osu.Game.Audio;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
@ -22,7 +26,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// <summary>
/// A component which outlines <see cref="DrawableHitObject"/>s and handles movement of selections.
/// </summary>
public class SelectionHandler : CompositeDrawable, IKeyBindingHandler<PlatformAction>
public class SelectionHandler : CompositeDrawable, IKeyBindingHandler<PlatformAction>, IHasContextMenu
{
public const float BORDER_RADIUS = 2;
@ -142,6 +146,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
#endregion
#region Outline Display
/// <summary>
/// Updates whether this <see cref="SelectionHandler"/> is visible.
/// </summary>
@ -176,5 +182,98 @@ namespace osu.Game.Screens.Edit.Compose.Components
outline.Size = bottomRight - topLeft;
outline.Position = topLeft;
}
#endregion
#region Sample Changes
/// <summary>
/// Adds a hit sample to all selected <see cref="HitObject"/>s.
/// </summary>
/// <param name="sampleName">The name of the hit sample.</param>
public void AddHitSample(string sampleName)
{
foreach (var h in SelectedHitObjects)
{
// Make sure there isn't already an existing sample
if (h.Samples.Any(s => s.Name == sampleName))
continue;
h.Samples.Add(new HitSampleInfo { Name = sampleName });
}
}
/// <summary>
/// Removes a hit sample from all selected <see cref="HitObject"/>s.
/// </summary>
/// <param name="sampleName">The name of the hit sample.</param>
public void RemoveHitSample(string sampleName)
{
foreach (var h in SelectedHitObjects)
h.SamplesBindable.RemoveAll(s => s.Name == sampleName);
}
#endregion
#region Context Menu
public virtual MenuItem[] ContextMenuItems
{
get
{
if (!selectedBlueprints.Any(b => b.IsHovered))
return Array.Empty<MenuItem>();
return new MenuItem[]
{
new OsuMenuItem("Sound")
{
Items = new[]
{
createHitSampleMenuItem("Whistle", HitSampleInfo.HIT_WHISTLE),
createHitSampleMenuItem("Clap", HitSampleInfo.HIT_CLAP),
createHitSampleMenuItem("Finish", HitSampleInfo.HIT_FINISH)
}
}
};
}
}
private MenuItem createHitSampleMenuItem(string name, string sampleName)
{
return new TernaryStateMenuItem(name, MenuItemType.Standard, setHitSampleState)
{
State = { Value = getHitSampleState() }
};
void setHitSampleState(TernaryState state)
{
switch (state)
{
case TernaryState.False:
RemoveHitSample(sampleName);
break;
case TernaryState.True:
AddHitSample(sampleName);
break;
}
}
TernaryState getHitSampleState()
{
int countExisting = SelectedHitObjects.Count(h => h.Samples.Any(s => s.Name == sampleName));
if (countExisting == 0)
return TernaryState.False;
if (countExisting < SelectedHitObjects.Count())
return TernaryState.Indeterminate;
return TernaryState.True;
}
}
#endregion
}
}

View File

@ -24,6 +24,7 @@ using osuTK.Input;
using System.Collections.Generic;
using osu.Framework;
using osu.Framework.Input.Bindings;
using osu.Game.Graphics.Cursor;
using osu.Game.Input.Bindings;
using osu.Game.Screens.Edit.Compose;
using osu.Game.Screens.Edit.Setup;
@ -90,87 +91,91 @@ namespace osu.Game.Screens.Edit
fileMenuItems.Add(new EditorMenuItem("Exit", MenuItemType.Standard, this.Exit));
InternalChildren = new[]
InternalChild = new OsuContextMenuContainer
{
new Container
RelativeSizeAxes = Axes.Both,
Children = new[]
{
Name = "Screen container",
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Top = 40, Bottom = 60 },
Child = screenContainer = new Container
new Container
{
Name = "Screen container",
RelativeSizeAxes = Axes.Both,
Masking = true
}
},
new Container
{
Name = "Top bar",
RelativeSizeAxes = Axes.X,
Height = 40,
Child = menuBar = new EditorMenuBar
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.Both,
Items = new[]
{
new MenuItem("File")
{
Items = fileMenuItems
}
}
}
},
new Container
{
Name = "Bottom bar",
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
RelativeSizeAxes = Axes.X,
Height = 60,
Children = new Drawable[]
{
bottomBackground = new Box { RelativeSizeAxes = Axes.Both },
new Container
Padding = new MarginPadding { Top = 40, Bottom = 60 },
Child = screenContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Vertical = 5, Horizontal = 10 },
Child = new GridContainer
Masking = true
}
},
new Container
{
Name = "Top bar",
RelativeSizeAxes = Axes.X,
Height = 40,
Child = menuBar = new EditorMenuBar
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.Both,
Items = new[]
{
new MenuItem("File")
{
Items = fileMenuItems
}
}
}
},
new Container
{
Name = "Bottom bar",
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
RelativeSizeAxes = Axes.X,
Height = 60,
Children = new Drawable[]
{
bottomBackground = new Box { RelativeSizeAxes = Axes.Both },
new Container
{
RelativeSizeAxes = Axes.Both,
ColumnDimensions = new[]
Padding = new MarginPadding { Vertical = 5, Horizontal = 10 },
Child = new GridContainer
{
new Dimension(GridSizeMode.Absolute, 220),
new Dimension(),
new Dimension(GridSizeMode.Absolute, 220)
},
Content = new[]
{
new Drawable[]
RelativeSizeAxes = Axes.Both,
ColumnDimensions = new[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Right = 10 },
Child = new TimeInfoContainer { RelativeSizeAxes = Axes.Both },
},
new SummaryTimeline
{
RelativeSizeAxes = Axes.Both,
},
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Left = 10 },
Child = new PlaybackControl { RelativeSizeAxes = Axes.Both },
}
new Dimension(GridSizeMode.Absolute, 220),
new Dimension(),
new Dimension(GridSizeMode.Absolute, 220)
},
}
},
Content = new[]
{
new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Right = 10 },
Child = new TimeInfoContainer { RelativeSizeAxes = Axes.Both },
},
new SummaryTimeline
{
RelativeSizeAxes = Axes.Both,
},
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Left = 10 },
Child = new PlaybackControl { RelativeSizeAxes = Axes.Both },
}
},
}
},
}
}
}
},
},
}
};
menuBar.Mode.ValueChanged += onModeChanged;