[同步官方更新]

This commit is contained in:
LA
2025-12-21 01:28:16 +08:00
parent 1b7b92a551
commit ac488b9ae7
19 changed files with 335 additions and 162 deletions

View File

@@ -21,7 +21,7 @@
]
},
"ppy.localisationanalyser.tools": {
"version": "2024.802.0",
"version": "2025.1208.0",
"commands": [
"localisation"
]

View File

@@ -5,7 +5,6 @@ using System.Linq;
using System.Collections.Generic;
using Humanizer;
using NUnit.Framework;
using osu.Framework.Input;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Audio;
@@ -196,11 +195,6 @@ namespace osu.Game.Tests.Visual.Editing
clickSamplePiece(1);
samplePopoverHasSingleBank(HitSampleInfo.BANK_SOFT);
setBankViaPopover(string.Empty);
hitObjectHasSampleBank(0, HitSampleInfo.BANK_SOFT);
hitObjectHasSampleBank(1, HitSampleInfo.BANK_SOFT);
samplePopoverHasSingleBank(HitSampleInfo.BANK_SOFT);
setBankViaPopover(HitSampleInfo.BANK_DRUM);
hitObjectHasSampleBank(0, HitSampleInfo.BANK_DRUM);
hitObjectHasSampleBank(1, HitSampleInfo.BANK_DRUM);
@@ -219,11 +213,6 @@ namespace osu.Game.Tests.Visual.Editing
clickSamplePiece(1);
samplePopoverHasIndeterminateBank();
setBankViaPopover(string.Empty);
hitObjectHasSampleBank(0, HitSampleInfo.BANK_NORMAL);
hitObjectHasSampleBank(1, HitSampleInfo.BANK_SOFT);
samplePopoverHasIndeterminateBank();
setBankViaPopover(HitSampleInfo.BANK_NORMAL);
hitObjectHasSampleBank(0, HitSampleInfo.BANK_NORMAL);
hitObjectHasSampleBank(1, HitSampleInfo.BANK_NORMAL);
@@ -878,17 +867,17 @@ namespace osu.Game.Tests.Visual.Editing
private void samplePopoverHasSingleBank(string bank) => AddUntilStep($"sample popover has bank {bank}", () =>
{
var popover = this.ChildrenOfType<SamplePointPiece.SampleEditPopover>().SingleOrDefault();
var textBox = popover?.ChildrenOfType<OsuTextBox>().First();
var dropdown = popover?.ChildrenOfType<LabelledDropdown<string>>().First();
return textBox?.Current.Value == bank && string.IsNullOrEmpty(textBox.PlaceholderText.ToString());
return dropdown?.Current.Value == bank;
});
private void samplePopoverHasIndeterminateBank() => AddUntilStep("sample popover has indeterminate bank", () =>
{
var popover = this.ChildrenOfType<SamplePointPiece.SampleEditPopover>().SingleOrDefault();
var textBox = popover?.ChildrenOfType<OsuTextBox>().First();
var dropdown = popover?.ChildrenOfType<LabelledDropdown<string>>().First();
return textBox != null && string.IsNullOrEmpty(textBox.Current.Value) && !string.IsNullOrEmpty(textBox.PlaceholderText.ToString());
return dropdown?.Current.Value == "(multiple)";
});
private void dismissPopover()
@@ -920,23 +909,15 @@ namespace osu.Game.Tests.Visual.Editing
private void setBankViaPopover(string bank) => AddStep($"set bank {bank} via popover", () =>
{
var popover = this.ChildrenOfType<SamplePointPiece.SampleEditPopover>().Single();
var textBox = popover.ChildrenOfType<LabelledTextBox>().First();
var textBox = popover.ChildrenOfType<LabelledDropdown<string>>().First();
textBox.Current.Value = bank;
// force a commit via keyboard.
// this is needed when testing attempting to set empty bank - which should revert to the previous value, but only on commit.
((IFocusManager)InputManager).ChangeFocus(textBox);
InputManager.Key(Key.Enter);
});
private void setAdditionBankViaPopover(string bank) => AddStep($"set addition bank {bank} via popover", () =>
{
var popover = this.ChildrenOfType<SamplePointPiece.SampleEditPopover>().Single();
var textBox = popover.ChildrenOfType<LabelledTextBox>().ToArray()[1];
var textBox = popover.ChildrenOfType<LabelledDropdown<string>>().ToArray()[1];
textBox.Current.Value = bank;
// force a commit via keyboard.
// this is needed when testing attempting to set empty bank - which should revert to the previous value, but only on commit.
((IFocusManager)InputManager).ChangeFocus(textBox);
InputManager.Key(Key.Enter);
});
private void toggleAdditionViaPopover(int index) => AddStep($"toggle addition {index} via popover", () =>

View File

@@ -11,7 +11,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{
[Test]
public void TestLabelledDropdown()
=> AddStep(@"create dropdown", () => Child = new LabelledDropdown<string>
=> AddStep(@"create dropdown", () => Child = new LabelledDropdown<string>(true)
{
Label = @"Countdown speed",
Items = new[]
@@ -25,7 +25,7 @@ namespace osu.Game.Tests.Visual.UserInterface
[Test]
public void TestLabelledEnumDropdown()
=> AddStep(@"create dropdown", () => Child = new LabelledEnumDropdown<BeatmapOnlineStatus>
=> AddStep(@"create dropdown", () => Child = new LabelledEnumDropdown<BeatmapOnlineStatus>(true)
{
Label = @"Beatmap status",
Description = @"This is a description"

View File

@@ -116,12 +116,13 @@ namespace osu.Game.Tournament.Screens.Setup
Failing = api.IsLoggedIn != true,
Description = "In order to access the API and display metadata, signing in is required."
},
new LabelledDropdown<RulesetInfo?>
new LabelledDropdown<RulesetInfo?>(padded: true)
{
Label = "Ruleset",
Description = "Decides what stats are displayed and which ranks are retrieved for players. This requires a restart to reload data for an existing bracket.",
Items = rulesets.AvailableRulesets,
Current = LadderInfo.Ruleset,
DropdownWidth = 0.5f,
},
new TournamentSwitcher
{

View File

@@ -3,7 +3,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@@ -72,7 +71,9 @@ namespace osu.Game.Collections
var createdItem = flow.Children.SingleOrDefault(item => item.Model.Value.ID == lastCreated);
if (createdItem != null)
scroll.ScrollTo(createdItem);
{
ScheduleAfterChildren(() => scroll.ScrollIntoView(createdItem));
}
lastCreated = null;
}
@@ -104,13 +105,8 @@ namespace osu.Game.Collections
}
}
protected override OsuRearrangeableListItem<Live<BeatmapCollection>> CreateOsuDrawable(Live<BeatmapCollection> item)
{
if (item.ID == scroll.PlaceholderItem.Model.ID)
return scroll.ReplacePlaceholder();
return new DrawableCollectionListItem(item, true);
}
protected override OsuRearrangeableListItem<Live<BeatmapCollection>> CreateOsuDrawable(Live<BeatmapCollection> item) =>
new DrawableCollectionListItem(item, true);
protected override void Dispose(bool isDisposing)
{
@@ -122,91 +118,22 @@ namespace osu.Game.Collections
/// The scroll container for this <see cref="DrawableCollectionList"/>.
/// Contains the main flow of <see cref="DrawableCollectionListItem"/> and attaches a placeholder item to the end of the list.
/// </summary>
/// <remarks>
/// Use <see cref="ReplacePlaceholder"/> to transfer the placeholder into the main list.
/// </remarks>
private partial class Scroll : OsuScrollContainer
{
/// <summary>
/// The currently-displayed placeholder item.
/// </summary>
public DrawableCollectionListItem PlaceholderItem { get; private set; } = null!;
protected override Container<Drawable> Content => content;
private readonly Container content;
private readonly Container<DrawableCollectionListItem> placeholderContainer;
private readonly FillFlowContainer content;
public Scroll()
{
ScrollbarOverlapsContent = false;
base.Content.Add(new FillFlowContainer
base.Content.Add(content = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
LayoutDuration = 200,
LayoutEasing = Easing.OutQuint,
Children = new Drawable[]
{
content = new Container { RelativeSizeAxes = Axes.X },
placeholderContainer = new Container<DrawableCollectionListItem>
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y
}
}
});
ReplacePlaceholder();
Debug.Assert(PlaceholderItem != null);
}
protected override void Update()
{
base.Update();
// AutoSizeAxes cannot be used as the height should represent the post-layout-transform height at all times, so that the placeholder doesn't bounce around.
content.Height = ((Flow)Child).Children.Sum(c => c.IsPresent ? c.DrawHeight + 5 : 0);
}
/// <summary>
/// Replaces the current <see cref="PlaceholderItem"/> with a new one, and returns the previous.
/// </summary>
/// <returns>The current <see cref="PlaceholderItem"/>.</returns>
public DrawableCollectionListItem ReplacePlaceholder()
{
var previous = PlaceholderItem;
placeholderContainer.Clear(false);
placeholderContainer.Add(PlaceholderItem = new NewCollectionEntryItem());
return previous;
}
}
private partial class NewCollectionEntryItem : DrawableCollectionListItem
{
[Resolved]
private RealmAccess realm { get; set; } = null!;
public NewCollectionEntryItem()
: base(new BeatmapCollection().ToLiveUnmanaged(), false)
{
}
protected override void LoadComplete()
{
base.LoadComplete();
TextBox.OnCommit += (sender, newText) =>
{
if (string.IsNullOrEmpty(TextBox.Text))
return;
realm.Write(r => r.Add(new BeatmapCollection(TextBox.Text)));
TextBox.Text = string.Empty;
};
}
}

View File

@@ -53,7 +53,11 @@ namespace osu.Game.Collections
ShowDragHandle.Value = false;
Masking = true;
CornerRadius = item_height / 2;
// This doesn't match the latest design spec (should be 5) but is an in-between that feels right to the eye
// until we move everything over to Form controls.
CornerRadius = 10;
CornerExponent = 2.5f;
}
protected override Drawable CreateContent() => content = new ItemContent(Model);
@@ -135,7 +139,8 @@ namespace osu.Game.Collections
{
this.collection = collection;
CornerRadius = item_height / 2;
CornerRadius = 10;
CornerExponent = 2.5f;
}
[BackgroundDependencyLoader]

View File

@@ -7,6 +7,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Game.Database;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
@@ -102,12 +103,13 @@ namespace osu.Game.Collections
new Container
{
RelativeSizeAxes = Axes.Both,
Masking = true,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colours.GreySeaFoamDarker
Colour = colours.GreySeaFoamDarker,
},
new Container
{
@@ -115,23 +117,30 @@ namespace osu.Game.Collections
Padding = new MarginPadding(10),
Children = new Drawable[]
{
list = new DrawableCollectionList
{
Padding = new MarginPadding { Vertical = 50 },
RelativeSizeAxes = Axes.Both,
},
searchTextBox = new BasicSearchTextBox
{
RelativeSizeAxes = Axes.X,
Y = 10,
Height = 40,
ReleaseFocusOnCommit = false,
HoldFocus = true,
PlaceholderText = HomeStrings.SearchPlaceholder,
},
list = new DrawableCollectionList
new Container
{
Padding = new MarginPadding
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Children = new Drawable[]
{
Top = 60,
},
RelativeSizeAxes = Axes.Both,
}
new NewCollectionEntryItem()
}
},
}
},
}
@@ -184,5 +193,30 @@ namespace osu.Game.Collections
// Ensure that textboxes commit
GetContainingFocusManager()?.TriggerFocusContention(this);
}
private partial class NewCollectionEntryItem : DrawableCollectionListItem
{
[Resolved]
private RealmAccess realm { get; set; } = null!;
public NewCollectionEntryItem()
: base(new BeatmapCollection().ToLiveUnmanaged(), false)
{
}
protected override void LoadComplete()
{
base.LoadComplete();
TextBox.OnCommit += (_, _) =>
{
if (string.IsNullOrEmpty(TextBox.Text))
return;
realm.Write(r => r.Add(new BeatmapCollection(TextBox.Text)));
TextBox.Text = string.Empty;
};
}
}
}
}

