mirror of
https://github.com/ppy/osu.git
synced 2026-05-18 18:29:58 +08:00
e05b6f44b9
(partially) Closes: #36233 Surpasses: #36244 This PR meant to be one of the last steps that finally make editor use the new forms. Initially it meant to only change one SliderWithTextBoxInput in "Effects section" in timing screen, however soon after it was obvious that there's many other places that still using it. This currently won't affect IndeterminateSliderWithTextBoxInput that is being used in hitsounds, for example, since I think it needs more consideration. Anyways, with this PR, SliderWithTextBoxInput, will no longer be used at all, as it's going to be replaced with modern FormSliderBar Comparison: |master|this PR| |:---:|:---:| |<img width="510" height="316" alt="532203751-eb965923-d3a8-441d-a7c8-5c364a6328ad" src="https://github.com/user-attachments/assets/268b45b8-e235-494f-91a5-d00db057dba8" />|<img width="540" height="321" alt="535466527-3a700a8b-bc3c-4610-998f-a4e55ee03eed" src="https://github.com/user-attachments/assets/20cd4b58-b0bd-49bc-8c48-7de5cf8556b3" />| |<img width="694" height="639" alt="534509844-f00e4da4-53c4-45e8-80ea-1be62da6c83b" src="https://github.com/user-attachments/assets/398c4484-a867-4df1-9de3-0940aa748a01" />|<img width="720" height="433" alt="изображение" src="https://github.com/user-attachments/assets/b6359443-a224-4a55-b171-07e8f013cf46" />| |<img width="715" height="353" alt="534509421-a6ac950f-16e8-4a16-bca6-1a781f82135f" src="https://github.com/user-attachments/assets/4854312b-772f-4b81-a800-89e58d4c715d" />|<img width="710" height="296" alt="изображение" src="https://github.com/user-attachments/assets/a7fed53e-e006-4285-92c9-bb84cb603f60" />| |<img width="717" height="374" alt="534509478-80222623-7766-481d-8682-088276d415ee" src="https://github.com/user-attachments/assets/8143b6dc-4599-45d5-bd3b-f059caf3d93d" />|<img width="718" height="328" alt="изображение" src="https://github.com/user-attachments/assets/bffa04de-983c-45ae-a1ec-373701ea0e49" />| |<img width="702" height="446" alt="534509935-58954060-7ac1-4392-8754-a58f909e86aa" src="https://github.com/user-attachments/assets/2bb67a2d-3f57-42a1-96ce-b30b4891e1a4" />|<img width="722" height="386" alt="изображение" src="https://github.com/user-attachments/assets/01b7fff4-7f31-4aac-90c9-353b15f4964e" />|
594 lines
22 KiB
C#
594 lines
22 KiB
C#
// 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 System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.Numerics;
|
|
using osu.Framework.Allocation;
|
|
using osu.Framework.Bindables;
|
|
using osu.Framework.Extensions.Color4Extensions;
|
|
using osu.Framework.Extensions.ObjectExtensions;
|
|
using osu.Framework.Graphics;
|
|
using osu.Framework.Graphics.Colour;
|
|
using osu.Framework.Graphics.Containers;
|
|
using osu.Framework.Graphics.Shapes;
|
|
using osu.Framework.Graphics.UserInterface;
|
|
using osu.Framework.Input;
|
|
using osu.Framework.Input.Events;
|
|
using osu.Framework.Localisation;
|
|
using osu.Game.Extensions;
|
|
using osu.Game.Graphics.Sprites;
|
|
using osu.Game.Graphics.UserInterface;
|
|
using osu.Game.Localisation;
|
|
using osu.Game.Overlays;
|
|
using Vector2 = osuTK.Vector2;
|
|
|
|
namespace osu.Game.Graphics.UserInterfaceV2
|
|
{
|
|
public partial class FormSliderBar<T> : CompositeDrawable, IHasCurrentValue<T>, IFormControl
|
|
where T : struct, INumber<T>, IMinMaxValue<T>
|
|
{
|
|
public Bindable<T> Current
|
|
{
|
|
get => current.Current;
|
|
set
|
|
{
|
|
current.Current = value;
|
|
currentNumberInstantaneous.Default = current.Default;
|
|
}
|
|
}
|
|
|
|
private readonly BindableNumberWithCurrent<T> current = new BindableNumberWithCurrent<T>();
|
|
|
|
private readonly BindableNumber<T> currentNumberInstantaneous = new BindableNumber<T>();
|
|
|
|
/// <summary>
|
|
/// Whether changes to the value should instantaneously transfer to outside bindables.
|
|
/// If <see langword="false"/>, the transfer will happen on text box commit (explicit, or implicit via focus loss), or on slider commit.
|
|
/// </summary>
|
|
public bool TransferValueOnCommit { get; set; }
|
|
|
|
private CompositeDrawable? tabbableContentContainer;
|
|
|
|
public CompositeDrawable? TabbableContentContainer
|
|
{
|
|
set
|
|
{
|
|
tabbableContentContainer = value;
|
|
|
|
if (textBox.IsNotNull())
|
|
textBox.TabbableContentContainer = tabbableContentContainer;
|
|
}
|
|
}
|
|
|
|
private LocalisableString caption;
|
|
|
|
/// <summary>
|
|
/// Caption describing this slider bar, displayed on top of the controls.
|
|
/// </summary>
|
|
public LocalisableString Caption
|
|
{
|
|
get => caption;
|
|
set
|
|
{
|
|
caption = value;
|
|
|
|
if (IsLoaded)
|
|
captionText.Caption = value;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Hint text containing an extended description of this slider bar, displayed in a tooltip when hovering the caption.
|
|
/// </summary>
|
|
public LocalisableString HintText { get; init; }
|
|
|
|
private float keyboardStep;
|
|
|
|
/// <summary>
|
|
/// A custom step value for each key press which actuates a change on this control.
|
|
/// </summary>
|
|
public float KeyboardStep
|
|
{
|
|
get => keyboardStep;
|
|
set
|
|
{
|
|
keyboardStep = value;
|
|
if (IsLoaded)
|
|
slider.KeyboardStep = value;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Whether to format the tooltip as a percentage or the actual value.
|
|
/// </summary>
|
|
public bool DisplayAsPercentage { get; init; }
|
|
|
|
/// <summary>
|
|
/// Whether sound effects should play when adjusting this slider.
|
|
/// </summary>
|
|
public bool PlaySamplesOnAdjust { get; init; }
|
|
|
|
/// <summary>
|
|
/// The string formatting function to use for the value label.
|
|
/// </summary>
|
|
public Func<T, LocalisableString> LabelFormat { get; init; }
|
|
|
|
/// <summary>
|
|
/// The string formatting function to use for the slider's tooltip text.
|
|
/// If not provided, <see cref="LabelFormat"/> is used.
|
|
/// </summary>
|
|
public Func<T, LocalisableString> TooltipFormat { get; init; }
|
|
|
|
private Box background = null!;
|
|
private Box flashLayer = null!;
|
|
private FormTextBox.InnerTextBox textBox = null!;
|
|
private OsuSpriteText valueLabel = null!;
|
|
private InnerSlider slider = null!;
|
|
private FormFieldCaption captionText = null!;
|
|
private IFocusManager focusManager = null!;
|
|
|
|
[Resolved]
|
|
private OverlayColourProvider colourProvider { get; set; } = null!;
|
|
|
|
private readonly Bindable<Language> currentLanguage = new Bindable<Language>();
|
|
|
|
public bool TakeFocus() => GetContainingFocusManager()?.ChangeFocus(textBox) == true;
|
|
|
|
public FormSliderBar()
|
|
{
|
|
LabelFormat ??= defaultLabelFormat;
|
|
TooltipFormat ??= v => LabelFormat(v);
|
|
}
|
|
|
|
[BackgroundDependencyLoader]
|
|
private void load(OsuColour colours, OsuGame? game)
|
|
{
|
|
RelativeSizeAxes = Axes.X;
|
|
AutoSizeAxes = Axes.Y;
|
|
|
|
Masking = true;
|
|
CornerRadius = 5;
|
|
|
|
InternalChildren = new Drawable[]
|
|
{
|
|
background = new Box
|
|
{
|
|
RelativeSizeAxes = Axes.Both,
|
|
Colour = colourProvider.Background5,
|
|
},
|
|
flashLayer = new Box
|
|
{
|
|
RelativeSizeAxes = Axes.Both,
|
|
Colour = Colour4.Transparent,
|
|
},
|
|
new Container
|
|
{
|
|
RelativeSizeAxes = Axes.X,
|
|
AutoSizeAxes = Axes.Y,
|
|
Padding = new MarginPadding
|
|
{
|
|
Vertical = 5,
|
|
Left = 9,
|
|
Right = 5,
|
|
},
|
|
Children = new Drawable[]
|
|
{
|
|
new FillFlowContainer
|
|
{
|
|
RelativeSizeAxes = Axes.X,
|
|
AutoSizeAxes = Axes.Y,
|
|
Direction = FillDirection.Vertical,
|
|
Spacing = new Vector2(0f, 4f),
|
|
Width = 0.5f,
|
|
Padding = new MarginPadding
|
|
{
|
|
Right = 10,
|
|
Vertical = 4,
|
|
},
|
|
Children = new Drawable[]
|
|
{
|
|
captionText = new FormFieldCaption
|
|
{
|
|
TooltipText = HintText,
|
|
},
|
|
new Container
|
|
{
|
|
RelativeSizeAxes = Axes.X,
|
|
AutoSizeAxes = Axes.Y,
|
|
Children = new Drawable[]
|
|
{
|
|
textBox = new FormNumberBox.InnerNumberBox(allowDecimals: true)
|
|
{
|
|
RelativeSizeAxes = Axes.X,
|
|
// the textbox is hidden when the control is unfocused,
|
|
// but clicking on the label should reach the textbox,
|
|
// therefore make it always present.
|
|
AlwaysPresent = true,
|
|
CommitOnFocusLost = true,
|
|
SelectAllOnFocus = true,
|
|
OnInputError = () =>
|
|
{
|
|
flashLayer.Colour = ColourInfo.GradientVertical(colours.Red3.Opacity(0), colours.Red3);
|
|
flashLayer.FadeOutFromOne(200, Easing.OutQuint);
|
|
},
|
|
TabbableContentContainer = tabbableContentContainer,
|
|
},
|
|
valueLabel = new TruncatingSpriteText
|
|
{
|
|
RelativeSizeAxes = Axes.X,
|
|
Padding = new MarginPadding { Right = 5 },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
slider = new InnerSlider
|
|
{
|
|
Anchor = Anchor.CentreRight,
|
|
Origin = Anchor.CentreRight,
|
|
RelativeSizeAxes = Axes.X,
|
|
Width = 0.5f,
|
|
Current = currentNumberInstantaneous,
|
|
OnCommit = () => current.Value = currentNumberInstantaneous.Value,
|
|
TooltipFormat = TooltipFormat,
|
|
DisplayAsPercentage = DisplayAsPercentage,
|
|
PlaySamplesOnAdjust = PlaySamplesOnAdjust,
|
|
ResetToDefault = () =>
|
|
{
|
|
if (!IsDisabled)
|
|
SetDefault();
|
|
}
|
|
}
|
|
},
|
|
},
|
|
};
|
|
|
|
if (game != null)
|
|
currentLanguage.BindTo(game.CurrentLanguage);
|
|
}
|
|
|
|
protected override void LoadComplete()
|
|
{
|
|
base.LoadComplete();
|
|
|
|
slider.KeyboardStep = keyboardStep;
|
|
captionText.Caption = caption;
|
|
|
|
focusManager = GetContainingFocusManager()!;
|
|
|
|
textBox.Focused.BindValueChanged(_ => updateState());
|
|
textBox.OnCommit += textCommitted;
|
|
textBox.Current.BindValueChanged(textChanged);
|
|
|
|
slider.IsDragging.BindValueChanged(_ => updateState());
|
|
slider.Focused.BindValueChanged(_ => updateState());
|
|
|
|
current.ValueChanged += e =>
|
|
{
|
|
currentNumberInstantaneous.Value = e.NewValue;
|
|
ValueChanged?.Invoke();
|
|
};
|
|
|
|
current.MinValueChanged += v => currentNumberInstantaneous.MinValue = v;
|
|
current.MaxValueChanged += v => currentNumberInstantaneous.MaxValue = v;
|
|
current.PrecisionChanged += v => currentNumberInstantaneous.Precision = v;
|
|
current.DisabledChanged += disabled =>
|
|
{
|
|
if (disabled)
|
|
{
|
|
// revert any changes before disabling to make sure we are in a consistent state.
|
|
currentNumberInstantaneous.Value = current.Value;
|
|
}
|
|
|
|
currentNumberInstantaneous.Disabled = disabled;
|
|
updateState();
|
|
};
|
|
|
|
current.CopyTo(currentNumberInstantaneous);
|
|
currentLanguage.BindValueChanged(_ => Schedule(updateValueDisplay));
|
|
currentNumberInstantaneous.BindDisabledChanged(_ => updateState());
|
|
currentNumberInstantaneous.BindValueChanged(e =>
|
|
{
|
|
if (!TransferValueOnCommit)
|
|
current.Value = e.NewValue;
|
|
|
|
updateState();
|
|
updateValueDisplay();
|
|
}, true);
|
|
}
|
|
|
|
private bool updatingFromTextBox;
|
|
|
|
private void textChanged(ValueChangedEvent<string> change)
|
|
{
|
|
tryUpdateSliderFromTextBox();
|
|
}
|
|
|
|
private void textCommitted(TextBox t, bool isNew)
|
|
{
|
|
tryUpdateSliderFromTextBox();
|
|
// If the attempted update above failed, restore text box to match the slider.
|
|
currentNumberInstantaneous.TriggerChange();
|
|
current.Value = currentNumberInstantaneous.Value;
|
|
|
|
flashLayer.Colour = ColourInfo.GradientVertical(colourProvider.Dark2.Opacity(0), colourProvider.Dark2);
|
|
flashLayer.FadeOutFromOne(800, Easing.OutQuint);
|
|
}
|
|
|
|
private void tryUpdateSliderFromTextBox()
|
|
{
|
|
updatingFromTextBox = true;
|
|
|
|
try
|
|
{
|
|
switch (currentNumberInstantaneous)
|
|
{
|
|
case Bindable<int> bindableInt:
|
|
bindableInt.Value = int.Parse(textBox.Current.Value);
|
|
break;
|
|
|
|
case Bindable<double> bindableDouble:
|
|
bindableDouble.Value = double.Parse(textBox.Current.Value);
|
|
break;
|
|
|
|
default:
|
|
currentNumberInstantaneous.Parse(textBox.Current.Value, CultureInfo.CurrentCulture);
|
|
break;
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// ignore parsing failures.
|
|
// sane state will eventually be restored by a commit (either explicit, or implicit via focus loss).
|
|
}
|
|
|
|
updatingFromTextBox = false;
|
|
}
|
|
|
|
protected override bool OnHover(HoverEvent e)
|
|
{
|
|
updateState();
|
|
return true;
|
|
}
|
|
|
|
protected override void OnHoverLost(HoverLostEvent e)
|
|
{
|
|
base.OnHoverLost(e);
|
|
updateState();
|
|
}
|
|
|
|
protected override bool OnClick(ClickEvent e)
|
|
{
|
|
if (!Current.Disabled)
|
|
focusManager.ChangeFocus(textBox);
|
|
return true;
|
|
}
|
|
|
|
private void updateState()
|
|
{
|
|
bool childHasFocus = slider.Focused.Value || textBox.Focused.Value;
|
|
|
|
textBox.ReadOnly = currentNumberInstantaneous.Disabled;
|
|
textBox.Alpha = textBox.Focused.Value ? 1 : 0;
|
|
valueLabel.Alpha = textBox.Focused.Value ? 0 : 1;
|
|
|
|
captionText.Colour = currentNumberInstantaneous.Disabled ? colourProvider.Background1 : colourProvider.Content2;
|
|
textBox.Colour = currentNumberInstantaneous.Disabled ? colourProvider.Background1 : colourProvider.Content1;
|
|
valueLabel.Colour = currentNumberInstantaneous.Disabled ? colourProvider.Background1 : colourProvider.Content1;
|
|
|
|
BorderThickness = childHasFocus || IsHovered || slider.IsDragging.Value ? 2 : 0;
|
|
|
|
if (Current.Disabled)
|
|
BorderColour = colourProvider.Dark1;
|
|
else
|
|
BorderColour = childHasFocus ? colourProvider.Highlight1 : colourProvider.Light4;
|
|
|
|
if (childHasFocus)
|
|
background.Colour = ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark3);
|
|
else if (IsHovered || slider.IsDragging.Value)
|
|
background.Colour = ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark4);
|
|
else
|
|
background.Colour = colourProvider.Background5;
|
|
}
|
|
|
|
private void updateValueDisplay()
|
|
{
|
|
if (updatingFromTextBox) return;
|
|
|
|
textBox.Text = currentNumberInstantaneous.Value.ToStandardFormattedString(OsuSliderBar<T>.MAX_DECIMAL_DIGITS);
|
|
valueLabel.Text = LabelFormat(currentNumberInstantaneous.Value);
|
|
}
|
|
|
|
private LocalisableString defaultLabelFormat(T value) => currentNumberInstantaneous.Value.ToStandardFormattedString(OsuSliderBar<T>.MAX_DECIMAL_DIGITS, DisplayAsPercentage);
|
|
|
|
private partial class InnerSlider : OsuSliderBar<T>
|
|
{
|
|
public BindableBool Focused { get; } = new BindableBool();
|
|
|
|
public BindableBool IsDragging { get; } = new BindableBool();
|
|
|
|
public Action? ResetToDefault { get; init; }
|
|
|
|
public Action? OnCommit { get; init; }
|
|
|
|
public sealed override LocalisableString TooltipText => base.TooltipText;
|
|
|
|
public required Func<T, LocalisableString> TooltipFormat { get; init; }
|
|
|
|
private Box leftBox = null!;
|
|
private Box rightBox = null!;
|
|
private InnerSliderNub nub = null!;
|
|
private HoverClickSounds sounds = null!;
|
|
public const float NUB_WIDTH = 10;
|
|
|
|
[Resolved]
|
|
private OverlayColourProvider colourProvider { get; set; } = null!;
|
|
|
|
[BackgroundDependencyLoader]
|
|
private void load()
|
|
{
|
|
Height = 40;
|
|
RelativeSizeAxes = Axes.X;
|
|
RangePadding = NUB_WIDTH / 2;
|
|
|
|
Children = new Drawable[]
|
|
{
|
|
new Container
|
|
{
|
|
RelativeSizeAxes = Axes.Both,
|
|
Masking = true,
|
|
CornerRadius = 5,
|
|
Children = new Drawable[]
|
|
{
|
|
leftBox = new Box
|
|
{
|
|
RelativeSizeAxes = Axes.Both,
|
|
Anchor = Anchor.CentreLeft,
|
|
Origin = Anchor.CentreLeft,
|
|
},
|
|
rightBox = new Box
|
|
{
|
|
RelativeSizeAxes = Axes.Both,
|
|
Anchor = Anchor.CentreRight,
|
|
Origin = Anchor.CentreRight,
|
|
},
|
|
},
|
|
},
|
|
new Container
|
|
{
|
|
RelativeSizeAxes = Axes.Both,
|
|
Padding = new MarginPadding { Horizontal = RangePadding, },
|
|
Child = nub = new InnerSliderNub
|
|
{
|
|
ResetToDefault = ResetToDefault,
|
|
}
|
|
},
|
|
sounds = new HoverClickSounds()
|
|
};
|
|
}
|
|
|
|
protected override void LoadComplete()
|
|
{
|
|
base.LoadComplete();
|
|
Current.BindDisabledChanged(_ => updateState(), true);
|
|
}
|
|
|
|
protected override void UpdateAfterChildren()
|
|
{
|
|
base.UpdateAfterChildren();
|
|
leftBox.Width = Math.Clamp(RangePadding + nub.DrawPosition.X, 0, Math.Max(0, DrawWidth)) / DrawWidth;
|
|
rightBox.Width = Math.Clamp(DrawWidth - nub.DrawPosition.X - RangePadding, 0, Math.Max(0, DrawWidth)) / DrawWidth;
|
|
}
|
|
|
|
protected override bool OnDragStart(DragStartEvent e)
|
|
{
|
|
bool dragging = base.OnDragStart(e);
|
|
IsDragging.Value = dragging;
|
|
updateState();
|
|
return dragging;
|
|
}
|
|
|
|
protected override void OnDragEnd(DragEndEvent e)
|
|
{
|
|
base.OnDragEnd(e);
|
|
IsDragging.Value = false;
|
|
updateState();
|
|
}
|
|
|
|
protected override bool OnHover(HoverEvent e)
|
|
{
|
|
updateState();
|
|
return base.OnHover(e);
|
|
}
|
|
|
|
protected override void OnHoverLost(HoverLostEvent e)
|
|
{
|
|
updateState();
|
|
base.OnHoverLost(e);
|
|
}
|
|
|
|
protected override void OnFocus(FocusEvent e)
|
|
{
|
|
updateState();
|
|
Focused.Value = true;
|
|
base.OnFocus(e);
|
|
}
|
|
|
|
protected override void OnFocusLost(FocusLostEvent e)
|
|
{
|
|
updateState();
|
|
Focused.Value = false;
|
|
base.OnFocusLost(e);
|
|
}
|
|
|
|
private void updateState()
|
|
{
|
|
sounds.Enabled.Value = !Current.Disabled;
|
|
rightBox.Colour = colourProvider.Background6;
|
|
|
|
if (Current.Disabled)
|
|
{
|
|
leftBox.Colour = colourProvider.Dark3;
|
|
nub.Colour = colourProvider.Dark1;
|
|
}
|
|
else
|
|
{
|
|
leftBox.Colour = HasFocus || IsHovered || IsDragged ? colourProvider.Highlight1.Opacity(0.5f) : colourProvider.Highlight1.Opacity(0.3f);
|
|
nub.Colour = HasFocus || IsHovered || IsDragged ? colourProvider.Highlight1 : colourProvider.Light4;
|
|
}
|
|
}
|
|
|
|
protected override void UpdateValue(float value)
|
|
{
|
|
nub.MoveToX(value, 200, Easing.OutPow10);
|
|
}
|
|
|
|
protected override bool Commit()
|
|
{
|
|
bool result = base.Commit();
|
|
|
|
if (result)
|
|
OnCommit?.Invoke();
|
|
|
|
return result;
|
|
}
|
|
|
|
protected sealed override LocalisableString GetTooltipText(T value) => TooltipFormat(value);
|
|
}
|
|
|
|
private partial class InnerSliderNub : Circle
|
|
{
|
|
public Action? ResetToDefault { get; set; }
|
|
|
|
[BackgroundDependencyLoader]
|
|
private void load()
|
|
{
|
|
Width = InnerSlider.NUB_WIDTH;
|
|
RelativeSizeAxes = Axes.Y;
|
|
RelativePositionAxes = Axes.X;
|
|
Origin = Anchor.TopCentre;
|
|
}
|
|
|
|
protected override bool OnClick(ClickEvent e) => true; // must be handled for double click handler to ever fire
|
|
|
|
protected override bool OnDoubleClick(DoubleClickEvent e)
|
|
{
|
|
ResetToDefault?.Invoke();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
public IEnumerable<LocalisableString> FilterTerms => new[] { Caption, HintText };
|
|
|
|
public event Action? ValueChanged;
|
|
|
|
public bool IsDefault => Current.IsDefault;
|
|
|
|
public void SetDefault() => Current.SetDefault();
|
|
|
|
public bool IsDisabled => Current.Disabled;
|
|
}
|
|
}
|