Skip to content

Commit 9d07ad2

Browse files
committed
Implement form control for adding/removing custom samples in editor
The test scene doesn't exercise the custom sample playback, but I hope I can be forgiven for this as setting up a custom editor beatmap just for this to work is rather cumbersome.
1 parent 469aa7b commit 9d07ad2

File tree

4 files changed

+397
-1
lines changed

4 files changed

+397
-1
lines changed
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the MIT Licence.
2+
// See the LICENCE file in the repository root for full licence text.
3+
4+
using osu.Framework.Graphics;
5+
using osu.Framework.Graphics.Cursor;
6+
using osu.Game.Graphics.Cursor;
7+
using osu.Game.Screens.Edit;
8+
using osu.Game.Screens.Edit.Components;
9+
using osu.Game.Tests.Visual.UserInterface;
10+
11+
namespace osu.Game.Tests.Visual.Editing
12+
{
13+
public partial class TestSceneFormSampleSet : ThemeComparisonTestScene
14+
{
15+
public TestSceneFormSampleSet()
16+
: base(false)
17+
{
18+
}
19+
20+
protected override Drawable CreateContent() => new PopoverContainer
21+
{
22+
RelativeSizeAxes = Axes.Both,
23+
Child = new OsuContextMenuContainer
24+
{
25+
RelativeSizeAxes = Axes.Both,
26+
Child = new FormSampleSet
27+
{
28+
Current =
29+
{
30+
Value = new EditorBeatmapSkin.SampleSet(3, "Custom set #3")
31+
{
32+
Filenames = ["normal-hitwhistle3.wav"]
33+
}
34+
},
35+
Anchor = Anchor.Centre,
36+
Origin = Anchor.Centre,
37+
Width = 0.4f,
38+
}
39+
}
40+
};
41+
}
42+
}

osu.Game/Graphics/UserInterfaceV2/FormFileSelector.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ public Popover GetPopover()
252252
return popover;
253253
}
254254