View File

@@ -9,8 +9,8 @@ namespace osu.Game.Graphics.UserInterfaceV2
{
public partial class LabelledDropdown<TItem> : LabelledComponent<OsuDropdown<TItem>, TItem>
{
public LabelledDropdown()
: base(true)
public LabelledDropdown(bool padded)
: base(padded)
{
}
@@ -20,12 +20,22 @@ namespace osu.Game.Graphics.UserInterfaceV2
set => Component.Items = value;
}
public float DropdownWidth
{
get => Component.Width;
set => Component.Width = value;
}
protected sealed override OsuDropdown<TItem> CreateComponent() => CreateDropdown().With(d =>
{
d.RelativeSizeAxes = Axes.X;
d.Width = 0.5f;
});
protected virtual OsuDropdown<TItem> CreateDropdown() => new OsuDropdown<TItem>();
protected virtual OsuDropdown<TItem> CreateDropdown() => new Dropdown();
private partial class Dropdown : OsuDropdown<TItem>
{
protected override DropdownMenu CreateMenu() => base.CreateMenu().With(menu => menu.MaxHeight = 200);
}
}
}

View File

@@ -9,6 +9,11 @@ namespace osu.Game.Graphics.UserInterfaceV2
public partial class LabelledEnumDropdown<TEnum> : LabelledDropdown<TEnum>
where TEnum : struct, Enum
{
public LabelledEnumDropdown(bool padded)
: base(padded)
{
}
protected override OsuDropdown<TEnum> CreateDropdown() => new OsuEnumDropdown<TEnum>();
}
}

