1
0
mirror of https://github.com/ppy/osu.git synced 2025-02-20 18:43:04 +08:00

Simplify editor "ternary button" structure

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.
This commit is contained in:
Bartłomiej Dach 2025-01-09 13:04:13 +01:00
parent 2e10f83b5c
commit 5c8ae6f851
No known key found for this signature in database
10 changed files with 147 additions and 117 deletions

View File

@ -72,7 +72,7 @@ namespace osu.Game.Rulesets.Catch.Edit
protected override Drawable CreateHitObjectInspector() => new CatchHitObjectInspector(DistanceSnapProvider);
protected override IEnumerable<TernaryButton> CreateTernaryButtons()
protected override IEnumerable<DrawableTernaryButton> CreateTernaryButtons()
=> base.CreateTernaryButtons()
.Concat(DistanceSnapProvider.CreateTernaryButtons());

View File

@ -53,9 +53,14 @@ namespace osu.Game.Rulesets.Osu.Edit
protected override Drawable CreateHitObjectInspector() => new OsuHitObjectInspector();
protected override IEnumerable<TernaryButton> CreateTernaryButtons()
protected override IEnumerable<DrawableTernaryButton> 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<HitObject> selectedHitObjects;

View File

@ -191,9 +191,14 @@ namespace osu.Game.Rulesets.Edit
}
}
public IEnumerable<TernaryButton> CreateTernaryButtons() => new[]
public IEnumerable<DrawableTernaryButton> 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)

View File

@ -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
/// </remarks>
protected abstract IReadOnlyList<CompositionTool> CompositionTools { get; }
/// <summary>
/// A collection of states which will be displayed to the user in the toolbox.
/// </summary>
public TernaryButton[] TernaryStates { get; private set; }
/// <summary>
/// Create all ternary states required to be displayed to the user.
/// </summary>
protected virtual IEnumerable<TernaryButton> CreateTernaryButtons() => BlueprintContainer.MainTernaryStates;
protected virtual IEnumerable<DrawableTernaryButton> CreateTernaryButtons() => BlueprintContainer.MainTernaryStates;
/// <summary>
/// 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;
}
}

View File

@ -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 },
}
}
},
});

View File

@ -1,12 +1,15 @@
// 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.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<TernaryState>
{
public Bindable<TernaryState> Current
{
get => current.Current;
set => current.Current = value;
}
private readonly BindableWithCurrent<TernaryState> current = new BindableWithCurrent<TernaryState>();
public required LocalisableString Description
{
get => Text;
set => Text = value;
}
public LocalisableString TooltipText { get; set; }
/// <summary>
/// A function which creates a drawable icon to represent this item. If null, a sane default should be used.
/// </summary>
public Func<Drawable>? 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;
}
}

View File

@ -1,23 +1,32 @@
// 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 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<Drawable>? CreateIcon { get; init; }
public SampleBankTernaryButton(TernaryButton normalButton, TernaryButton additionsButton)
public readonly BindableWithCurrent<TernaryState> NormalState = new BindableWithCurrent<TernaryState>();
public readonly BindableWithCurrent<TernaryState> AdditionsState = new BindableWithCurrent<TernaryState>();
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()
{

View File

@ -1,48 +0,0 @@
// 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.Bindables;
using osu.Framework.Graphics;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Screens.Edit.Components.TernaryButtons
{
public class TernaryButton
{
public readonly Bindable<TernaryState> Bindable;
public readonly Bindable<bool> Enabled = new Bindable<bool>(true);
public readonly string Description;
/// <summary>
/// A function which creates a drawable icon to represent this item. If null, a sane default should be used.
/// </summary>
public readonly Func<Drawable>? CreateIcon;
public string Tooltip { get; set; } = string.Empty;
public TernaryButton(Bindable<TernaryState> bindable, string description, Func<Drawable>? 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;
}
}
}
}

View File

@ -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
/// <summary>
/// A collection of states which will be displayed to the user in the toolbox.
/// </summary>
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; }
/// <summary>
/// Create all ternary states required to be displayed to the user.
/// </summary>
protected virtual IEnumerable<TernaryButton> CreateTernaryButtons()
protected virtual IEnumerable<DrawableTernaryButton> 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<TernaryButton> createSampleBankTernaryButtons(Dictionary<string, Bindable<TernaryState>> sampleBankStates)
private IEnumerable<SampleBankTernaryButton> 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;
}
}

View File

@ -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<TernaryButton> createTernaryButtons()
private IEnumerable<DrawableTernaryButton> 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;