1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-15 06:42:56 +08:00

Merge pull request #10243 from peppy/editor-ternary-buttons

Add ternary toggle buttons to editor toolbox selection
This commit is contained in:
Dan Balasescu 2020-09-25 19:37:18 +09:00 committed by GitHub
commit f588cf459d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 231 additions and 50 deletions

View File

@ -9,7 +9,9 @@ using osu.Framework.Bindables;
using osu.Framework.Caching;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Mods;
@ -17,6 +19,7 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Edit.Components.TernaryButtons;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
@ -39,11 +42,11 @@ namespace osu.Game.Rulesets.Osu.Edit
new SpinnerCompositionTool()
};
private readonly BindableBool distanceSnapToggle = new BindableBool(true) { Description = "Distance Snap" };
private readonly Bindable<TernaryState> distanceSnapToggle = new Bindable<TernaryState>();
protected override IEnumerable<Bindable<bool>> Toggles => base.Toggles.Concat(new[]
protected override IEnumerable<TernaryButton> CreateTernaryButtons() => base.CreateTernaryButtons().Concat(new[]
{
distanceSnapToggle
new TernaryButton(distanceSnapToggle, "Distance Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Ruler })
});
private BindableList<HitObject> selectedHitObjects;
@ -156,7 +159,7 @@ namespace osu.Game.Rulesets.Osu.Edit
distanceSnapGridCache.Invalidate();
distanceSnapGrid = null;
if (!distanceSnapToggle.Value)
if (distanceSnapToggle.Value != TernaryState.True)
return;
switch (BlueprintContainer.CurrentTool)

View File

@ -17,6 +17,11 @@ namespace osu.Game.Audio
public const string HIT_NORMAL = @"hitnormal";
public const string HIT_CLAP = @"hitclap";
/// <summary>
/// All valid sample addition constants.
/// </summary>
public static IEnumerable<string> AllAdditions => new[] { HIT_WHISTLE, HIT_CLAP, HIT_FINISH };
/// <summary>
/// The bank to load the sample from.
/// </summary>

View File

@ -6,7 +6,6 @@ using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
@ -14,7 +13,6 @@ using osu.Framework.Input.Events;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Configuration;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Mods;
@ -24,6 +22,7 @@ using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Components.RadioButtons;
using osu.Game.Screens.Edit.Components.TernaryButtons;
using osu.Game.Screens.Edit.Compose;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
@ -63,7 +62,7 @@ namespace osu.Game.Rulesets.Edit
private RadioButtonCollection toolboxCollection;
private ToolboxGroup togglesCollection;
private FillFlowContainer togglesCollection;
protected HitObjectComposer(Ruleset ruleset)
{
@ -121,14 +120,19 @@ namespace osu.Game.Rulesets.Edit
Spacing = new Vector2(10),
Children = new Drawable[]
{
new ToolboxGroup("toolbox") { Child = toolboxCollection = new RadioButtonCollection { RelativeSizeAxes = Axes.X } },
togglesCollection = new ToolboxGroup("toggles")
new ToolboxGroup("toolbox (1-9)")
{
ChildrenEnumerable = Toggles.Select(b => new SettingsCheckbox
Child = toolboxCollection = new RadioButtonCollection { RelativeSizeAxes = Axes.X }
},
new ToolboxGroup("toggles (Q~P)")
{
Bindable = b,
LabelText = b?.Description ?? "unknown"
})
Child = togglesCollection = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 5),
},
}
}
},
@ -139,6 +143,9 @@ namespace osu.Game.Rulesets.Edit
.Select(t => new RadioButton(t.Name, () => toolSelected(t), t.CreateIcon))
.ToList();
TernaryStates = CreateTernaryButtons().ToArray();
togglesCollection.AddRange(TernaryStates.Select(b => new DrawableTernaryButton(b)));
setSelectTool();
EditorBeatmap.SelectedHitObjects.CollectionChanged += selectionChanged;
@ -167,10 +174,14 @@ namespace osu.Game.Rulesets.Edit
protected abstract IReadOnlyList<HitObjectCompositionTool> CompositionTools { get; }
/// <summary>
/// A collection of toggles which will be displayed to the user.
/// The display name will be decided by <see cref="Bindable{T}.Description"/>.
/// A collection of states which will be displayed to the user in the toolbox.
/// </summary>
protected virtual IEnumerable<Bindable<bool>> Toggles => BlueprintContainer.Toggles;
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.TernaryStates;
/// <summary>
/// Construct a relevant blueprint container. This will manage hitobject selection/placement input handling and display logic.
@ -215,9 +226,9 @@ namespace osu.Game.Rulesets.Edit
{
var item = togglesCollection.ElementAtOrDefault(rightIndex);
if (item is SettingsCheckbox checkbox)
if (item is DrawableTernaryButton button)
{
checkbox.Bindable.Value = !checkbox.Bindable.Value;
button.Button.Toggle();
return true;
}
}

