From 5c8ae6f851b681ff06dc1e778ac48c73b4092ddd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 9 Jan 2025 13:04:13 +0100 Subject: [PATCH] Simplify editor "ternary button" structure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As I look into re-implementing the ability to choose combo colour for an object (also known as "colourhax") from the editor UI, I stumble upon these wretched ternary items again and sigh a deep sigh of annoyance. The structure is overly rigid. `TernaryItem` does nothing that `DrawableTernaryItem` couldn't, except make it more annoying to add specific sub-variants of `DrawableTernaryItem` that could do more things. Yes you could sprinkle more levels of virtuals to `CreateDrawableButton()` or something, but after all, as Saint Exupéry says, "perfection is finally attained not when there is no longer anything to add, but when there is no longer anything to take away." So I'm leaning for taking one step towards perfection. --- .../Edit/CatchHitObjectComposer.cs | 2 +- .../Edit/OsuHitObjectComposer.cs | 9 ++- .../Edit/ComposerDistanceSnapProvider.cs | 9 ++- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 14 ++--- .../Edit/ScrollingHitObjectComposer.cs | 7 ++- .../TernaryButtons/DrawableTernaryButton.cs | 62 ++++++++++++++----- .../TernaryButtons/SampleBankTernaryButton.cs | 38 ++++++++---- .../TernaryButtons/TernaryButton.cs | 48 -------------- .../Components/ComposeBlueprintContainer.cs | 58 ++++++++++------- .../Components/Timeline/SamplePointPiece.cs | 17 +++-- 10 files changed, 147 insertions(+), 117 deletions(-) delete mode 100644 osu.Game/Screens/Edit/Components/TernaryButtons/TernaryButton.cs diff --git a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs index aae3369d40..e0d80e0e64 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs @@ -72,7 +72,7 @@ namespace osu.Game.Rulesets.Catch.Edit protected override Drawable CreateHitObjectInspector() => new CatchHitObjectInspector(DistanceSnapProvider); - protected override IEnumerable CreateTernaryButtons() + protected override IEnumerable CreateTernaryButtons() => base.CreateTernaryButtons() .Concat(DistanceSnapProvider.CreateTernaryButtons()); diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 7c50558b92..e8b9d0544e 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -53,9 +53,14 @@ namespace osu.Game.Rulesets.Osu.Edit protected override Drawable CreateHitObjectInspector() => new OsuHitObjectInspector(); - protected override IEnumerable CreateTernaryButtons() + protected override IEnumerable CreateTernaryButtons() => base.CreateTernaryButtons() - .Append(new TernaryButton(rectangularGridSnapToggle, "Grid Snap", () => new SpriteIcon { Icon = OsuIcon.EditorGridSnap })) + .Append(new DrawableTernaryButton + { + Current = rectangularGridSnapToggle, + Description = "Grid Snap", + CreateIcon = () => new SpriteIcon { Icon = OsuIcon.EditorGridSnap }, + }) .Concat(DistanceSnapProvider.CreateTernaryButtons()); private BindableList selectedHitObjects; diff --git a/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs b/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs index 7337a75509..0ca01ccee6 100644 --- a/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs +++ b/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs @@ -191,9 +191,14 @@ namespace osu.Game.Rulesets.Edit } } - public IEnumerable CreateTernaryButtons() => new[] + public IEnumerable CreateTernaryButtons() => new[] { - new TernaryButton(DistanceSnapToggle, "Distance Snap", () => new SpriteIcon { Icon = OsuIcon.EditorDistanceSnap }) + new DrawableTernaryButton + { + Current = DistanceSnapToggle, + Description = "Distance Snap", + CreateIcon = () => new SpriteIcon { Icon = OsuIcon.EditorDistanceSnap }, + } }; public void HandleToggleViaKey(KeyboardEvent key) diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 4b64548f9c..9f277b6190 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -269,10 +269,9 @@ namespace osu.Game.Rulesets.Edit }; } - TernaryStates = CreateTernaryButtons().ToArray(); - togglesCollection.AddRange(TernaryStates.Select(b => new DrawableTernaryButton(b))); + togglesCollection.AddRange(CreateTernaryButtons().ToArray()); - sampleBankTogglesCollection.AddRange(BlueprintContainer.SampleBankTernaryStates.Zip(BlueprintContainer.SampleAdditionBankTernaryStates).Select(b => new SampleBankTernaryButton(b.First, b.Second))); + sampleBankTogglesCollection.AddRange(BlueprintContainer.SampleBankTernaryStates); SetSelectTool(); @@ -368,15 +367,10 @@ namespace osu.Game.Rulesets.Edit /// protected abstract IReadOnlyList CompositionTools { get; } - /// - /// A collection of states which will be displayed to the user in the toolbox. - /// - public TernaryButton[] TernaryStates { get; private set; } - /// /// Create all ternary states required to be displayed to the user. /// - protected virtual IEnumerable CreateTernaryButtons() => BlueprintContainer.MainTernaryStates; + protected virtual IEnumerable CreateTernaryButtons() => BlueprintContainer.MainTernaryStates; /// /// Construct a relevant blueprint container. This will manage hitobject selection/placement input handling and display logic. @@ -437,7 +431,7 @@ namespace osu.Game.Rulesets.Edit { if (togglesCollection.ElementAtOrDefault(rightIndex) is DrawableTernaryButton button) { - button.Button.Toggle(); + button.Toggle(); return true; } } diff --git a/osu.Game/Rulesets/Edit/ScrollingHitObjectComposer.cs b/osu.Game/Rulesets/Edit/ScrollingHitObjectComposer.cs index 223b770b48..e7161ce36c 100644 --- a/osu.Game/Rulesets/Edit/ScrollingHitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/ScrollingHitObjectComposer.cs @@ -56,7 +56,12 @@ namespace osu.Game.Rulesets.Edit Spacing = new Vector2(0, 5), Children = new[] { - new DrawableTernaryButton(new TernaryButton(showSpeedChanges, "Show speed changes", () => new SpriteIcon { Icon = FontAwesome.Solid.TachometerAlt })) + new DrawableTernaryButton + { + Current = showSpeedChanges, + Description = "Show speed changes", + CreateIcon = () => new SpriteIcon { Icon = FontAwesome.Solid.TachometerAlt }, + } } }, }); diff --git a/osu.Game/Screens/Edit/Components/TernaryButtons/DrawableTernaryButton.cs b/osu.Game/Screens/Edit/Components/TernaryButtons/DrawableTernaryButton.cs index fcbc719f46..326fdbc731 100644 --- a/osu.Game/Screens/Edit/Components/TernaryButtons/DrawableTernaryButton.cs +++ b/osu.Game/Screens/Edit/Components/TernaryButtons/DrawableTernaryButton.cs @@ -1,12 +1,15 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; using osu.Framework.Localisation; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; @@ -16,8 +19,29 @@ using osuTK.Graphics; namespace osu.Game.Screens.Edit.Components.TernaryButtons { - public partial class DrawableTernaryButton : OsuButton, IHasTooltip + public partial class DrawableTernaryButton : OsuButton, IHasTooltip, IHasCurrentValue { + public Bindable Current + { + get => current.Current; + set => current.Current = value; + } + + private readonly BindableWithCurrent current = new BindableWithCurrent(); + + public required LocalisableString Description + { + get => Text; + set => Text = value; + } + + public LocalisableString TooltipText { get; set; } + + /// + /// A function which creates a drawable icon to represent this item. If null, a sane default should be used. + /// + public Func? CreateIcon { get; init; } + private Color4 defaultBackgroundColour; private Color4 defaultIconColour; private Color4 selectedBackgroundColour; @@ -25,14 +49,8 @@ namespace osu.Game.Screens.Edit.Components.TernaryButtons protected Drawable Icon { get; private set; } = null!; - public readonly TernaryButton Button; - - public DrawableTernaryButton(TernaryButton button) + public DrawableTernaryButton() { - Button = button; - - Text = button.Description; - RelativeSizeAxes = Axes.X; } @@ -45,7 +63,7 @@ namespace osu.Game.Screens.Edit.Components.TernaryButtons defaultIconColour = defaultBackgroundColour.Darken(0.5f); selectedIconColour = selectedBackgroundColour.Lighten(0.5f); - Add(Icon = (Button.CreateIcon?.Invoke() ?? new Circle()).With(b => + Add(Icon = (CreateIcon?.Invoke() ?? new Circle()).With(b => { b.Blending = BlendingParameters.Additive; b.Anchor = Anchor.CentreLeft; @@ -59,18 +77,32 @@ namespace osu.Game.Screens.Edit.Components.TernaryButtons { base.LoadComplete(); - Button.Bindable.BindValueChanged(_ => updateSelectionState(), true); - Button.Enabled.BindTo(Enabled); + current.BindValueChanged(_ => updateSelectionState(), true); Action = onAction; } private void onAction() { - if (!Button.Enabled.Value) + if (!Enabled.Value) return; - Button.Toggle(); + Toggle(); + } + + public void Toggle() + { + switch (Current.Value) + { + case TernaryState.False: + case TernaryState.Indeterminate: + Current.Value = TernaryState.True; + break; + + case TernaryState.True: + Current.Value = TernaryState.False; + break; + } } private void updateSelectionState() @@ -78,7 +110,7 @@ namespace osu.Game.Screens.Edit.Components.TernaryButtons if (!IsLoaded) return; - switch (Button.Bindable.Value) + switch (Current.Value) { case TernaryState.Indeterminate: Icon.Colour = selectedIconColour.Darken(0.5f); @@ -104,7 +136,5 @@ namespace osu.Game.Screens.Edit.Components.TernaryButtons Anchor = Anchor.CentreLeft, X = 40f }; - - public LocalisableString TooltipText => Button.Tooltip; } } diff --git a/osu.Game/Screens/Edit/Components/TernaryButtons/SampleBankTernaryButton.cs b/osu.Game/Screens/Edit/Components/TernaryButtons/SampleBankTernaryButton.cs index 33eb2ac0b4..a9aa4b4227 100644 --- a/osu.Game/Screens/Edit/Components/TernaryButtons/SampleBankTernaryButton.cs +++ b/osu.Game/Screens/Edit/Components/TernaryButtons/SampleBankTernaryButton.cs @@ -1,23 +1,32 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using Humanizer; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Edit; namespace osu.Game.Screens.Edit.Components.TernaryButtons { public partial class SampleBankTernaryButton : CompositeDrawable { - public readonly TernaryButton NormalButton; - public readonly TernaryButton AdditionsButton; + public string BankName { get; } + public Func? CreateIcon { get; init; } - public SampleBankTernaryButton(TernaryButton normalButton, TernaryButton additionsButton) + public readonly BindableWithCurrent NormalState = new BindableWithCurrent(); + public readonly BindableWithCurrent AdditionsState = new BindableWithCurrent(); + + public DrawableTernaryButton NormalButton { get; private set; } = null!; + public DrawableTernaryButton AdditionsButton { get; private set; } = null!; + + public SampleBankTernaryButton(string bankName) { - NormalButton = normalButton; - AdditionsButton = additionsButton; + BankName = bankName; } [BackgroundDependencyLoader] @@ -36,7 +45,12 @@ namespace osu.Game.Screens.Edit.Components.TernaryButtons AutoSizeAxes = Axes.Y, Width = 0.5f, Padding = new MarginPadding { Right = 1 }, - Child = new InlineDrawableTernaryButton(NormalButton), + Child = NormalButton = new InlineDrawableTernaryButton + { + Current = NormalState, + Description = BankName.Titleize(), + CreateIcon = CreateIcon, + }, }, new Container { @@ -46,18 +60,18 @@ namespace osu.Game.Screens.Edit.Components.TernaryButtons AutoSizeAxes = Axes.Y, Width = 0.5f, Padding = new MarginPadding { Left = 1 }, - Child = new InlineDrawableTernaryButton(AdditionsButton), + Child = AdditionsButton = new InlineDrawableTernaryButton + { + Current = AdditionsState, + Description = BankName.Titleize(), + CreateIcon = CreateIcon, + }, }, }; } private partial class InlineDrawableTernaryButton : DrawableTernaryButton { - public InlineDrawableTernaryButton(TernaryButton button) - : base(button) - { - } - [BackgroundDependencyLoader] private void load() { diff --git a/osu.Game/Screens/Edit/Components/TernaryButtons/TernaryButton.cs b/osu.Game/Screens/Edit/Components/TernaryButtons/TernaryButton.cs deleted file mode 100644 index b7aaf517f5..0000000000 --- a/osu.Game/Screens/Edit/Components/TernaryButtons/TernaryButton.cs +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Game.Graphics.UserInterface; - -namespace osu.Game.Screens.Edit.Components.TernaryButtons -{ - public class TernaryButton - { - public readonly Bindable Bindable; - - public readonly Bindable Enabled = new Bindable(true); - - public readonly string Description; - - /// - /// A function which creates a drawable icon to represent this item. If null, a sane default should be used. - /// - public readonly Func? CreateIcon; - - public string Tooltip { get; set; } = string.Empty; - - public TernaryButton(Bindable bindable, string description, Func? createIcon = null) - { - Bindable = bindable; - Description = description; - CreateIcon = createIcon; - } - - public void Toggle() - { - switch (Bindable.Value) - { - case TernaryState.False: - case TernaryState.Indeterminate: - Bindable.Value = TernaryState.True; - break; - - case TernaryState.True: - Bindable.Value = TernaryState.False; - break; - } - } - } -} diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index 0ffd1072cd..bbb4095206 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -65,11 +65,7 @@ namespace osu.Game.Screens.Edit.Compose.Components private void load() { MainTernaryStates = CreateTernaryButtons().ToArray(); - SampleBankTernaryStates = createSampleBankTernaryButtons(SelectionHandler.SelectionBankStates).ToArray(); - SampleAdditionBankTernaryStates = createSampleBankTernaryButtons(SelectionHandler.SelectionAdditionBankStates).ToArray(); - - SelectionHandler.AutoSelectionBankEnabled.BindValueChanged(_ => updateAutoBankTernaryButtonTooltip(), true); - SelectionHandler.SelectionAdditionBanksEnabled.BindValueChanged(_ => updateAdditionBankTernaryButtonTooltips(), true); + SampleBankTernaryStates = createSampleBankTernaryButtons().ToArray(); AddInternal(new DrawableRulesetDependenciesProvidingContainer(Composer.Ruleset) { @@ -98,6 +94,9 @@ namespace osu.Game.Screens.Edit.Compose.Components foreach (var kvp in SelectionHandler.SelectionAdditionBankStates) kvp.Value.BindValueChanged(_ => updatePlacementSamples()); + + SelectionHandler.AutoSelectionBankEnabled.BindValueChanged(_ => updateAutoBankTernaryButtonTooltip(), true); + SelectionHandler.SelectionAdditionBanksEnabled.BindValueChanged(_ => updateAdditionBankTernaryButtonTooltips(), true); } protected override void TransferBlueprintFor(HitObject hitObject, DrawableHitObject drawableObject) @@ -238,28 +237,45 @@ namespace osu.Game.Screens.Edit.Compose.Components /// /// A collection of states which will be displayed to the user in the toolbox. /// - public TernaryButton[] MainTernaryStates { get; private set; } + public DrawableTernaryButton[] MainTernaryStates { get; private set; } - public TernaryButton[] SampleBankTernaryStates { get; private set; } - - public TernaryButton[] SampleAdditionBankTernaryStates { get; private set; } + public SampleBankTernaryButton[] SampleBankTernaryStates { get; private set; } /// /// Create all ternary states required to be displayed to the user. /// - protected virtual IEnumerable CreateTernaryButtons() + protected virtual IEnumerable CreateTernaryButtons() { //TODO: this should only be enabled (visible?) for rulesets that provide combo-supporting HitObjects. - yield return new TernaryButton(NewCombo, "New combo", () => new SpriteIcon { Icon = OsuIcon.EditorNewComboA }); + yield return new DrawableTernaryButton + { + Current = NewCombo, + Description = "New combo", + CreateIcon = () => new SpriteIcon { Icon = OsuIcon.EditorNewComboA }, + }; foreach (var kvp in SelectionHandler.SelectionSampleStates) - yield return new TernaryButton(kvp.Value, kvp.Key.Replace("hit", string.Empty).Titleize(), () => GetIconForSample(kvp.Key)); + { + yield return new DrawableTernaryButton + { + Current = kvp.Value, + Description = kvp.Key.Replace(@"hit", string.Empty).Titleize(), + CreateIcon = () => GetIconForSample(kvp.Key), + }; + } } - private IEnumerable createSampleBankTernaryButtons(Dictionary> sampleBankStates) + private IEnumerable createSampleBankTernaryButtons() { - foreach (var kvp in sampleBankStates) - yield return new TernaryButton(kvp.Value, kvp.Key.Titleize(), () => getIconForBank(kvp.Key)); + foreach (string bankName in HitSampleInfo.ALL_BANKS.Prepend(EditorSelectionHandler.HIT_BANK_AUTO)) + { + yield return new SampleBankTernaryButton(bankName) + { + NormalState = { Current = SelectionHandler.SelectionBankStates[bankName], }, + AdditionsState = { Current = SelectionHandler.SelectionAdditionBankStates[bankName], }, + CreateIcon = () => getIconForBank(bankName) + }; + } } private Drawable getIconForBank(string sampleName) @@ -295,19 +311,19 @@ namespace osu.Game.Screens.Edit.Compose.Components { bool enabled = SelectionHandler.AutoSelectionBankEnabled.Value; - var autoBankButton = SampleBankTernaryStates.Single(t => t.Bindable == SelectionHandler.SelectionBankStates[EditorSelectionHandler.HIT_BANK_AUTO]); - autoBankButton.Enabled.Value = enabled; - autoBankButton.Tooltip = !enabled ? "Auto normal bank can only be used during hit object placement" : string.Empty; + var autoBankButton = SampleBankTernaryStates.Single(t => t.BankName == EditorSelectionHandler.HIT_BANK_AUTO); + autoBankButton.NormalButton.Enabled.Value = enabled; + autoBankButton.NormalButton.TooltipText = !enabled ? "Auto normal bank can only be used during hit object placement" : string.Empty; } private void updateAdditionBankTernaryButtonTooltips() { bool enabled = SelectionHandler.SelectionAdditionBanksEnabled.Value; - foreach (var ternaryButton in SampleAdditionBankTernaryStates) + foreach (var ternaryButton in SampleBankTernaryStates) { - ternaryButton.Enabled.Value = enabled; - ternaryButton.Tooltip = !enabled ? "Add an addition sample first to be able to set a bank" : string.Empty; + ternaryButton.AdditionsButton.Enabled.Value = enabled; + ternaryButton.AdditionsButton.TooltipText = !enabled ? "Add an addition sample first to be able to set a bank" : string.Empty; } } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index 4ca3f93f13..5e8637c1ac 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -300,7 +300,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline createStateBindables(); updateTernaryStates(); - togglesCollection.AddRange(createTernaryButtons().Select(b => new DrawableTernaryButton(b) { RelativeSizeAxes = Axes.None, Size = new Vector2(40, 40) })); + togglesCollection.AddRange(createTernaryButtons()); } private string? getCommonBank() => allRelevantSamples.Select(h => GetBankValue(h.samples)).Distinct().Count() == 1 @@ -444,10 +444,19 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } } - private IEnumerable createTernaryButtons() + private IEnumerable createTernaryButtons() { foreach ((string sampleName, var bindable) in selectionSampleStates) - yield return new TernaryButton(bindable, string.Empty, () => ComposeBlueprintContainer.GetIconForSample(sampleName)); + { + yield return new DrawableTernaryButton + { + Current = bindable, + Description = string.Empty, + CreateIcon = () => ComposeBlueprintContainer.GetIconForSample(sampleName), + RelativeSizeAxes = Axes.None, + Size = new Vector2(40, 40), + }; + } } private void addHitSample(string sampleName) @@ -516,7 +525,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (item is not DrawableTernaryButton button) return base.OnKeyDown(e); - button.Button.Toggle(); + button.Toggle(); } return true;