255-
protected partial class FileChooserPopover : OsuPopover
255+
public partial class FileChooserPopover : OsuPopover
256256
{
257257
protected override string PopInSampleName => "UI/overlay-big-pop-in";
258258
protected override string PopOutSampleName => "UI/overlay-big-pop-out";
Lines changed: 346 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,346 @@
1+
// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the MIT Licence.
2+
// See the LICENCE file in the repository root for full licence text.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Diagnostics;
7+
using System.IO;
8+
using System.Linq;
9+
using osu.Framework.Allocation;
10+
using osu.Framework.Audio.Sample;
11+
using osu.Framework.Bindables;
12+
using osu.Framework.Extensions;
13+
using osu.Framework.Extensions.Color4Extensions;
14+
using osu.Framework.Graphics;
15+
using osu.Framework.Graphics.Colour;
16+
using osu.Framework.Graphics.Containers;
17+
using osu.Framework.Graphics.Cursor;
18+
using osu.Framework.Graphics.Shapes;
19+
using osu.Framework.Graphics.Sprites;
20+
using osu.Framework.Graphics.UserInterface;
21+
using osu.Framework.Input.Events;
22+
using osu.Framework.Localisation;
23+
using osu.Game.Audio;
24+
using osu.Game.Graphics;
25+
using osu.Game.Graphics.Backgrounds;
26+
using osu.Game.Graphics.Sprites;
27+
using osu.Game.Graphics.UserInterface;
28+
using osu.Game.Graphics.UserInterfaceV2;
29+
using osu.Game.Overlays;
30+
using osu.Game.Resources.Localisation.Web;
31+
using osu.Game.Utils;
32+
using osuTK;
33+
using osuTK.Graphics;
34+
35+
namespace osu.Game.Screens.Edit.Components
36+
{
37+
public partial class FormSampleSet : CompositeDrawable, IHasCurrentValue<EditorBeatmapSkin.SampleSet?>
38+
{
39+
public Bindable<EditorBeatmapSkin.SampleSet?> Current
40+
{
41+
get => current.Current;
42+
set => current.Current = value;
43+
}
44+
45+
public Func<FileInfo, string>? SampleAddRequested { get; init; }
46+
public Action<string>? SampleRemoveRequested { get; init; }
47+
48+
private readonly BindableWithCurrent<EditorBeatmapSkin.SampleSet?> current = new BindableWithCurrent<EditorBeatmapSkin.SampleSet?>();
49+
private readonly Dictionary<(string sound, string bank), SampleButton> buttons = new Dictionary<(string, string), SampleButton>();
50+
51+
private Box background = null!;
52+
private FormFieldCaption caption = null!;
53+
54+
[Resolved]
55+
private OverlayColourProvider colourProvider { get; set; } = null!;
56+
57+
[BackgroundDependencyLoader]
58+
private void load()
59+
{
60+
RelativeSizeAxes = Axes.X;
61+
AutoSizeAxes = Axes.Y;
62+
63+
Masking = true;
64+
CornerRadius = 5;
65+
66+
InternalChildren = new Drawable[]
67+
{
68+
background = new Box
69+
{
70+
RelativeSizeAxes = Axes.Both,
71+
Colour = colourProvider.Background5,
72+
},
73+
new FillFlowContainer
74+
{
75+
RelativeSizeAxes = Axes.X,
76+
AutoSizeAxes = Axes.Y,
77+
Padding = new MarginPadding(9),
78+
Spacing = new Vector2(7),
79+
Direction = FillDirection.Vertical,
80+
Children = new Drawable[]
81+
{
82+
caption = new FormFieldCaption(),
83+
new GridContainer
84+
{
85+
AutoSizeAxes = Axes.Both,
86+
RowDimensions = Enumerable.Repeat(new Dimension(GridSizeMode.AutoSize), 4).ToArray(),
87+
ColumnDimensions = Enumerable.Repeat(new Dimension(GridSizeMode.AutoSize), 5).ToArray(),
88+
Content = createTableContent().ToArray(),
89+
}
90+
},
91+
},
92+
};
93+
}
94+
95+
private IEnumerable<Drawable[]> createTableContent()
96+
{
97+
string[] columns = [HitSampleInfo.HIT_NORMAL, ..HitSampleInfo.ALL_ADDITIONS];
98+
string[] rows = HitSampleInfo.ALL_BANKS;
99+
100+
yield return columns.Select(makeTableHeading).Prepend(Empty()).ToArray();
101+
102+
foreach (string row in rows)
103+
{
104+
List<Drawable> drawables = [makeTableHeading(row)];
105+
106+
foreach (string col in columns)
107+
drawables.Add(buttons[(col, row)] = makeButton());
108+
109+
yield return drawables.ToArray();
110+
}
111+
}
112+
113+
private OsuSpriteText makeTableHeading(string text) => new OsuSpriteText
114+
{
115+
Text = text,
116+
Font = OsuFont.Style.Caption1,
117+
Anchor = Anchor.Centre,
118+
Origin = Anchor.Centre,
119+
};
120+
121+
private SampleButton makeButton() => new SampleButton
122+
{
123+
Width = 60,
124+
Anchor = Anchor.Centre,
125+
Origin = Anchor.Centre,
126+
Margin = new MarginPadding(5),
127+
SampleAddRequested = SampleAddRequested,
128+
SampleRemoveRequested = SampleRemoveRequested,
129+
};
130+
131+
protected override void LoadComplete()
132+
{
133+
base.LoadComplete();
134+
135+
updateState();
136+
Current.BindValueChanged(setChanged, true);
137+
}
138+
139+
private void setChanged(ValueChangedEvent<EditorBeatmapSkin.SampleSet?> valueChangedEvent)
140+
{
141+
var set = valueChangedEvent.NewValue;
142+
143+
caption.Caption = set?.Name ?? default(LocalisableString);
144+
Alpha = set != null && set.SampleSetIndex > 0 ? 1 : 0;
145+
146+
if (set != null)
147+
{
148+
foreach (var (sound, button) in buttons)
149+
{
150+
button.ExpectedFilename.Value = $@"{sound.bank}-{sound.sound}{(set.SampleSetIndex > 1 ? set.SampleSetIndex : null)}";
151+
button.ActualFilename.Value = set.FindSound(sound.sound, sound.bank);
152+
}
153+
}
154+
}
155+
156+
protected override bool OnHover(HoverEvent e)
157+
{
158+
updateState();
159+
return true;
160+
}
161+
162+
protected override void OnHoverLost(HoverLostEvent e)
163+
{
164+
updateState();
165+
base.OnHoverLost(e);
166+
}
167+
168+
private void updateState()
169+
{
170+
background.Colour = colourProvider.Background5;
171+
caption.Colour = colourProvider.Content2;
172+
173+
BorderThickness = IsHovered ? 2 : 0;
174+
175+
if (IsHovered)
176+
BorderColour = colourProvider.Light4;
177+
}
178+
179+
public partial class SampleButton : OsuButton, IHasPopover, IHasContextMenu
180+
{
181+
/// <summary>
182+
/// The expected filename for the sample that this button represents.
183+
/// Does not contain extension.
184+
/// </summary>
185+
public Bindable<string> ExpectedFilename { get; } = new Bindable<string>();
186+
187+
/// <summary>
188+
/// The actual chosen filename for the sample that this button represent.
189+
/// Can be <see langword="null"/> if the sample is omitted / missing.
190+
/// Does contain extension.
191+
/// </summary>
192+
public Bindable<string?> ActualFilename { get; } = new Bindable<string?>();
193+
194+
/// <summary>
195+
/// Invoked when a new sample is selected via this button.
196+
/// </summary>
197+
public Func<FileInfo, string>? SampleAddRequested { get; init; }
198+
199+
/// <summary>
200+
/// Invoked when a sample removal is selected via this button.
201+
/// </summary>
202+
public Action<string>? SampleRemoveRequested { get; init; }
203+
204+
private Bindable<FileInfo?> selectedFile { get; } = new Bindable<FileInfo?>();
205+
206+
private TrianglesV2? triangles { get; set; }
207+
208+
protected override float HoverLayerFinalAlpha => 0;
209+
210+
private Color4? triangleGradientSecondColour;
211+
private SpriteIcon icon = null!;
212+
213+
[Resolved]
214+
private OverlayColourProvider overlayColourProvider { get; set; } = null!;
215+
216+
[Resolved]
217+
private EditorBeatmap? editorBeatmap { get; set; }
218+
219+
private HoverSounds? hoverSounds;
220+
private ISample? sample;
221+
222+
public SampleButton()
223+
: base(null)
224+
{
225+
}
226+
227+
[BackgroundDependencyLoader]
228+
private void load()
229+
{
230+
Add(icon = new SpriteIcon
231+
{
232+
Icon = FontAwesome.Solid.Plus,
233+
Size = new Vector2(16),
234+
Shadow = true,
235+
Anchor = Anchor.Centre,
236+
Origin = Anchor.Centre,
237+
});
238+
239+
Action = () =>
240+
{
241+
if (ActualFilename.Value == null)
242+
{
243+
selectedFile.Value = null;
244+
this.ShowPopover();
245+
}
246+
else
247+
sample?.Play();
248+
};
249+
250+
if (editorBeatmap?.BeatmapSkin != null)
251+
editorBeatmap.BeatmapSkin.BeatmapSkinChanged += recycleSamples;
252+
}
253+
254+
protected override void LoadComplete()
255+
{
256+
base.LoadComplete();
257+
258+
Content.CornerRadius = 4;
259+
260+
Add(triangles = new TrianglesV2
261+
{
262+
Thickness = 0.02f,
263+
SpawnRatio = 0.6f,
264+
RelativeSizeAxes = Axes.Both,
265+
Depth = float.MaxValue,
266+
});
267+
268+
ActualFilename.BindValueChanged(_ => updateState(), true);
269+
selectedFile.BindValueChanged(_ => addSample());
270+
}
271+
272+
private void updateState()
273+
{
274+
BackgroundColour = ActualFilename.Value == null ? overlayColourProvider.Background3 : overlayColourProvider.Colour3;
275+
triangleGradientSecondColour = BackgroundColour.Lighten(0.2f);
276+
icon.Icon = ActualFilename.Value == null ? FontAwesome.Solid.Plus : FontAwesome.Solid.Play;
277+
278+
recycleSamples();
279+
280+
if (triangles == null)
281+
return;
282+
283+
triangles.Colour = ColourInfo.GradientVertical(triangleGradientSecondColour.Value, BackgroundColour);
284+
}
285+
286+
private void recycleSamples()
287+
{
288+
if (hoverSounds?.Parent == this)
289+
{
290+
RemoveInternal(hoverSounds, true);
291+
hoverSounds = null;
292+
}
293+
294+
AddInternal(hoverSounds = (ActualFilename.Value == null ? new HoverClickSounds(HoverSampleSet.Button) : new HoverSounds(HoverSampleSet.Button)));
295+
296+
sample = ActualFilename.Value == null ? null : editorBeatmap?.BeatmapSkin?.Skin.Samples?.Get(ActualFilename.Value);
297+
}
298+
299+
protected override bool OnHover(HoverEvent e)
300+
{
301+
Debug.Assert(triangleGradientSecondColour != null);
302+
303+
Background.FadeColour(triangleGradientSecondColour.Value, 300, Easing.OutQuint);
304+
return base.OnHover(e);
305+
}
306+
307+
protected override void OnHoverLost(HoverLostEvent e)
308+
{
309+
Background.FadeColour(BackgroundColour, 300, Easing.OutQuint);
310+
base.OnHoverLost(e);
311+
}
312+
313+
private void addSample()
314+
{
315+
if (selectedFile.Value == null)
316+
return;
317+
318+
this.HidePopover();
319+
ActualFilename.Value = SampleAddRequested?.Invoke(selectedFile.Value) ?? selectedFile.Value.ToString();
320+
}
321+
322+
private void deleteSample()
323+
{
324+
if (ActualFilename.Value == null)
325+
return;
326+
327+
SampleRemoveRequested?.Invoke(ActualFilename.Value);
328+
ActualFilename.Value = null;
329+
}
330+
331+
public Popover? GetPopover() => ActualFilename.Value == null ? new FormFileSelector.FileChooserPopover(SupportedExtensions.AUDIO_EXTENSIONS, selectedFile, null) : null;
332+
333+
public MenuItem[]? ContextMenuItems =>
334+
ActualFilename.Value != null
335+
? [new OsuMenuItem(CommonStrings.ButtonsDelete, MenuItemType.Destructive, deleteSample)]
336+
: null;
337+
338+
protected override void Dispose(bool isDisposing)
339+
{
340+
if (editorBeatmap?.BeatmapSkin != null)
341+
editorBeatmap.BeatmapSkin.BeatmapSkinChanged -= recycleSamples;
342+
base.Dispose(isDisposing);
343+
}
344+
}
345+
}
346+
}

0 commit comments

Comments
 (0)