View File

@ -0,0 +1,112 @@
// 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.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Edit.Components.TernaryButtons
{
internal class DrawableTernaryButton : TriangleButton
{
private Color4 defaultBackgroundColour;
private Color4 defaultBubbleColour;
private Color4 selectedBackgroundColour;
private Color4 selectedBubbleColour;
private Drawable icon;
public readonly TernaryButton Button;
public DrawableTernaryButton(TernaryButton button)
{
Button = button;
Text = button.Description;
RelativeSizeAxes = Axes.X;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
defaultBackgroundColour = colours.Gray3;
defaultBubbleColour = defaultBackgroundColour.Darken(0.5f);
selectedBackgroundColour = colours.BlueDark;
selectedBubbleColour = selectedBackgroundColour.Lighten(0.5f);
Triangles.Alpha = 0;
Content.EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Shadow,
Radius = 2,
Offset = new Vector2(0, 1),
Colour = Color4.Black.Opacity(0.5f)
};
Add(icon = (Button.CreateIcon?.Invoke() ?? new Circle()).With(b =>
{
b.Blending = BlendingParameters.Additive;
b.Anchor = Anchor.CentreLeft;
b.Origin = Anchor.CentreLeft;
b.Size = new Vector2(20);
b.X = 10;
}));
}
protected override void LoadComplete()
{
base.LoadComplete();
Button.Bindable.BindValueChanged(selected => updateSelectionState(), true);
Action = onAction;
}
private void onAction()
{
Button.Toggle();
}
private void updateSelectionState()
{
if (!IsLoaded)
return;
switch (Button.Bindable.Value)
{
case TernaryState.Indeterminate:
icon.Colour = selectedBubbleColour.Darken(0.5f);
BackgroundColour = selectedBackgroundColour.Darken(0.5f);
break;
case TernaryState.False:
icon.Colour = defaultBubbleColour;
BackgroundColour = defaultBackgroundColour;
break;
case TernaryState.True:
icon.Colour = selectedBubbleColour;
BackgroundColour = selectedBackgroundColour;
break;
}
}
protected override SpriteText CreateText() => new OsuSpriteText
{
Depth = -1,
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
X = 40f
};
}
}

View File

@ -0,0 +1,44 @@
// 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 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 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

@ -7,12 +7,15 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Screens.Edit.Components.TernaryButtons;
using osuTK;
namespace osu.Game.Screens.Edit.Compose.Components
@ -48,6 +51,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
[BackgroundDependencyLoader]
private void load()
{
TernaryStates = CreateTernaryButtons().ToArray();
AddInternal(placementBlueprintContainer);
}
@ -57,36 +62,34 @@ namespace osu.Game.Screens.Edit.Compose.Components
inputManager = GetContainingInputManager();
Beatmap.SelectedHitObjects.CollectionChanged += (_, __) => updateTogglesFromSelection();
// the updated object may be in the selection
Beatmap.HitObjectUpdated += _ => updateTogglesFromSelection();
// updates to selected are handled for us by SelectionHandler.
NewCombo.BindTo(SelectionHandler.SelectionNewComboState);
// we are responsible for current placement blueprint updated based on state changes.
NewCombo.ValueChanged += combo =>
{
if (Beatmap.SelectedHitObjects.Count > 0)
{
SelectionHandler.SetNewCombo(combo.NewValue);
}
else if (currentPlacement != null)
{
// update placement object from toggle
if (currentPlacement == null) return;
if (currentPlacement.HitObject is IHasComboInformation c)
c.NewCombo = combo.NewValue;
}
c.NewCombo = combo.NewValue == TernaryState.True;
};
}
private void updateTogglesFromSelection() =>
NewCombo.Value = Beatmap.SelectedHitObjects.OfType<IHasComboInformation>().All(c => c.NewCombo);
public readonly Bindable<TernaryState> NewCombo = new Bindable<TernaryState> { Description = "New Combo" };
public readonly Bindable<bool> NewCombo = new Bindable<bool> { Description = "New Combo" };
/// <summary>
/// A collection of states which will be displayed to the user in the toolbox.
/// </summary>
public TernaryButton[] TernaryStates { get; private set; }
public virtual IEnumerable<Bindable<bool>> Toggles => new[]
/// <summary>
/// Create all ternary states required to be displayed to the user.
/// </summary>
protected virtual IEnumerable<TernaryButton> CreateTernaryButtons()
{
//TODO: this should only be enabled (visible?) for rulesets that provide combo-supporting HitObjects.
NewCombo
};
yield return new TernaryButton(NewCombo, "New combo", () => new SpriteIcon { Icon = FontAwesome.Regular.DotCircle });
}
#region Placement

