diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 6ec071be2f..17a371079a 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -21,7 +21,7 @@ ] }, "ppy.localisationanalyser.tools": { - "version": "2024.802.0", + "version": "2025.1208.0", "commands": [ "localisation" ] diff --git a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs index 765fe1ecf6..082064d198 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs @@ -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().SingleOrDefault(); - var textBox = popover?.ChildrenOfType().First(); + var dropdown = popover?.ChildrenOfType>().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().SingleOrDefault(); - var textBox = popover?.ChildrenOfType().First(); + var dropdown = popover?.ChildrenOfType>().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().Single(); - var textBox = popover.ChildrenOfType().First(); + var textBox = popover.ChildrenOfType>().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().Single(); - var textBox = popover.ChildrenOfType().ToArray()[1]; + var textBox = popover.ChildrenOfType>().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", () => diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledDropdown.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledDropdown.cs index 300b451cf5..941f6667e2 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledDropdown.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledDropdown.cs @@ -11,7 +11,7 @@ namespace osu.Game.Tests.Visual.UserInterface { [Test] public void TestLabelledDropdown() - => AddStep(@"create dropdown", () => Child = new LabelledDropdown + => AddStep(@"create dropdown", () => Child = new LabelledDropdown(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 + => AddStep(@"create dropdown", () => Child = new LabelledEnumDropdown(true) { Label = @"Beatmap status", Description = @"This is a description" diff --git a/osu.Game.Tournament/Screens/Setup/SetupScreen.cs b/osu.Game.Tournament/Screens/Setup/SetupScreen.cs index 536e8ba767..bd368d88a0 100644 --- a/osu.Game.Tournament/Screens/Setup/SetupScreen.cs +++ b/osu.Game.Tournament/Screens/Setup/SetupScreen.cs @@ -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 + new LabelledDropdown(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 { diff --git a/osu.Game/Collections/DrawableCollectionList.cs b/osu.Game/Collections/DrawableCollectionList.cs index c494b830d1..5c79549c0a 100644 --- a/osu.Game/Collections/DrawableCollectionList.cs +++ b/osu.Game/Collections/DrawableCollectionList.cs @@ -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> CreateOsuDrawable(Live item) - { - if (item.ID == scroll.PlaceholderItem.Model.ID) - return scroll.ReplacePlaceholder(); - - return new DrawableCollectionListItem(item, true); - } + protected override OsuRearrangeableListItem> CreateOsuDrawable(Live item) => + new DrawableCollectionListItem(item, true); protected override void Dispose(bool isDisposing) { @@ -122,91 +118,22 @@ namespace osu.Game.Collections /// The scroll container for this . /// Contains the main flow of and attaches a placeholder item to the end of the list. /// - /// - /// Use to transfer the placeholder into the main list. - /// private partial class Scroll : OsuScrollContainer { - /// - /// The currently-displayed placeholder item. - /// - public DrawableCollectionListItem PlaceholderItem { get; private set; } = null!; - protected override Container Content => content; - private readonly Container content; - - private readonly Container 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 - { - 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); - } - - /// - /// Replaces the current with a new one, and returns the previous. - /// - /// The current . - 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; - }; } } diff --git a/osu.Game/Collections/DrawableCollectionListItem.cs b/osu.Game/Collections/DrawableCollectionListItem.cs index 3031112333..5df746459c 100644 --- a/osu.Game/Collections/DrawableCollectionListItem.cs +++ b/osu.Game/Collections/DrawableCollectionListItem.cs @@ -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] diff --git a/osu.Game/Collections/ManageCollectionsDialog.cs b/osu.Game/Collections/ManageCollectionsDialog.cs index 79166840f9..958e169542 100644 --- a/osu.Game/Collections/ManageCollectionsDialog.cs +++ b/osu.Game/Collections/ManageCollectionsDialog.cs @@ -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; + }; + } + } } } diff --git a/osu.Game/Graphics/UserInterfaceV2/LabelledDropdown.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledDropdown.cs index dbbae390a7..5dd9db7506 100644 --- a/osu.Game/Graphics/UserInterfaceV2/LabelledDropdown.cs +++ b/osu.Game/Graphics/UserInterfaceV2/LabelledDropdown.cs @@ -9,8 +9,8 @@ namespace osu.Game.Graphics.UserInterfaceV2 { public partial class LabelledDropdown : LabelledComponent, 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 CreateComponent() => CreateDropdown().With(d => { d.RelativeSizeAxes = Axes.X; - d.Width = 0.5f; }); - protected virtual OsuDropdown CreateDropdown() => new OsuDropdown(); + protected virtual OsuDropdown CreateDropdown() => new Dropdown(); + + private partial class Dropdown : OsuDropdown + { + protected override DropdownMenu CreateMenu() => base.CreateMenu().With(menu => menu.MaxHeight = 200); + } } } diff --git a/osu.Game/Graphics/UserInterfaceV2/LabelledEnumDropdown.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledEnumDropdown.cs index 9c2c8397ed..5658175804 100644 --- a/osu.Game/Graphics/UserInterfaceV2/LabelledEnumDropdown.cs +++ b/osu.Game/Graphics/UserInterfaceV2/LabelledEnumDropdown.cs @@ -9,6 +9,11 @@ namespace osu.Game.Graphics.UserInterfaceV2 public partial class LabelledEnumDropdown : LabelledDropdown where TEnum : struct, Enum { + public LabelledEnumDropdown(bool padded) + : base(padded) + { + } + protected override OsuDropdown CreateDropdown() => new OsuEnumDropdown(); } } diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 0b1b69ed88..1ccd92769c 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -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; diff --git a/osu.Game/Overlays/DevBuildBanner.cs b/osu.Game/Overlays/DevBuildBanner.cs index 81fe84e94d..f514483e76 100644 --- a/osu.Game/Overlays/DevBuildBanner.cs +++ b/osu.Game/Overlays/DevBuildBanner.cs @@ -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 { diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index ec104b8874..dfce2e0e72 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -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; diff --git a/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs b/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs index ea5f69be7b..4688c7c8ca 100644 --- a/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs @@ -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) diff --git a/osu.Game/Screens/Edit/Components/TernaryButtons/DrawableTernaryButton.cs b/osu.Game/Screens/Edit/Components/TernaryButtons/DrawableTernaryButton.cs index 7b36b5f957..f9c6ee6b43 100644 --- a/osu.Game/Screens/Edit/Components/TernaryButtons/DrawableTernaryButton.cs +++ b/osu.Game/Screens/Edit/Components/TernaryButtons/DrawableTernaryButton.cs @@ -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; } diff --git a/osu.Game/Screens/Edit/Components/TernaryButtons/SampleSetTernaryButton.cs b/osu.Game/Screens/Edit/Components/TernaryButtons/SampleSetTernaryButton.cs new file mode 100644 index 0000000000..96227ec53e --- /dev/null +++ b/osu.Game/Screens/Edit/Components/TernaryButtons/SampleSetTernaryButton.cs @@ -0,0 +1,58 @@ +// Copyright (c) ppy Pty Ltd . 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), + }); + } + } +} diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs index 4a43c5a31c..1783bfb1e5 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs @@ -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; } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index cdd2f52dab..7f7f749c44 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -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 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 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 bank = null!; + private LabelledDropdown additionBank = null!; + private FillFlowContainer? sampleSetsFlow; + private LabelledDropdown? sampleSetDropdown; private IndeterminateSliderWithTextBoxInput 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(padded: false) { - Label = "Bank Name", - SelectAllOnFocus = true, + Label = "Normal Bank", + Items = HitSampleInfo.ALL_BANKS, }, - additionBank = new LabelledTextBox + additionBank = new LabelledDropdown(padded: false) { Label = "Addition Bank", - SelectAllOnFocus = true, + Items = HitSampleInfo.ALL_BANKS, }, + createSampleSetContent(), volume = new IndeterminateSliderWithTextBoxInput("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 + { + 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(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 activeSets = new HashSet(); + + 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().ToArray(); + demoSample.Play(); + }); + /// /// Applies the given update action on all samples of /// 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, diff --git a/osu.Game/Screens/Edit/EditorBeatmapSkin.cs b/osu.Game/Screens/Edit/EditorBeatmapSkin.cs index 07fa1cb49c..ab1f5e4ad1 100644 --- a/osu.Game/Screens/Edit/EditorBeatmapSkin.cs +++ b/osu.Game/Screens/Edit/EditorBeatmapSkin.cs @@ -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 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 indices = new HashSet(); + + 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); diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index fe0ce5afbc..3a0d0b16e5 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -38,7 +38,7 @@ namespace osu.Game.Skinning /// /// A sample store which can be used to perform user file lookups for this skin. /// - protected ISampleStore? Samples { get; } + protected internal ISampleStore? Samples { get; } public readonly Live SkinInfo;