View File

@@ -63,7 +63,6 @@ using osu.Game.Resources;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
using osu.Game.Screens;
using osu.Game.Skinning;
using osu.Game.Utils;
using RuntimeInfo = osu.Framework.RuntimeInfo;

View File

@@ -32,7 +32,7 @@ namespace osu.Game.Overlays
Origin = Anchor.BottomCentre,
Font = OsuFont.Numeric.With(weight: FontWeight.Bold, size: 12),
Colour = colours.YellowDark,
Text = @"DEVELOPER BUILD For Ez2Lazer",
Text = @"DEVELOPER BUILD",
},
new Sprite
{

View File

@@ -35,7 +35,6 @@ using osu.Game.Screens.Edit.Components.Menus;
using osu.Game.Skinning;
using osu.Framework.Graphics.Cursor;
using osu.Game.Input.Bindings;
using osu.Game.LAsEzExtensions.Configuration;
using osu.Game.LAsEzExtensions.Screens;
using osu.Game.Utils;

View File

@@ -129,8 +129,11 @@ namespace osu.Game.Rulesets.Edit
// Inherit the bank from the previous hit object
HitObject.Samples = HitObject.Samples.Select(s => s.Name == HitSampleInfo.HIT_NORMAL ? s.With(newBank: lastHitNormal.Bank) : s).ToList();
// Inherit the volume from the previous hit object
HitObject.Samples = HitObject.Samples.Select(s => s.With(newVolume: lastHitNormal.Volume)).ToList();
// Inherit the volume and sample set info from the previous hit object
HitObject.Samples = HitObject.Samples.Select(s => s.With(
newVolume: lastHitNormal.Volume,
newSuffix: lastHitNormal.Suffix,
newUseBeatmapSamples: lastHitNormal.UseBeatmapSamples)).ToList();
}
if (HitObject is IHasRepeats hasRepeats)

