2022-02-20 00:52:16 +08:00
|
|
|
// 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;
|
2022-05-07 16:56:03 +08:00
|
|
|
using System.Diagnostics;
|
2022-02-20 00:52:16 +08:00
|
|
|
using System.Linq;
|
|
|
|
using System.Threading;
|
2022-03-01 04:36:13 +08:00
|
|
|
using System.Threading.Tasks;
|
2022-02-20 00:52:16 +08:00
|
|
|
using Humanizer;
|
|
|
|
using osu.Framework.Allocation;
|
|
|
|
using osu.Framework.Bindables;
|
2022-02-20 01:33:13 +08:00
|
|
|
using osu.Framework.Extensions.Color4Extensions;
|
2022-02-20 00:52:16 +08:00
|
|
|
using osu.Framework.Graphics;
|
2022-02-20 01:33:13 +08:00
|
|
|
using osu.Framework.Graphics.Sprites;
|
2022-02-20 21:10:35 +08:00
|
|
|
using osu.Framework.Input.Events;
|
2022-06-21 19:10:22 +08:00
|
|
|
using osu.Game.Configuration;
|
2022-02-20 00:52:16 +08:00
|
|
|
using osu.Game.Graphics;
|
2022-02-20 01:33:13 +08:00
|
|
|
using osu.Game.Graphics.UserInterface;
|
2022-05-07 16:56:03 +08:00
|
|
|
using osu.Game.Localisation;
|
2022-06-21 18:49:01 +08:00
|
|
|
using osu.Game.Overlays.Mods.Input;
|
2022-02-20 00:52:16 +08:00
|
|
|
using osu.Game.Rulesets.Mods;
|
|
|
|
using osuTK;
|
2022-02-20 01:33:13 +08:00
|
|
|
using osuTK.Graphics;
|
2022-02-20 00:52:16 +08:00
|
|
|
|
|
|
|
namespace osu.Game.Overlays.Mods
|
|
|
|
{
|
2022-07-23 02:01:22 +08:00
|
|
|
public partial class ModColumn : ModSelectColumn
|
2022-02-20 00:52:16 +08:00
|
|
|
{
|
2022-03-28 06:16:10 +08:00
|
|
|
public readonly ModType ModType;
|
|
|
|
|
2022-05-12 01:02:45 +08:00
|
|
|
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;
|
2022-05-12 02:23:56 +08:00
|
|
|
|
2022-05-12 02:28:11 +08:00
|
|
|
foreach (var mod in availableMods)
|
|
|
|
{
|
|
|
|
mod.Active.BindValueChanged(_ => updateState());
|
2023-06-18 19:49:29 +08:00
|
|
|
mod.MatchingTextFilter.BindValueChanged(_ => updateState());
|
2023-05-06 03:41:30 +08:00
|
|
|
mod.ValidForSelection.BindValueChanged(_ => updateState());
|
2022-05-12 02:28:11 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
updateState();
|
|
|
|
|
2022-05-12 02:23:56 +08:00
|
|
|
if (IsLoaded)
|
|
|
|
asyncLoadPanels();
|
2022-05-12 01:02:45 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-12 00:37:31 +08:00
|
|
|
protected virtual ModPanel CreateModPanel(ModState mod) => new ModPanel(mod);
|
2022-03-28 04:55:52 +08:00
|
|
|
|
2022-06-21 20:48:41 +08:00
|
|
|
private readonly bool allowIncompatibleSelection;
|
2022-02-20 00:52:16 +08:00
|
|
|
|
2022-02-20 01:33:13 +08:00
|
|
|
private readonly ToggleAllCheckbox? toggleAllCheckbox;
|
2022-02-20 00:52:16 +08:00
|
|
|
|
2022-06-21 20:48:41 +08:00
|
|
|
private Bindable<ModSelectHotkeyStyle> hotkeyStyle = null!;
|
|
|
|
private IModHotkeyHandler hotkeyHandler = null!;
|
|
|
|
|
2022-03-01 04:36:13 +08:00
|
|
|
private Task? latestLoadTask;
|
2024-02-25 03:44:58 +08:00
|
|
|
private ModPanel[]? latestLoadedPanels;
|
|
|
|
internal bool ItemsLoaded => latestLoadTask?.IsCompleted == true && allPanelsLoaded;
|
2024-04-23 19:36:12 +08:00
|
|
|
private bool? wasPresent;
|
2024-02-25 03:44:58 +08:00
|
|
|
|
|
|
|
private bool allPanelsLoaded
|
|
|
|
{
|
|
|
|
get
|
|
|
|
{
|
|
|
|
if (latestLoadedPanels == null)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
foreach (var panel in latestLoadedPanels)
|
|
|
|
{
|
|
|
|
if (panel.Parent == null)
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
2022-08-16 02:34:09 +08:00
|
|
|
|
|
|
|
public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks;
|
2022-03-01 04:36:13 +08:00
|
|
|
|
2022-06-21 20:48:41 +08:00
|
|
|
public ModColumn(ModType modType, bool allowIncompatibleSelection)
|
2022-02-20 00:52:16 +08:00
|
|
|
{
|
2022-03-28 06:16:10 +08:00
|
|
|
ModType = modType;
|
2022-06-21 20:48:41 +08:00
|
|
|
this.allowIncompatibleSelection = allowIncompatibleSelection;
|
2022-02-20 00:52:16 +08:00
|
|
|
|
2022-07-23 02:01:22 +08:00
|
|
|
HeaderText = ModType.Humanize(LetterCasing.Title);
|
2022-02-20 01:33:13 +08:00
|
|
|
|
2022-06-21 20:48:41 +08:00
|
|
|
if (allowIncompatibleSelection)
|
2022-02-20 01:33:13 +08:00
|
|
|
{
|
2022-07-23 02:01:22 +08:00
|
|
|
ControlContainer.Height = 35;
|
|
|
|
ControlContainer.Add(toggleAllCheckbox = new ToggleAllCheckbox(this)
|
2022-02-20 01:33:13 +08:00
|
|
|
{
|
|
|
|
Anchor = Anchor.CentreLeft,
|
|
|
|
Origin = Anchor.CentreLeft,
|
2022-02-28 06:08:31 +08:00
|
|
|
Scale = new Vector2(0.8f),
|
2022-02-20 01:33:13 +08:00
|
|
|
RelativeSizeAxes = Axes.X,
|
2024-05-16 12:27:54 +08:00
|
|
|
Shear = new Vector2(-OsuGame.SHEAR, 0)
|
2022-02-20 01:33:13 +08:00
|
|
|
});
|
2022-07-23 02:01:22 +08:00
|
|
|
ItemsFlow.Padding = new MarginPadding
|
2022-02-20 19:18:59 +08:00
|
|
|
{
|
|
|
|
Top = 0,
|
2022-02-28 06:08:31 +08:00
|
|
|
Bottom = 7,
|
|
|
|
Horizontal = 7
|
2022-02-20 19:18:59 +08:00
|
|
|
};
|
2022-02-20 01:33:13 +08:00
|
|
|
}
|
2022-02-20 00:52:16 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
[BackgroundDependencyLoader]
|
2022-07-23 02:01:22 +08:00
|
|
|
private void load(OsuColour colours, OsuConfigManager configManager)
|
2022-02-20 00:52:16 +08:00
|
|
|
{
|
2022-07-23 02:01:22 +08:00
|
|
|
AccentColour = colours.ForModType(ModType);
|
2022-02-20 00:52:16 +08:00
|
|
|
|
2022-02-20 01:33:13 +08:00
|
|
|
if (toggleAllCheckbox != null)
|
|
|
|
{
|
2022-07-23 02:01:22 +08:00
|
|
|
toggleAllCheckbox.AccentColour = AccentColour;
|
|
|
|
toggleAllCheckbox.AccentHoverColour = AccentColour.Lighten(0.3f);
|
2022-02-20 01:33:13 +08:00
|
|
|
}
|
|
|
|
|
2022-06-21 19:10:22 +08:00
|
|
|
hotkeyStyle = configManager.GetBindable<ModSelectHotkeyStyle>(OsuSetting.ModSelectHotkeyStyle);
|
2022-02-20 00:52:16 +08:00
|
|
|
}
|
|
|
|
|
2022-05-07 16:56:03 +08:00
|
|
|
protected override void LoadComplete()
|
|
|
|
{
|
|
|
|
base.LoadComplete();
|
|
|
|
|
|
|
|
toggleAllCheckbox?.Current.BindValueChanged(_ => updateToggleAllText(), true);
|
2022-06-21 22:43:04 +08:00
|
|
|
hotkeyStyle.BindValueChanged(val => hotkeyHandler = createHotkeyHandler(val.NewValue), true);
|
2022-05-12 02:23:56 +08:00
|
|
|
asyncLoadPanels();
|
2022-05-07 16:56:03 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
private void updateToggleAllText()
|
|
|
|
{
|
|
|
|
Debug.Assert(toggleAllCheckbox != null);
|
|
|
|
toggleAllCheckbox.LabelText = toggleAllCheckbox.Current.Value ? CommonStrings.DeselectAll : CommonStrings.SelectAll;
|
|
|
|
}
|
|
|
|
|
2022-05-04 03:44:44 +08:00
|
|
|
private CancellationTokenSource? cancellationTokenSource;
|
|
|
|
|
2022-05-06 23:33:32 +08:00
|
|
|
private void asyncLoadPanels()
|
2022-05-04 03:44:44 +08:00
|
|
|
{
|
2022-02-20 00:52:16 +08:00
|
|
|
cancellationTokenSource?.Cancel();
|
|
|
|
|
2022-08-18 03:52:55 +08:00
|
|
|
var panels = availableMods.Select(mod => CreateModPanel(mod).With(panel => panel.Shear = Vector2.Zero)).ToArray();
|
|
|
|
latestLoadedPanels = panels;
|
2022-02-20 00:52:16 +08:00
|
|
|
|
2022-08-09 01:20:50 +08:00
|
|
|
latestLoadTask = LoadComponentsAsync(panels, loaded =>
|
2022-05-12 01:30:06 +08:00
|
|
|
{
|
2022-07-23 02:01:22 +08:00
|
|
|
ItemsFlow.ChildrenEnumerable = loaded;
|
2022-05-12 01:30:06 +08:00
|
|
|
updateState();
|
|
|
|
}, (cancellationTokenSource = new CancellationTokenSource()).Token);
|
2022-02-20 00:52:16 +08:00
|
|
|
}
|
2022-02-20 01:33:13 +08:00
|
|
|
|
2022-05-07 03:35:36 +08:00
|
|
|
private void updateState()
|
2022-03-28 06:16:10 +08:00
|
|
|
{
|
2023-06-02 16:33:38 +08:00
|
|
|
Alpha = availableMods.All(mod => !mod.Visible) ? 0 : 1;
|
2022-05-08 20:44:54 +08:00
|
|
|
|
2022-05-07 03:35:36 +08:00
|
|
|
if (toggleAllCheckbox != null && !SelectionAnimationRunning)
|
|
|
|
{
|
2023-06-18 20:03:26 +08:00
|
|
|
bool anyPanelsVisible = availableMods.Any(panel => panel.Visible);
|
2023-05-29 19:22:40 +08:00
|
|
|
|
2023-06-18 20:03:26 +08:00
|
|
|
toggleAllCheckbox.Alpha = anyPanelsVisible ? 1 : 0;
|
|
|
|
|
|
|
|
// checking `anyPanelsVisible` is important since `.All()` returns `true` for empty enumerables.
|
|
|
|
if (anyPanelsVisible)
|
2023-06-02 16:33:38 +08:00
|
|
|
toggleAllCheckbox.Current.Value = availableMods.Where(panel => panel.Visible).All(panel => panel.Active.Value);
|
2022-05-07 03:35:36 +08:00
|
|
|
}
|
2022-05-04 03:44:44 +08:00
|
|
|
}
|
|
|
|
|
2022-02-20 01:45:04 +08:00
|
|
|
#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>();
|
|
|
|
|
2022-05-07 00:08:11 +08:00
|
|
|
internal bool SelectionAnimationRunning => pendingSelectionOperations.Count > 0;
|
2022-02-20 20:40:52 +08:00
|
|
|
|
2022-02-20 01:45:04 +08:00
|
|
|
protected override void Update()
|
|
|
|
{
|
|
|
|
base.Update();
|
|
|
|
|
2024-04-23 19:36:12 +08:00
|
|
|
// 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;
|
|
|
|
|
2022-02-20 01:45:04 +08:00
|
|
|
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).
|
2023-06-10 18:55:37 +08:00
|
|
|
selectionDelay = Math.Max(ModSelectPanel.SAMPLE_PLAYBACK_DELAY, selectionDelay * 0.8f);
|
2022-02-20 01:45:04 +08:00
|
|
|
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();
|
|
|
|
|
2023-06-02 16:33:38 +08:00
|
|
|
foreach (var button in availableMods.Where(b => !b.Active.Value && b.Visible))
|
2022-02-20 01:45:04 +08:00
|
|
|
pendingSelectionOperations.Enqueue(() => button.Active.Value = true);
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Deselects all mods.
|
|
|
|
/// </summary>
|
|
|
|
public void DeselectAll()
|
|
|
|
{
|
|
|
|
pendingSelectionOperations.Clear();
|
|
|
|
|
2023-05-05 14:31:27 +08:00
|
|
|
foreach (var button in availableMods.Where(b => b.Active.Value))
|
2023-05-07 20:34:01 +08:00
|
|
|
{
|
2023-06-09 21:55:19 +08:00
|
|
|
if (!button.Visible)
|
2023-06-18 20:04:00 +08:00
|
|
|
button.Active.Value = false;
|
2023-05-07 20:34:01 +08:00
|
|
|
else
|
|
|
|
pendingSelectionOperations.Enqueue(() => button.Active.Value = false);
|
|
|
|
}
|
2022-02-20 01:45:04 +08:00
|
|
|
}
|
|
|
|
|
2022-04-18 02:32:45 +08:00
|
|
|
/// <summary>
|
2022-05-04 18:40:08 +08:00
|
|
|
/// Run any delayed selections (due to animation) immediately to leave mods in a good (final) state.
|
2022-04-18 02:32:45 +08:00
|
|
|
/// </summary>
|
2022-05-04 18:40:08 +08:00
|
|
|
public void FlushPendingSelections()
|
2022-04-18 02:32:45 +08:00
|
|
|
{
|
|
|
|
while (pendingSelectionOperations.TryDequeue(out var dequeuedAction))
|
|
|
|
dequeuedAction();
|
|
|
|
}
|
|
|
|
|
2022-02-20 01:33:13 +08:00
|
|
|
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();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-20 01:45:04 +08:00
|
|
|
private readonly ModColumn column;
|
|
|
|
|
|
|
|
public ToggleAllCheckbox(ModColumn column)
|
2022-02-20 01:33:13 +08:00
|
|
|
: base(false)
|
|
|
|
{
|
2022-02-20 01:45:04 +08:00
|
|
|
this.column = column;
|
2022-02-20 01:33:13 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
2022-02-20 01:45:04 +08:00
|
|
|
|
|
|
|
protected override void OnUserChange(bool value)
|
|
|
|
{
|
|
|
|
if (value)
|
|
|
|
column.SelectAll();
|
|
|
|
else
|
|
|
|
column.DeselectAll();
|
|
|
|
}
|
2022-02-20 01:33:13 +08:00
|
|
|
}
|
2022-02-20 01:45:04 +08:00
|
|
|
|
|
|
|
#endregion
|
2022-02-20 20:40:52 +08:00
|
|
|
|
2022-02-20 21:10:35 +08:00
|
|
|
#region Keyboard selection support
|
|
|
|
|
2022-06-21 20:48:41 +08:00
|
|
|
/// <summary>
|
|
|
|
/// Creates an appropriate <see cref="IModHotkeyHandler"/> for this column's <see cref="ModType"/> and
|
|
|
|
/// the supplied <paramref name="hotkeyStyle"/>.
|
|
|
|
/// </summary>
|
2022-06-21 22:43:04 +08:00
|
|
|
private IModHotkeyHandler createHotkeyHandler(ModSelectHotkeyStyle hotkeyStyle)
|
2022-06-21 20:48:41 +08:00
|
|
|
{
|
|
|
|
switch (ModType)
|
|
|
|
{
|
|
|
|
case ModType.DifficultyReduction:
|
|
|
|
case ModType.DifficultyIncrease:
|
|
|
|
case ModType.Automation:
|
|
|
|
return hotkeyStyle == ModSelectHotkeyStyle.Sequential
|
2022-06-24 13:48:43 +08:00
|
|
|
? SequentialModHotkeyHandler.Create(ModType)
|
2022-06-21 20:48:41 +08:00
|
|
|
: new ClassicModHotkeyHandler(allowIncompatibleSelection);
|
|
|
|
|
|
|
|
default:
|
|
|
|
return new NoopModHotkeyHandler();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-20 21:10:35 +08:00
|
|
|
protected override bool OnKeyDown(KeyDownEvent e)
|
|
|
|
{
|
2022-06-21 18:49:01 +08:00
|
|
|
if (e.ControlPressed || e.AltPressed || e.SuperPressed || e.Repeat)
|
|
|
|
return false;
|
2022-02-20 21:10:35 +08:00
|
|
|
|
2022-06-21 19:37:17 +08:00
|
|
|
return hotkeyHandler.HandleHotkeyPressed(e, availableMods);
|
2022-02-20 21:10:35 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
#endregion
|
2022-02-20 00:52:16 +08:00
|
|
|
}
|
|
|
|
}
|