2022-02-19 16:52:16 +00: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.
2022-04-17 20:12:06 +00:00
#nullable enable
2022-02-19 16:52:16 +00:00
using System ;
using System.Collections.Generic ;
using System.Linq ;
using System.Threading ;
2022-02-28 20:36:13 +00:00
using System.Threading.Tasks ;
2022-02-19 16:52:16 +00:00
using Humanizer ;
using osu.Framework.Allocation ;
using osu.Framework.Bindables ;
2022-02-19 17:33:13 +00:00
using osu.Framework.Extensions.Color4Extensions ;
2022-04-17 20:12:06 +00:00
using osu.Framework.Extensions.IEnumerableExtensions ;
2022-02-19 16:52:16 +00:00
using osu.Framework.Graphics ;
using osu.Framework.Graphics.Colour ;
using osu.Framework.Graphics.Containers ;
using osu.Framework.Graphics.Shapes ;
2022-02-19 17:33:13 +00:00
using osu.Framework.Graphics.Sprites ;
2022-02-20 13:10:35 +00:00
using osu.Framework.Input.Events ;
2022-02-19 16:52:16 +00:00
using osu.Game.Graphics ;
using osu.Game.Graphics.Containers ;
2022-02-19 17:33:13 +00:00
using osu.Game.Graphics.UserInterface ;
2022-02-19 16:52:16 +00:00
using osu.Game.Rulesets.Mods ;
using osu.Game.Utils ;
using osuTK ;
2022-02-19 17:33:13 +00:00
using osuTK.Graphics ;
2022-02-20 13:10:35 +00:00
using osuTK.Input ;
2022-02-19 16:52:16 +00:00
namespace osu.Game.Overlays.Mods
{
public class ModColumn : CompositeDrawable
{
2022-04-05 09:27:34 +00:00
public readonly Container TopLevelContent ;
2022-03-27 22:16:10 +00:00
public readonly ModType ModType ;
2022-02-20 12:40:52 +00:00
private Func < Mod , bool > ? filter ;
2022-02-28 20:39:21 +00:00
/// <summary>
/// Function determining whether each mod in the column should be displayed.
/// A return value of <see langword="true"/> means that the mod is not filtered and therefore its corresponding panel should be displayed.
/// A return value of <see langword="false"/> means that the mod is filtered out and therefore its corresponding panel should be hidden.
/// </summary>
2022-02-20 12:40:52 +00:00
public Func < Mod , bool > ? Filter
{
get = > filter ;
set
{
filter = value ;
2022-05-06 19:35:36 +00:00
updateState ( ) ;
2022-02-20 12:40:52 +00:00
}
}
2022-04-24 17:13:19 +00:00
public Bindable < bool > Active = new BindableBool ( true ) ;
2022-05-03 19:44:44 +00:00
/// <summary>
/// List of mods marked as selected in this column.
/// </summary>
/// <remarks>
/// Note that the mod instances returned by this property are owned solely by this column
/// (as in, they are locally-managed clones, to ensure proper isolation from any other external instances).
/// </remarks>
public IReadOnlyList < Mod > SelectedMods { get ; private set ; } = Array . Empty < Mod > ( ) ;
/// <summary>
/// Invoked when a mod panel has been selected interactively by the user.
/// </summary>
public event Action ? SelectionChangedByUser ;
2022-04-24 17:13:19 +00:00
protected override bool ReceivePositionalInputAtSubTree ( Vector2 screenSpacePos ) = > base . ReceivePositionalInputAtSubTree ( screenSpacePos ) & & Active . Value ;
2022-03-26 21:43:17 +00:00
2022-03-27 20:55:52 +00:00
protected virtual ModPanel CreateModPanel ( Mod mod ) = > new ModPanel ( mod ) ;
2022-02-20 13:10:35 +00:00
private readonly Key [ ] ? toggleKeys ;
2022-02-19 16:52:16 +00:00
private readonly Bindable < Dictionary < ModType , IReadOnlyList < Mod > > > availableMods = new Bindable < Dictionary < ModType , IReadOnlyList < Mod > > > ( ) ;
2022-05-03 19:44:44 +00:00
/// <summary>
/// All mods that are available for the current ruleset in this particular column.
/// </summary>
/// <remarks>
/// Note that the mod instances in this list are owned solely by this column
/// (as in, they are locally-managed clones, to ensure proper isolation from any other external instances).
/// </remarks>
private IReadOnlyList < Mod > localAvailableMods = Array . Empty < Mod > ( ) ;
2022-02-19 16:52:16 +00:00
private readonly TextFlowContainer headerText ;
private readonly Box headerBackground ;
private readonly Container contentContainer ;
private readonly Box contentBackground ;
private readonly FillFlowContainer < ModPanel > panelFlow ;
2022-02-19 17:33:13 +00:00
private readonly ToggleAllCheckbox ? toggleAllCheckbox ;
2022-02-19 16:52:16 +00:00
private Colour4 accentColour ;
2022-02-28 20:36:13 +00:00
private Task ? latestLoadTask ;
internal bool ItemsLoaded = > latestLoadTask = = null ;
2022-02-27 22:08:31 +00:00
private const float header_height = 42 ;
2022-02-19 16:52:16 +00:00
2022-02-20 13:10:35 +00:00
public ModColumn ( ModType modType , bool allowBulkSelection , Key [ ] ? toggleKeys = null )
2022-02-19 16:52:16 +00:00
{
2022-03-27 22:16:10 +00:00
ModType = modType ;
2022-02-20 13:10:35 +00:00
this . toggleKeys = toggleKeys ;
2022-02-19 16:52:16 +00:00
2022-02-27 22:08:31 +00:00
Width = 320 ;
2022-02-19 16:52:16 +00:00
RelativeSizeAxes = Axes . Y ;
2022-04-20 07:30:58 +00:00
Shear = new Vector2 ( ShearedOverlayContainer . SHEAR , 0 ) ;
2022-02-19 16:52:16 +00:00
2022-02-19 17:33:13 +00:00
Container controlContainer ;
2022-02-19 16:52:16 +00:00
InternalChildren = new Drawable [ ]
{
2022-04-05 09:27:34 +00:00
TopLevelContent = new Container
2022-02-19 16:52:16 +00:00
{
2022-04-05 09:27:34 +00:00
RelativeSizeAxes = Axes . Both ,
CornerRadius = ModPanel . CORNER_RADIUS ,
Masking = true ,
2022-02-19 16:52:16 +00:00
Children = new Drawable [ ]
{
2022-04-05 09:27:34 +00:00
new Container
2022-02-19 16:52:16 +00:00
{
RelativeSizeAxes = Axes . X ,
2022-04-05 09:27:34 +00:00
Height = header_height + ModPanel . CORNER_RADIUS ,
Children = new Drawable [ ]
2022-02-19 16:52:16 +00:00
{
2022-04-05 09:27:34 +00:00
headerBackground = new Box
{
RelativeSizeAxes = Axes . X ,
Height = header_height + ModPanel . CORNER_RADIUS
} ,
headerText = new OsuTextFlowContainer ( t = >
{
t . Font = OsuFont . TorusAlternate . With ( size : 17 ) ;
t . Shadow = false ;
t . Colour = Colour4 . Black ;
} )
{
RelativeSizeAxes = Axes . X ,
AutoSizeAxes = Axes . Y ,
Anchor = Anchor . CentreLeft ,
Origin = Anchor . CentreLeft ,
2022-04-20 07:30:58 +00:00
Shear = new Vector2 ( - ShearedOverlayContainer . SHEAR , 0 ) ,
2022-04-05 09:27:34 +00:00
Padding = new MarginPadding
{
Horizontal = 17 ,
Bottom = ModPanel . CORNER_RADIUS
}
}
2022-02-19 16:52:16 +00:00
}
2022-04-05 09:27:34 +00:00
} ,
new Container
2022-02-19 16:52:16 +00:00
{
2022-04-05 09:27:34 +00:00
RelativeSizeAxes = Axes . Both ,
Padding = new MarginPadding { Top = header_height } ,
Child = contentContainer = new Container
2022-02-19 16:52:16 +00:00
{
RelativeSizeAxes = Axes . Both ,
2022-04-05 09:27:34 +00:00
Masking = true ,
CornerRadius = ModPanel . CORNER_RADIUS ,
BorderThickness = 3 ,
Children = new Drawable [ ]
2022-02-19 16:52:16 +00:00
{
2022-04-05 09:27:34 +00:00
contentBackground = new Box
2022-02-19 16:52:16 +00:00
{
2022-04-05 09:27:34 +00:00
RelativeSizeAxes = Axes . Both
2022-02-19 16:52:16 +00:00
} ,
2022-04-05 09:27:34 +00:00
new GridContainer
2022-02-19 16:52:16 +00:00
{
2022-04-05 09:27:34 +00:00
RelativeSizeAxes = Axes . Both ,
RowDimensions = new [ ]
{
new Dimension ( GridSizeMode . AutoSize ) ,
new Dimension ( )
} ,
Content = new [ ]
2022-02-19 16:52:16 +00:00
{
2022-04-05 09:27:34 +00:00
new Drawable [ ]
{
controlContainer = new Container
{
RelativeSizeAxes = Axes . X ,
Padding = new MarginPadding { Horizontal = 14 }
}
} ,
new Drawable [ ]
2022-02-19 16:52:16 +00:00
{
2022-04-05 09:27:34 +00:00
new NestedVerticalScrollContainer
{
RelativeSizeAxes = Axes . Both ,
ClampExtension = 100 ,
ScrollbarOverlapsContent = false ,
Child = panelFlow = new FillFlowContainer < ModPanel >
{
RelativeSizeAxes = Axes . X ,
AutoSizeAxes = Axes . Y ,
Spacing = new Vector2 ( 0 , 7 ) ,
Padding = new MarginPadding ( 7 )
}
}
2022-02-19 16:52:16 +00:00
}
}
2022-02-20 11:18:59 +00:00
}
2022-02-19 16:52:16 +00:00
}
}
}
}
}
} ;
createHeaderText ( ) ;
2022-02-19 17:33:13 +00:00
if ( allowBulkSelection )
{
2022-02-27 22:08:31 +00:00
controlContainer . Height = 35 ;
2022-02-19 17:45:04 +00:00
controlContainer . Add ( toggleAllCheckbox = new ToggleAllCheckbox ( this )
2022-02-19 17:33:13 +00:00
{
Anchor = Anchor . CentreLeft ,
Origin = Anchor . CentreLeft ,
2022-02-27 22:08:31 +00:00
Scale = new Vector2 ( 0.8f ) ,
2022-02-19 17:33:13 +00:00
RelativeSizeAxes = Axes . X ,
LabelText = "Enable All" ,
2022-04-20 07:30:58 +00:00
Shear = new Vector2 ( - ShearedOverlayContainer . SHEAR , 0 )
2022-02-19 17:33:13 +00:00
} ) ;
2022-02-20 11:18:59 +00:00
panelFlow . Padding = new MarginPadding
{
Top = 0 ,
2022-02-27 22:08:31 +00:00
Bottom = 7 ,
Horizontal = 7
2022-02-20 11:18:59 +00:00
} ;
2022-02-19 17:33:13 +00:00
}
2022-02-19 16:52:16 +00:00
}
private void createHeaderText ( )
{
2022-03-27 22:16:10 +00:00
IEnumerable < string > headerTextWords = ModType . Humanize ( LetterCasing . Title ) . Split ( ' ' ) ;
2022-02-19 16:52:16 +00:00
if ( headerTextWords . Count ( ) > 1 )
{
headerText . AddText ( $"{headerTextWords.First()} " , t = > t . Font = t . Font . With ( weight : FontWeight . SemiBold ) ) ;
headerTextWords = headerTextWords . Skip ( 1 ) ;
}
headerText . AddText ( string . Join ( ' ' , headerTextWords ) ) ;
}
[BackgroundDependencyLoader]
private void load ( OsuGameBase game , OverlayColourProvider colourProvider , OsuColour colours )
{
availableMods . BindTo ( game . AvailableMods ) ;
2022-05-03 19:44:44 +00:00
// this `BindValueChanged` callback is intentionally here, to ensure that local available mods are constructed as early as possible.
// this is needed to make sure no external changes to mods are dropped while mod panels are asynchronously loading.
availableMods . BindValueChanged ( _ = > updateLocalAvailableMods ( ) , true ) ;
2022-02-19 16:52:16 +00:00
2022-03-27 22:16:10 +00:00
headerBackground . Colour = accentColour = colours . ForModType ( ModType ) ;
2022-02-19 16:52:16 +00:00
2022-02-19 17:33:13 +00:00
if ( toggleAllCheckbox ! = null )
{
toggleAllCheckbox . AccentColour = accentColour ;
toggleAllCheckbox . AccentHoverColour = accentColour . Lighten ( 0.3f ) ;
}
2022-02-19 16:52:16 +00:00
contentContainer . BorderColour = ColourInfo . GradientVertical ( colourProvider . Background4 , colourProvider . Background3 ) ;
contentBackground . Colour = colourProvider . Background4 ;
}
2022-05-03 19:44:44 +00:00
private void updateLocalAvailableMods ( )
2022-02-19 16:52:16 +00:00
{
2022-04-17 20:12:06 +00:00
var newMods = ModUtils . FlattenMods ( availableMods . Value . GetValueOrDefault ( ModType ) ? ? Array . Empty < Mod > ( ) )
. Select ( m = > m . DeepClone ( ) )
. ToList ( ) ;
2022-02-19 16:52:16 +00:00
2022-05-03 19:44:44 +00:00
if ( newMods . SequenceEqual ( localAvailableMods ) )
2022-02-19 16:52:16 +00:00
return ;
2022-05-03 19:44:44 +00:00
localAvailableMods = newMods ;
2022-05-06 15:33:32 +00:00
if ( ! IsLoaded )
// if we're coming from BDL, perform the first load synchronously to make sure everything is in place as early as possible.
onPanelsLoaded ( createPanels ( ) ) ;
else
asyncLoadPanels ( ) ;
2022-05-03 19:44:44 +00:00
}
private CancellationTokenSource ? cancellationTokenSource ;
2022-05-06 15:33:32 +00:00
private void asyncLoadPanels ( )
2022-05-03 19:44:44 +00:00
{
2022-02-19 16:52:16 +00:00
cancellationTokenSource ? . Cancel ( ) ;
2022-05-06 15:33:32 +00:00
var panels = createPanels ( ) ;
2022-02-19 16:52:16 +00:00
2022-02-28 20:36:13 +00:00
Task ? loadTask ;
2022-05-06 15:33:32 +00:00
latestLoadTask = loadTask = LoadComponentsAsync ( panels , onPanelsLoaded , ( cancellationTokenSource = new CancellationTokenSource ( ) ) . Token ) ;
2022-02-28 20:36:13 +00:00
loadTask . ContinueWith ( _ = >
{
if ( loadTask = = latestLoadTask )
latestLoadTask = null ;
} ) ;
2022-02-19 16:52:16 +00:00
}
2022-02-19 17:33:13 +00:00
2022-05-06 15:33:32 +00:00
private IEnumerable < ModPanel > createPanels ( )
{
var panels = localAvailableMods . Select ( mod = > CreateModPanel ( mod ) . With ( panel = > panel . Shear = new Vector2 ( - ShearedOverlayContainer . SHEAR , 0 ) ) ) ;
return panels ;
}
private void onPanelsLoaded ( IEnumerable < ModPanel > loaded )
{
panelFlow . ChildrenEnumerable = loaded ;
updateState ( ) ;
foreach ( var panel in panelFlow )
{
panel . Active . BindValueChanged ( _ = > panelStateChanged ( panel ) ) ;
}
}
2022-05-06 19:35:36 +00:00
private void updateState ( )
2022-03-27 22:16:10 +00:00
{
foreach ( var panel in panelFlow )
2022-05-06 19:35:36 +00:00
{
2022-05-03 19:44:44 +00:00
panel . Active . Value = SelectedMods . Contains ( panel . Mod ) ;
2022-05-06 19:35:36 +00:00
panel . ApplyFilter ( Filter ) ;
}
if ( toggleAllCheckbox ! = null & & ! SelectionAnimationRunning )
{
toggleAllCheckbox . Alpha = panelFlow . Any ( panel = > ! panel . Filtered . Value ) ? 1 : 0 ;
toggleAllCheckbox . Current . Value = panelFlow . Where ( panel = > ! panel . Filtered . Value ) . All ( panel = > panel . Active . Value ) ;
}
2022-05-03 19:44:44 +00:00
}
/// <summary>
/// This flag helps to determine the source of changes to <see cref="SelectedMods"/>.
/// If the value is false, then <see cref="SelectedMods"/> are changing due to a user selection on the UI.
/// If the value is true, then <see cref="SelectedMods"/> are changing due to an external <see cref="SetSelection"/> call.
/// </summary>
private bool externalSelectionUpdateInProgress ;
private void panelStateChanged ( ModPanel panel )
{
2022-05-06 19:46:56 +00:00
if ( externalSelectionUpdateInProgress )
return ;
2022-05-03 19:44:44 +00:00
var newSelectedMods = panel . Active . Value
? SelectedMods . Append ( panel . Mod )
: SelectedMods . Except ( panel . Mod . Yield ( ) ) ;
SelectedMods = newSelectedMods . ToArray ( ) ;
2022-05-06 19:35:36 +00:00
updateState ( ) ;
2022-05-06 19:46:56 +00:00
SelectionChangedByUser ? . Invoke ( ) ;
2022-05-03 19:44:44 +00:00
}
/// <summary>
/// Adjusts the set of selected mods in this column to match the passed in <paramref name="mods"/>.
/// </summary>
/// <remarks>
/// This method exists to be able to receive mod instances that come from potentially-external sources and to copy the changes across to this column's state.
/// <see cref="ModSelectScreen"/> uses this to substitute any external mod references in <see cref="ModSelectScreen.SelectedMods"/>
/// to references that are owned by this column.
/// </remarks>
internal void SetSelection ( IReadOnlyList < Mod > mods )
{
externalSelectionUpdateInProgress = true ;
var newSelection = new List < Mod > ( ) ;
foreach ( var mod in localAvailableMods )
2022-04-17 20:12:06 +00:00
{
2022-05-03 19:44:44 +00:00
var matchingSelectedMod = mods . SingleOrDefault ( selected = > selected . GetType ( ) = = mod . GetType ( ) ) ;
if ( matchingSelectedMod ! = null )
{
mod . CopyFrom ( matchingSelectedMod ) ;
newSelection . Add ( mod ) ;
}
2022-04-17 20:14:28 +00:00
else
2022-05-03 19:44:44 +00:00
{
mod . ResetSettingsToDefaults ( ) ;
}
2022-04-17 20:12:06 +00:00
}
2022-05-03 19:44:44 +00:00
SelectedMods = newSelection ;
2022-05-06 19:35:36 +00:00
updateState ( ) ;
2022-05-03 19:44:44 +00:00
externalSelectionUpdateInProgress = false ;
2022-03-27 22:16:10 +00:00
}
2022-02-19 17:45:04 +00: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-06 16:08:11 +00:00
internal bool SelectionAnimationRunning = > pendingSelectionOperations . Count > 0 ;
2022-02-20 12:40:52 +00:00
2022-02-19 17:45:04 +00:00
protected override void Update ( )
{
base . Update ( ) ;
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 ( 30 , 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 ( ) ;
2022-02-20 12:40:52 +00:00
foreach ( var button in panelFlow . Where ( b = > ! b . Active . Value & & ! b . Filtered . Value ) )
2022-02-19 17:45:04 +00:00
pendingSelectionOperations . Enqueue ( ( ) = > button . Active . Value = true ) ;
}
/// <summary>
/// Deselects all mods.
/// </summary>
public void DeselectAll ( )
{
pendingSelectionOperations . Clear ( ) ;
2022-02-20 12:40:52 +00:00
foreach ( var button in panelFlow . Where ( b = > b . Active . Value & & ! b . Filtered . Value ) )
2022-02-19 17:45:04 +00:00
pendingSelectionOperations . Enqueue ( ( ) = > button . Active . Value = false ) ;
}
2022-04-17 18:32:45 +00:00
/// <summary>
2022-05-04 10:40:08 +00:00
/// Run any delayed selections (due to animation) immediately to leave mods in a good (final) state.
2022-04-17 18:32:45 +00:00
/// </summary>
2022-05-04 10:40:08 +00:00
public void FlushPendingSelections ( )
2022-04-17 18:32:45 +00:00
{
while ( pendingSelectionOperations . TryDequeue ( out var dequeuedAction ) )
dequeuedAction ( ) ;
}
2022-02-19 17:33:13 +00:00
private 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-19 17:45:04 +00:00
private readonly ModColumn column ;
public ToggleAllCheckbox ( ModColumn column )
2022-02-19 17:33:13 +00:00
: base ( false )
{
2022-02-19 17:45:04 +00:00
this . column = column ;
2022-02-19 17:33:13 +00: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-19 17:45:04 +00:00
protected override void OnUserChange ( bool value )
{
if ( value )
column . SelectAll ( ) ;
else
column . DeselectAll ( ) ;
}
2022-02-19 17:33:13 +00:00
}
2022-02-19 17:45:04 +00:00
#endregion
2022-02-20 12:40:52 +00:00
2022-02-20 13:10:35 +00:00
#region Keyboard selection support
protected override bool OnKeyDown ( KeyDownEvent e )
{
2022-04-27 07:55:15 +00:00
if ( e . ControlPressed | | e . AltPressed | | e . SuperPressed ) return false ;
2022-02-20 13:10:35 +00:00
if ( toggleKeys = = null ) return false ;
int index = Array . IndexOf ( toggleKeys , e . Key ) ;
if ( index < 0 ) return false ;
var panel = panelFlow . ElementAtOrDefault ( index ) ;
if ( panel = = null | | panel . Filtered . Value ) return false ;
panel . Active . Toggle ( ) ;
return true ;
}
#endregion
2022-02-19 16:52:16 +00:00
}
}