View File

@ -316,19 +316,22 @@ namespace osu.Game.Screens.Edit.Compose.Components
#region Selection State
private readonly Bindable<TernaryState> selectionNewComboState = new Bindable<TernaryState>();
/// <summary>
/// The state of "new combo" for all selected hitobjects.
/// </summary>
public readonly Bindable<TernaryState> SelectionNewComboState = new Bindable<TernaryState>();
private readonly Dictionary<string, Bindable<TernaryState>> selectionSampleStates = new Dictionary<string, Bindable<TernaryState>>();
/// <summary>
/// The state of each sample type for all selected hitobjects. Keys match with <see cref="HitSampleInfo"/> constant specifications.
/// </summary>
public readonly Dictionary<string, Bindable<TernaryState>> SelectionSampleStates = new Dictionary<string, Bindable<TernaryState>>();
/// <summary>
/// Set up ternary state bindables and bind them to selection/hitobject changes (in both directions)
/// </summary>
private void createStateBindables()
{
// hit samples
var sampleTypes = new[] { HitSampleInfo.HIT_WHISTLE, HitSampleInfo.HIT_CLAP, HitSampleInfo.HIT_FINISH };
foreach (var sampleName in sampleTypes)
foreach (var sampleName in HitSampleInfo.AllAdditions)
{
var bindable = new Bindable<TernaryState>
{
@ -349,11 +352,11 @@ namespace osu.Game.Screens.Edit.Compose.Components
}
};
selectionSampleStates[sampleName] = bindable;
SelectionSampleStates[sampleName] = bindable;
}
// new combo
selectionNewComboState.ValueChanged += state =>
SelectionNewComboState.ValueChanged += state =>
{
switch (state.NewValue)
{
@ -377,9 +380,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// </summary>
protected virtual void UpdateTernaryStates()
{
selectionNewComboState.Value = GetStateFromSelection(SelectedHitObjects.OfType<IHasComboInformation>(), h => h.NewCombo);
SelectionNewComboState.Value = GetStateFromSelection(SelectedHitObjects.OfType<IHasComboInformation>(), h => h.NewCombo);
foreach (var (sampleName, bindable) in selectionSampleStates)
foreach (var (sampleName, bindable) in SelectionSampleStates)
{
bindable.Value = GetStateFromSelection(SelectedHitObjects, h => h.Samples.Any(s => s.Name == sampleName));
}
@ -413,7 +416,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (selectedBlueprints.All(b => b.HitObject is IHasComboInformation))
{
items.Add(new TernaryStateMenuItem("New combo") { State = { BindTarget = selectionNewComboState } });
items.Add(new TernaryStateMenuItem("New combo") { State = { BindTarget = SelectionNewComboState } });
}
if (selectedBlueprints.Count == 1)
@ -423,7 +426,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
new OsuMenuItem("Sound")
{
Items = selectionSampleStates.Select(kvp =>
Items = SelectionSampleStates.Select(kvp =>
new TernaryStateMenuItem(kvp.Value.Description) { State = { BindTarget = kvp.Value } }).ToArray()
},
new OsuMenuItem("Delete", MenuItemType.Destructive, deleteSelected),