mirror of
https://github.com/ppy/osu.git
synced 2024-12-17 11:12:55 +08:00
310b4d90cc
Discussed in [discord](https://discord.com/channels/188630481301012481/188630652340404224/1240490608934653984).
357 lines
12 KiB
C#
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
|
|
}
|
|
}
|