View File

@@ -49,7 +49,8 @@ namespace osu.Game.Screens.Edit.Components.TernaryButtons
public Drawable Icon { get; private set; } = null!;
public DrawableTernaryButton()
public DrawableTernaryButton(HoverSampleSet? hoverSampleSet = HoverSampleSet.Button)
: base(hoverSampleSet)
{
RelativeSizeAxes = Axes.X;
}

View File

@@ -0,0 +1,58 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Screens.Edit.Components.TernaryButtons
{
public partial class SampleSetTernaryButton : DrawableTernaryButton
{
public EditorBeatmapSkin.SampleSet SampleSet { get; }
public SampleSetTernaryButton(EditorBeatmapSkin.SampleSet sampleSet)
: base(null)
{
SampleSet = sampleSet;
CreateIcon = () => sampleSet.SampleSetIndex == 0
? new SpriteIcon { Icon = OsuIcon.SkinA }
: new Container
{
Child = new OsuSpriteText
{
Text = sampleSet.SampleSetIndex.ToString(),
Font = OsuFont.Style.Body.With(weight: FontWeight.Bold),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
};
switch (sampleSet.SampleSetIndex)
{
case 0:
RelativeSizeAxes = Axes.X;
Width = 1;
break;
default:
RelativeSizeAxes = Axes.None;
Width = Height;
break;
}
}
[BackgroundDependencyLoader]
private void load()
{
AddRangeInternal(new Drawable[]
{
new HoverSounds(HoverSampleSet.Button),
});
}
}
}

View File

@@ -36,13 +36,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
private double? lastSeekTime;
protected override bool OnDragStart(DragStartEvent e)
{
var localPos = ToLocalSpace(e.ScreenSpaceMousePosition);
if (localPos.Y <= DrawHeight / 2) return false;
return true;
}
protected override bool OnDragStart(DragStartEvent e) => true;
protected override void OnDrag(DragEvent e)
{
@@ -58,9 +52,6 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
protected override bool OnMouseDown(MouseDownEvent e)
{
var localPos = ToLocalSpace(e.ScreenSpaceMousePosition);
if (localPos.Y <= DrawHeight / 2) return false; // only lower half
seekToPosition(e.ScreenSpaceMousePosition, instant: true);
return true;
}

View File

@@ -20,10 +20,11 @@ using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Rulesets.Objects;
using osu.Game.Screens.Edit.Components.TernaryButtons;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Screens.Edit.Components.TernaryButtons;
using osu.Game.Screens.Edit.Timing;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
using osuTK.Input;
@@ -127,7 +128,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private void updateText()
{
Label.Text = $"{abbreviateBank(GetBankValue(GetSamples()))} {GetVolumeValue(GetSamples())}";
Label.Text = $"{abbreviateBank(GetBankValue(GetSamples()))}{GetSuffix(GetSamples())} {GetVolumeValue(GetSamples())}";
if (!contracted.Value)
LabelContainer.ResizeWidthTo(Label.Width, 200, Easing.OutQuint);
@@ -149,6 +150,17 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
return samples.FirstOrDefault(o => o.Name == HitSampleInfo.HIT_NORMAL)?.Bank;
}
public static string GetSuffix(IEnumerable<HitSampleInfo> samples)
{
var suffixes = samples.Select(o => o.Suffix).Distinct().ToList();
// having multiple values should never happen, but just for safety...
if (suffixes.Count != 1 || suffixes.Single() is not string commonSuffix)
return string.Empty;
return $@":{commonSuffix}";
}
public static string? GetAdditionBankValue(IEnumerable<HitSampleInfo> samples)
{
var firstAddition = samples.FirstOrDefault(o => o.Name != HitSampleInfo.HIT_NORMAL);
@@ -176,9 +188,12 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
private readonly HitObject hitObject;
private LabelledTextBox bank = null!;
private LabelledTextBox additionBank = null!;
private LabelledDropdown<string> bank = null!;
private LabelledDropdown<string> additionBank = null!;
private FillFlowContainer<SampleSetTernaryButton>? sampleSetsFlow;
private LabelledDropdown<EditorBeatmapSkin.SampleSet>? sampleSetDropdown;
private IndeterminateSliderWithTextBoxInput<int> volume = null!;
private SkinnableSound demoSample = null!;
private FillFlowContainer togglesCollection = null!;
@@ -229,11 +244,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
flow = new FillFlowContainer
{
Width = 200,
Width = 220,
Direction = FillDirection.Vertical,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(0, 10),
Children = new Drawable[]
Children = new[]
{
togglesCollection = new FillFlowContainer
{
@@ -242,27 +257,30 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
Direction = FillDirection.Horizontal,
Spacing = new Vector2(5, 5),
},
bank = new LabelledTextBox
bank = new LabelledDropdown<string>(padded: false)
{
Label = "Bank Name",
SelectAllOnFocus = true,
Label = "Normal Bank",
Items = HitSampleInfo.ALL_BANKS,
},
additionBank = new LabelledTextBox
additionBank = new LabelledDropdown<string>(padded: false)
{
Label = "Addition Bank",
SelectAllOnFocus = true,
Items = HitSampleInfo.ALL_BANKS,
},
createSampleSetContent(),
volume = new IndeterminateSliderWithTextBoxInput<int>("Volume", new BindableInt(100)
{
MinValue = DrawableHitObject.MINIMUM_SAMPLE_VOLUME,
MaxValue = 100,
})
}
},
new EditorSkinProvidingContainer(beatmap)
{
Child = demoSample = new SkinnableSound()
}
};
bank.TabbableContentContainer = flow;
additionBank.TabbableContentContainer = flow;
volume.TabbableContentContainer = flow;
// if the piece belongs to a currently selected object, assume that the user wants to change all selected objects.
@@ -283,10 +301,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
setBank(val.NewValue);
updatePrimaryBankState();
playDemoSample();
});
// on commit, ensure that the value is correct by sourcing it from the objects' samples again.
// this ensures that committing empty text causes a revert to the previous value.
bank.OnCommit += (_, _) => updatePrimaryBankState();
updateAdditionBankState();
additionBank.Current.BindValueChanged(val =>
@@ -296,8 +312,10 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
setAdditionBank(val.NewValue);
updateAdditionBankState();
playDemoSample();
});
additionBank.OnCommit += (_, _) => updateAdditionBankState();
updateSampleSetState();
volume.Current.BindValueChanged(val =>
{
@@ -310,6 +328,58 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
togglesCollection.AddRange(createTernaryButtons());
}
private Drawable createSampleSetContent()
{
if (beatmap.BeatmapSkin == null)
return Empty();
var sampleSets = beatmap.BeatmapSkin.GetAvailableSampleSets().ToList();
if (sampleSets.Count == 0)
return Empty();
sampleSets.Insert(0, new EditorBeatmapSkin.SampleSet(0, "User skin"));
if (sampleSets.Count < 20)
{
sampleSetsFlow = new FillFlowContainer<SampleSetTernaryButton>
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(5),
ChildrenEnumerable = sampleSets.Select(set => new SampleSetTernaryButton(set) { Description = set.Name }),
};
foreach (var ternary in sampleSetsFlow)
{
ternary.Current.BindValueChanged(val =>
{
if (val.NewValue == TernaryState.True)
setSampleSet(ternary.SampleSet);
updateSampleSetState();
playDemoSample();
});
}
return sampleSetsFlow;
}
sampleSetDropdown = new LabelledDropdown<EditorBeatmapSkin.SampleSet>(padded: false)
{
Label = "Sample Set",
Items = sampleSets,
};
sampleSetDropdown.Current.BindValueChanged(val =>
{
setSampleSet(val.NewValue);
updateSampleSetState();
playDemoSample();
});
return sampleSetDropdown;
}
private string? getCommonBank() => allRelevantSamples.Select(h => GetBankValue(h.samples)).Distinct().Count() == 1
? GetBankValue(allRelevantSamples.First().samples)
: null;
@@ -327,15 +397,13 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private void updatePrimaryBankState()
{
string? commonBank = getCommonBank();
bank.Current.Value = commonBank;
bank.PlaceholderText = string.IsNullOrEmpty(commonBank) ? "(multiple)" : string.Empty;
bank.Current.Value = !string.IsNullOrEmpty(commonBank) ? commonBank : "(multiple)";
}
private void updateAdditionBankState()
{
string? commonAdditionBank = getCommonAdditionBank();
additionBank.PlaceholderText = string.IsNullOrEmpty(commonAdditionBank) ? "(multiple)" : string.Empty;
additionBank.Current.Value = commonAdditionBank;
additionBank.Current.Value = !string.IsNullOrEmpty(commonAdditionBank) ? commonAdditionBank : "(multiple)";
bool anyAdditions = allRelevantSamples.Any(o => o.samples.Any(s => s.Name != HitSampleInfo.HIT_NORMAL));
if (anyAdditions)
@@ -344,6 +412,40 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
additionBank.Hide();
}
private void updateSampleSetState()
{
HashSet<int> activeSets = new HashSet<int>();
foreach (var sample in allRelevantSamples.SelectMany(h => h.samples))
{
if (sample.Suffix == null)
activeSets.Add(sample.UseBeatmapSamples ? 1 : 0);
else if (int.TryParse(sample.Suffix, out int suffix))
activeSets.Add(suffix);
}
if (sampleSetsFlow != null)
{
var onState = activeSets.Count > 1 ? TernaryState.Indeterminate : TernaryState.True;
foreach (var ternary in sampleSetsFlow)
ternary.Current.Value = activeSets.Contains(ternary.SampleSet.SampleSetIndex) ? onState : TernaryState.False;
}
if (sampleSetDropdown != null)
{
sampleSetDropdown.Current.Value = activeSets.Count == 1
? sampleSetDropdown.Items.Single(i => i.SampleSetIndex == activeSets.Single())
: new EditorBeatmapSkin.SampleSet(-1, "(multiple)");
}
}
private void playDemoSample() => Scheduler.AddOnce(() =>
{
demoSample.Samples = allRelevantSamples.First().samples.Cast<ISampleInfo>().ToArray();
demoSample.Play();
});
/// <summary>
/// Applies the given update action on all samples of <see cref="allRelevantSamples"/>
/// and invokes the necessary update notifiers for the beatmap and hit objects.
@@ -397,6 +499,19 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
});
}
private void setSampleSet(EditorBeatmapSkin.SampleSet newSampleSet)
{
updateAllRelevantSamples((_, relevantSamples) =>
{
for (int i = 0; i < relevantSamples.Count; i++)
{
relevantSamples[i] = relevantSamples[i].With(
newSuffix: newSampleSet.SampleSetIndex >= 2 ? newSampleSet.SampleSetIndex.ToString() : null,
newUseBeatmapSamples: newSampleSet.SampleSetIndex >= 1);
}
});
}
private void setVolume(int newVolume)
{
updateAllRelevantSamples((_, relevantSamples) =>
@@ -435,6 +550,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
addHitSample(sampleName);
break;
}
playDemoSample();
};
selectionSampleStates[sampleName] = bindable;
@@ -455,7 +572,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
foreach ((string sampleName, var bindable) in selectionSampleStates)
{
yield return new DrawableTernaryButton
yield return new DrawableTernaryButton(null)
{
Current = bindable,
Description = string.Empty,

View File

@@ -3,6 +3,8 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@@ -61,6 +63,46 @@ namespace osu.Game.Screens.Edit
invokeSkinChanged();
}
public record SampleSet(int SampleSetIndex, string Name)
{
public SampleSet(int sampleSetIndex)
: this(sampleSetIndex, $@"Custom #{sampleSetIndex}")
{
}
public override string ToString() => Name;
}
public IEnumerable<SampleSet> GetAvailableSampleSets()
{
string[] possibleSounds = HitSampleInfo.ALL_ADDITIONS.Prepend(HitSampleInfo.HIT_NORMAL).ToArray();
string[] possibleBanks = HitSampleInfo.ALL_BANKS;
string[] possiblePrefixes = possibleSounds.SelectMany(sound => possibleBanks.Select(bank => $@"{bank}-{sound}")).ToArray();
HashSet<int> indices = new HashSet<int>();
if (Skin.Samples != null)
{
foreach (string sample in Skin.Samples.GetAvailableResources())
{
foreach (string possiblePrefix in possiblePrefixes)
{
if (!sample.StartsWith(possiblePrefix, StringComparison.InvariantCultureIgnoreCase))
continue;
string indexString = Path.GetFileNameWithoutExtension(sample)[possiblePrefix.Length..];
if (string.IsNullOrEmpty(indexString))
indices.Add(1);
if (int.TryParse(indexString, out int index))
indices.Add(index);
}
}
}
return indices.OrderBy(i => i).Select(i => new SampleSet(i));
}
#region Delegated ISkin implementation
public Drawable? GetDrawableComponent(ISkinComponentLookup lookup) => Skin.GetDrawableComponent(lookup);

View File

@@ -38,7 +38,7 @@ namespace osu.Game.Skinning
/// <summary>
/// A sample store which can be used to perform user file lookups for this skin.
/// </summary>
protected ISampleStore? Samples { get; }
protected internal ISampleStore? Samples { get; }
public readonly Live<SkinInfo> SkinInfo;