1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 21:07:33 +08:00
osu-lazer/osu.Game/Overlays/Mods/ModColumn.cs

357 lines
12 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.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Humanizer;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
using osu.Game.Overlays.Mods.Input;
using osu.Game.Rulesets.Mods;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Overlays.Mods
{
public partial class ModColumn : ModSelectColumn
{
public readonly ModType ModType;
private IReadOnlyList<ModState> availableMods = Array.Empty<ModState>();
/// <summary>
/// Sets the list of mods to show in this column.
/// </summary>
public IReadOnlyList<ModState> AvailableMods
{
get => availableMods;
set
{
Debug.Assert(value.All(mod => mod.Mod.Type == ModType));
availableMods = value;
foreach (var mod in availableMods)
{
mod.Active.BindValueChanged(_ => updateState());
mod.MatchingTextFilter.BindValueChanged(_ => updateState());
mod.ValidForSelection.BindValueChanged(_ => updateState());
}
updateState();
if (IsLoaded)
asyncLoadPanels();
}
}
protected virtual ModPanel CreateModPanel(ModState mod) => new ModPanel(mod);
private readonly bool allowIncompatibleSelection;
private readonly ToggleAllCheckbox? toggleAllCheckbox;
private Bindable<ModSelectHotkeyStyle> hotkeyStyle = null!;
private IModHotkeyHandler hotkeyHandler = null!;
private Task? latestLoadTask;
private ModPanel[]? latestLoadedPanels;
internal bool ItemsLoaded => latestLoadTask?.IsCompleted == true && allPanelsLoaded;
private bool? wasPresent;
private bool allPanelsLoaded
{
get
{
if (latestLoadedPanels == null)
return false;
foreach (var panel in latestLoadedPanels)
{
if (panel.Parent == null)
return false;
}
return true;
}
}
public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks;
public ModColumn(ModType modType, bool allowIncompatibleSelection)
{
ModType = modType;
this.allowIncompatibleSelection = allowIncompatibleSelection;
HeaderText = ModType.Humanize(LetterCasing.Title);
if (allowIncompatibleSelection)
{
ControlContainer.Height = 35;
ControlContainer.Add(toggleAllCheckbox = new ToggleAllCheckbox(this)
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Scale = new Vector2(0.8f),
RelativeSizeAxes = Axes.X,
Shear = new Vector2(-OsuGame.SHEAR, 0)
});
ItemsFlow.Padding = new MarginPadding
{
Top = 0,
Bottom = 7,
Horizontal = 7
};
}
}
[BackgroundDependencyLoader]
private void load(OsuColour colours, OsuConfigManager configManager)
{
AccentColour = colours.ForModType(ModType);
if (toggleAllCheckbox != null)
{
toggleAllCheckbox.AccentColour = AccentColour;
toggleAllCheckbox.AccentHoverColour = AccentColour.Lighten(0.3f);
}
hotkeyStyle = configManager.GetBindable<ModSelectHotkeyStyle>(OsuSetting.ModSelectHotkeyStyle);
}
protected override void LoadComplete()
{
base.LoadComplete();
toggleAllCheckbox?.Current.BindValueChanged(_ => updateToggleAllText(), true);
hotkeyStyle.BindValueChanged(val => hotkeyHandler = createHotkeyHandler(val.NewValue), true);
asyncLoadPanels();
}
private void updateToggleAllText()
{
Debug.Assert(toggleAllCheckbox != null);
toggleAllCheckbox.LabelText = toggleAllCheckbox.Current.Value ? CommonStrings.DeselectAll : CommonStrings.SelectAll;
}
private CancellationTokenSource? cancellationTokenSource;
private void asyncLoadPanels()
{
cancellationTokenSource?.Cancel();
var panels = availableMods.Select(mod => CreateModPanel(mod).With(panel => panel.Shear = Vector2.Zero)).ToArray();
latestLoadedPanels = panels;
latestLoadTask = LoadComponentsAsync(panels, loaded =>
{
ItemsFlow.ChildrenEnumerable = loaded;
updateState();
}, (cancellationTokenSource = new CancellationTokenSource()).Token);
}
private void updateState()
{
Alpha = availableMods.All(mod => !mod.Visible) ? 0 : 1;
if (toggleAllCheckbox != null && !SelectionAnimationRunning)
{
bool anyPanelsVisible = availableMods.Any(panel => panel.Visible);
toggleAllCheckbox.Alpha = anyPanelsVisible ? 1 : 0;
// checking `anyPanelsVisible` is important since `.All()` returns `true` for empty enumerables.
if (anyPanelsVisible)
toggleAllCheckbox.Current.Value = availableMods.Where(panel => panel.Visible).All(panel => panel.Active.Value);
}
}
#region Bulk select / deselect
private const double initial_multiple_selection_delay = 120;
private double selectionDelay = initial_multiple_selection_delay;
private double lastSelection;
private readonly Queue<Action> pendingSelectionOperations = new Queue<Action>();
internal bool SelectionAnimationRunning => pendingSelectionOperations.Count > 0;
protected override void Update()
{
base.Update();
// we override `IsPresent` to include the scheduler's pending task state to make async loads work correctly when columns are masked away
// (see description of https://github.com/ppy/osu/pull/19783).
// however, because of that we must also ensure that we signal correct invalidations (https://github.com/ppy/osu-framework/issues/5129).
// failing to do so causes columns to be stuck in "present" mode despite actually not being present themselves.
// this works because `Update()` will always run after a scheduler update, which is what causes the presence state change responsible for the failure.
if (wasPresent != null && wasPresent != IsPresent)
Invalidate(Invalidation.Presence);
wasPresent = IsPresent;
if (selectionDelay == initial_multiple_selection_delay || Time.Current - lastSelection >= selectionDelay)
{
if (pendingSelectionOperations.TryDequeue(out var dequeuedAction))
{
dequeuedAction();
// each time we play an animation, we decrease the time until the next animation (to ramp the visual and audible elements).
selectionDelay = Math.Max(ModSelectPanel.SAMPLE_PLAYBACK_DELAY, selectionDelay * 0.8f);
lastSelection = Time.Current;
}
else
{
// reset the selection delay after all animations have been completed.
// this will cause the next action to be immediately performed.
selectionDelay = initial_multiple_selection_delay;
}
}
}
/// <summary>
/// Selects all mods.
/// </summary>
public void SelectAll()
{
pendingSelectionOperations.Clear();
foreach (var button in availableMods.Where(b => !b.Active.Value && b.Visible))
pendingSelectionOperations.Enqueue(() => button.Active.Value = true);
}
/// <summary>
/// Deselects all mods.
/// </summary>
public void DeselectAll()
{
pendingSelectionOperations.Clear();
foreach (var button in availableMods.Where(b => b.Active.Value))
{
if (!button.Visible)
button.Active.Value = false;
else
pendingSelectionOperations.Enqueue(() => button.Active.Value = false);
}
}
/// <summary>
/// Run any delayed selections (due to animation) immediately to leave mods in a good (final) state.
/// </summary>
public void FlushPendingSelections()
{
while (pendingSelectionOperations.TryDequeue(out var dequeuedAction))
dequeuedAction();
}
private partial class ToggleAllCheckbox : OsuCheckbox
{
private Color4 accentColour;
public Color4 AccentColour
{
get => accentColour;
set
{
accentColour = value;
updateState();
}
}
private Color4 accentHoverColour;
public Color4 AccentHoverColour
{
get => accentHoverColour;
set
{
accentHoverColour = value;
updateState();
}
}
private readonly ModColumn column;
public ToggleAllCheckbox(ModColumn column)
: base(false)
{
this.column = column;
}
protected override void ApplyLabelParameters(SpriteText text)
{
base.ApplyLabelParameters(text);
text.Font = text.Font.With(weight: FontWeight.SemiBold);
}
[BackgroundDependencyLoader]
private void load()
{
updateState();
}
private void updateState()
{
Nub.AccentColour = AccentColour;
Nub.GlowingAccentColour = AccentHoverColour;
Nub.GlowColour = AccentHoverColour.Opacity(0.2f);
}
protected override void OnUserChange(bool value)
{
if (value)
column.SelectAll();
else
column.DeselectAll();
}
}
#endregion
#region Keyboard selection support
/// <summary>
/// Creates an appropriate <see cref="IModHotkeyHandler"/> for this column's <see cref="ModType"/> and
/// the supplied <paramref name="hotkeyStyle"/>.
/// </summary>
private IModHotkeyHandler createHotkeyHandler(ModSelectHotkeyStyle hotkeyStyle)
{
switch (ModType)
{
case ModType.DifficultyReduction:
case ModType.DifficultyIncrease:
case ModType.Automation:
return hotkeyStyle == ModSelectHotkeyStyle.Sequential
? SequentialModHotkeyHandler.Create(ModType)
: new ClassicModHotkeyHandler(allowIncompatibleSelection);
default:
return new NoopModHotkeyHandler();
}
}
protected override bool OnKeyDown(KeyDownEvent e)
{
if (e.ControlPressed || e.AltPressed || e.SuperPressed || e.Repeat)
return false;
return hotkeyHandler.HandleHotkeyPressed(e, availableMods);
}
#endregion
}
}