2019-01-24 08:43:03 +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.
2018-04-13 09:19:50 +00:00
2018-11-20 07:51:59 +00:00
using osuTK ;
2018-04-13 09:19:50 +00:00
using osu.Framework.Graphics ;
using osu.Framework.Graphics.Containers ;
using System ;
using System.Collections.Generic ;
using System.Linq ;
using osu.Game.Configuration ;
2018-11-20 07:51:59 +00:00
using osuTK.Input ;
2020-01-09 04:43:44 +00:00
using osu.Framework.Utils ;
2018-04-13 09:19:50 +00:00
using System.Diagnostics ;
using osu.Framework.Allocation ;
2019-02-21 10:04:31 +00:00
using osu.Framework.Bindables ;
2018-04-13 09:19:50 +00:00
using osu.Framework.Caching ;
using osu.Framework.Threading ;
using osu.Framework.Extensions.IEnumerableExtensions ;
2020-03-02 09:55:28 +00:00
using osu.Framework.Input.Bindings ;
2018-10-02 03:02:47 +00:00
using osu.Framework.Input.Events ;
2018-04-13 09:19:50 +00:00
using osu.Game.Beatmaps ;
2020-06-25 12:33:02 +00:00
using osu.Game.Extensions ;
2018-04-13 09:19:50 +00:00
using osu.Game.Graphics.Containers ;
using osu.Game.Graphics.Cursor ;
2020-03-02 09:55:28 +00:00
using osu.Game.Input.Bindings ;
2018-04-13 09:19:50 +00:00
using osu.Game.Screens.Select.Carousel ;
namespace osu.Game.Screens.Select
{
2020-03-02 09:55:28 +00:00
public class BeatmapCarousel : CompositeDrawable , IKeyBindingHandler < GlobalAction >
2018-04-13 09:19:50 +00:00
{
2020-04-20 19:42:43 +00:00
/// <summary>
/// Height of the area above the carousel that should be treated as visible due to transparency of elements in front of it.
/// </summary>
2020-04-20 19:43:07 +00:00
public float BleedTop { get ; set ; }
2020-04-20 19:42:43 +00:00
/// <summary>
/// Height of the area below the carousel that should be treated as visible due to transparency of elements in front of it.
/// </summary>
2020-04-20 19:43:07 +00:00
public float BleedBottom { get ; set ; }
2019-07-26 04:07:28 +00:00
2018-04-13 09:19:50 +00:00
/// <summary>
/// Triggered when the <see cref="BeatmapSets"/> loaded change and are completely loaded.
/// </summary>
public Action BeatmapSetsChanged ;
/// <summary>
/// The currently selected beatmap.
/// </summary>
public BeatmapInfo SelectedBeatmap = > selectedBeatmap ? . Beatmap ;
2019-02-21 09:56:34 +00:00
private CarouselBeatmap selectedBeatmap = > selectedBeatmapSet ? . Beatmaps . FirstOrDefault ( s = > s . State . Value = = CarouselItemState . Selected ) ;
2018-04-13 09:19:50 +00:00
/// <summary>
/// The currently selected beatmap set.
/// </summary>
public BeatmapSetInfo SelectedBeatmapSet = > selectedBeatmapSet ? . BeatmapSet ;
2020-04-11 07:58:13 +00:00
/// <summary>
/// A function to optionally decide on a recommended difficulty from a beatmap set.
/// </summary>
public Func < IEnumerable < BeatmapInfo > , BeatmapInfo > GetRecommendedBeatmap ;
2018-04-13 09:19:50 +00:00
private CarouselBeatmapSet selectedBeatmapSet ;
/// <summary>
/// Raised when the <see cref="SelectedBeatmap"/> is changed.
/// </summary>
public Action < BeatmapInfo > SelectionChanged ;
2018-09-26 05:01:15 +00:00
public override bool HandleNonPositionalInput = > AllowSelection ;
public override bool HandlePositionalInput = > AllowSelection ;
2018-04-13 09:19:50 +00:00
2020-01-27 05:55:47 +00:00
public override bool PropagatePositionalInputSubTree = > AllowSelection ;
public override bool PropagateNonPositionalInputSubTree = > AllowSelection ;
2018-04-13 09:19:50 +00:00
/// <summary>
2019-03-21 11:51:06 +00:00
/// Whether carousel items have completed asynchronously loaded.
2018-04-13 09:19:50 +00:00
/// </summary>
2019-03-21 11:51:06 +00:00
public bool BeatmapSetsLoaded { get ; private set ; }
2018-04-13 09:19:50 +00:00
2020-03-13 03:30:27 +00:00
private readonly CarouselScrollContainer scroll ;
2019-08-15 05:00:12 +00:00
2018-04-13 09:19:50 +00:00
private IEnumerable < CarouselBeatmapSet > beatmapSets = > root . Children . OfType < CarouselBeatmapSet > ( ) ;
2020-01-29 07:51:14 +00:00
// todo: only used for testing, maybe remove.
2018-04-13 09:19:50 +00:00
public IEnumerable < BeatmapSetInfo > BeatmapSets
{
2018-08-28 16:42:25 +00:00
get = > beatmapSets . Select ( g = > g . BeatmapSet ) ;
2019-06-30 13:23:48 +00:00
set = > loadBeatmapSets ( value ) ;
2018-08-28 16:42:25 +00:00
}
2019-06-30 13:23:48 +00:00
private void loadBeatmapSets ( IEnumerable < BeatmapSetInfo > beatmapSets )
2018-08-28 16:42:25 +00:00
{
CarouselRoot newRoot = new CarouselRoot ( this ) ;
2019-06-30 13:23:48 +00:00
beatmapSets . Select ( createCarouselSet ) . Where ( g = > g ! = null ) . ForEach ( newRoot . AddChild ) ;
newRoot . Filter ( activeCriteria ) ;
2018-04-13 09:19:50 +00:00
2019-06-30 13:23:48 +00:00
// preload drawables as the ctor overhead is quite high currently.
2019-11-12 12:03:21 +00:00
_ = newRoot . Drawables ;
2018-04-13 09:19:50 +00:00
2019-06-30 13:23:48 +00:00
root = newRoot ;
2019-09-24 17:42:12 +00:00
if ( selectedBeatmapSet ! = null & & ! beatmapSets . Contains ( selectedBeatmapSet . BeatmapSet ) )
selectedBeatmapSet = null ;
2019-06-30 13:23:48 +00:00
scrollableContent . Clear ( false ) ;
itemsCache . Invalidate ( ) ;
scrollPositionCache . Invalidate ( ) ;
2019-07-25 03:18:18 +00:00
// Run on late scheduler want to ensure this runs after all pending UpdateBeatmapSet / RemoveBeatmapSet operations are run.
SchedulerAfterChildren . Add ( ( ) = >
2019-06-30 13:23:48 +00:00
{
BeatmapSetsChanged ? . Invoke ( ) ;
BeatmapSetsLoaded = true ;
} ) ;
2018-04-13 09:19:50 +00:00
}
private readonly List < float > yPositions = new List < float > ( ) ;
2019-08-09 10:12:29 +00:00
private readonly Cached itemsCache = new Cached ( ) ;
private readonly Cached scrollPositionCache = new Cached ( ) ;
2018-04-13 09:19:50 +00:00
private readonly Container < DrawableCarouselItem > scrollableContent ;
2018-04-13 10:50:37 +00:00
public Bindable < bool > RightClickScrollingEnabled = new Bindable < bool > ( ) ;
2018-04-13 09:19:50 +00:00
public Bindable < RandomSelectAlgorithm > RandomAlgorithm = new Bindable < RandomSelectAlgorithm > ( ) ;
private readonly List < CarouselBeatmapSet > previouslyVisitedRandomSets = new List < CarouselBeatmapSet > ( ) ;
private readonly Stack < CarouselBeatmap > randomSelectedBeatmaps = new Stack < CarouselBeatmap > ( ) ;
protected List < DrawableCarouselItem > Items = new List < DrawableCarouselItem > ( ) ;
2020-04-11 07:41:11 +00:00
2018-04-13 09:19:50 +00:00
private CarouselRoot root ;
2020-04-11 07:41:11 +00:00
2020-05-27 07:08:47 +00:00
private IBindable < WeakReference < BeatmapSetInfo > > itemUpdated ;
2020-05-19 07:44:22 +00:00
private IBindable < WeakReference < BeatmapSetInfo > > itemRemoved ;
private IBindable < WeakReference < BeatmapInfo > > itemHidden ;
private IBindable < WeakReference < BeatmapInfo > > itemRestored ;
2018-04-13 09:19:50 +00:00
public BeatmapCarousel ( )
{
root = new CarouselRoot ( this ) ;
2019-08-15 05:00:12 +00:00
InternalChild = new OsuContextMenuContainer
2018-04-13 09:19:50 +00:00
{
2019-08-15 05:00:12 +00:00
RelativeSizeAxes = Axes . Both ,
2019-08-15 09:27:45 +00:00
Child = scroll = new CarouselScrollContainer
2018-04-13 09:19:50 +00:00
{
2019-08-15 05:00:12 +00:00
Masking = false ,
RelativeSizeAxes = Axes . Both ,
Child = scrollableContent = new Container < DrawableCarouselItem >
{
RelativeSizeAxes = Axes . X ,
}
2018-04-13 09:19:50 +00:00
}
} ;
}
2020-01-29 07:51:14 +00:00
[Resolved]
private BeatmapManager beatmaps { get ; set ; }
2020-03-26 16:42:08 +00:00
2018-04-13 09:19:50 +00:00
[BackgroundDependencyLoader(permitNulls: true)]
2020-04-09 15:47:28 +00:00
private void load ( OsuConfigManager config )
2018-04-13 09:19:50 +00:00
{
config . BindWith ( OsuSetting . RandomSelectAlgorithm , RandomAlgorithm ) ;
2018-04-18 10:26:54 +00:00
config . BindWith ( OsuSetting . SongSelectRightMouseScroll , RightClickScrollingEnabled ) ;
2018-04-13 10:50:37 +00:00
2019-08-15 05:00:12 +00:00
RightClickScrollingEnabled . ValueChanged + = enabled = > scroll . RightMouseScrollbar = enabled . NewValue ;
2018-04-13 10:50:37 +00:00
RightClickScrollingEnabled . TriggerChange ( ) ;
2019-06-30 13:23:48 +00:00
2020-05-27 07:08:47 +00:00
itemUpdated = beatmaps . ItemUpdated . GetBoundCopy ( ) ;
itemUpdated . BindValueChanged ( beatmapUpdated ) ;
2020-05-19 07:44:22 +00:00
itemRemoved = beatmaps . ItemRemoved . GetBoundCopy ( ) ;
itemRemoved . BindValueChanged ( beatmapRemoved ) ;
itemHidden = beatmaps . BeatmapHidden . GetBoundCopy ( ) ;
itemHidden . BindValueChanged ( beatmapHidden ) ;
itemRestored = beatmaps . BeatmapRestored . GetBoundCopy ( ) ;
itemRestored . BindValueChanged ( beatmapRestored ) ;
2020-01-29 07:51:14 +00:00
2020-03-12 06:26:22 +00:00
loadBeatmapSets ( GetLoadableBeatmaps ( ) ) ;
2018-04-13 09:19:50 +00:00
}
2020-04-28 12:43:35 +00:00
protected virtual IEnumerable < BeatmapSetInfo > GetLoadableBeatmaps ( ) = > beatmaps . GetAllUsableBeatmapSetsEnumerable ( IncludedDetails . AllButFiles ) ;
2020-03-12 06:26:22 +00:00
2019-07-25 03:18:18 +00:00
public void RemoveBeatmapSet ( BeatmapSetInfo beatmapSet ) = > Schedule ( ( ) = >
2018-04-13 09:19:50 +00:00
{
2019-07-25 03:18:18 +00:00
var existingSet = beatmapSets . FirstOrDefault ( b = > b . BeatmapSet . ID = = beatmapSet . ID ) ;
2018-04-13 09:19:50 +00:00
2019-07-25 03:18:18 +00:00
if ( existingSet = = null )
return ;
2018-04-13 09:19:50 +00:00
2019-07-25 03:18:18 +00:00
root . RemoveChild ( existingSet ) ;
itemsCache . Invalidate ( ) ;
} ) ;
2018-04-13 09:19:50 +00:00
2019-06-26 02:40:33 +00:00
public void UpdateBeatmapSet ( BeatmapSetInfo beatmapSet ) = > Schedule ( ( ) = >
2018-04-13 09:19:50 +00:00
{
2019-06-26 02:40:33 +00:00
int? previouslySelectedID = null ;
CarouselBeatmapSet existingSet = beatmapSets . FirstOrDefault ( b = > b . BeatmapSet . ID = = beatmapSet . ID ) ;
2018-04-13 09:19:50 +00:00
2019-06-26 02:40:33 +00:00
// If the selected beatmap is about to be removed, store its ID so it can be re-selected if required
if ( existingSet ? . State ? . Value = = CarouselItemState . Selected )
previouslySelectedID = selectedBeatmap ? . Beatmap . ID ;
2018-04-13 09:19:50 +00:00
2019-06-26 02:40:33 +00:00
var newSet = createCarouselSet ( beatmapSet ) ;
2018-04-13 09:19:50 +00:00
2019-06-26 02:40:33 +00:00
if ( existingSet ! = null )
root . RemoveChild ( existingSet ) ;
2018-04-13 09:19:50 +00:00
2019-06-26 02:40:33 +00:00
if ( newSet = = null )
{
itemsCache . Invalidate ( ) ;
return ;
}
2018-04-13 09:19:50 +00:00
2019-06-26 02:40:33 +00:00
root . AddChild ( newSet ) ;
2018-04-13 09:19:50 +00:00
2020-03-13 02:51:26 +00:00
// only reset scroll position if already near the scroll target.
// without this, during a large beatmap import it is impossible to navigate the carousel.
applyActiveCriteria ( false , alwaysResetScrollPosition : false ) ;
2018-04-13 09:19:50 +00:00
2020-05-05 01:31:11 +00:00
// check if we can/need to maintain our current selection.
2019-06-26 02:40:33 +00:00
if ( previouslySelectedID ! = null )
select ( ( CarouselItem ) newSet . Beatmaps . FirstOrDefault ( b = > b . Beatmap . ID = = previouslySelectedID ) ? ? newSet ) ;
2018-04-13 09:19:50 +00:00
2019-06-26 02:40:33 +00:00
itemsCache . Invalidate ( ) ;
Schedule ( ( ) = > BeatmapSetsChanged ? . Invoke ( ) ) ;
} ) ;
2018-04-13 09:19:50 +00:00
/// <summary>
/// Selects a given beatmap on the carousel.
/// </summary>
/// <param name="beatmap">The beatmap to select.</param>
/// <param name="bypassFilters">Whether to select the beatmap even if it is filtered (i.e., not visible on carousel).</param>
/// <returns>True if a selection was made, False if it wasn't.</returns>
public bool SelectBeatmap ( BeatmapInfo beatmap , bool bypassFilters = true )
{
2020-04-16 09:10:35 +00:00
// ensure that any pending events from BeatmapManager have been run before attempting a selection.
Scheduler . Update ( ) ;
2018-04-13 09:19:50 +00:00
if ( beatmap ? . Hidden ! = false )
return false ;
foreach ( CarouselBeatmapSet set in beatmapSets )
{
2019-02-21 09:56:34 +00:00
if ( ! bypassFilters & & set . Filtered . Value )
2018-04-13 09:19:50 +00:00
continue ;
var item = set . Beatmaps . FirstOrDefault ( p = > p . Beatmap . Equals ( beatmap ) ) ;
if ( item = = null )
// The beatmap that needs to be selected doesn't exist in this set
continue ;
2019-02-21 09:56:34 +00:00
if ( ! bypassFilters & & item . Filtered . Value )
2020-03-12 06:52:03 +00:00
return false ;
2018-04-13 09:19:50 +00:00
2020-03-12 06:52:03 +00:00
select ( item ) ;
2020-02-10 07:31:52 +00:00
2020-03-12 06:52:03 +00:00
// if we got here and the set is filtered, it means we were bypassing filters.
// in this case, reapplying the filter is necessary to ensure the panel is in the correct place
// (since it is forcefully being included in the carousel).
if ( set . Filtered . Value )
{
Debug . Assert ( bypassFilters ) ;
2020-02-10 07:31:52 +00:00
2020-03-12 06:52:03 +00:00
applyActiveCriteria ( false ) ;
2018-04-13 09:19:50 +00:00
}
2020-03-12 06:52:03 +00:00
return true ;
2018-04-13 09:19:50 +00:00
}
return false ;
}
/// <summary>
/// Increment selection in the carousel in a chosen direction.
/// </summary>
/// <param name="direction">The direction to increment. Negative is backwards.</param>
/// <param name="skipDifficulties">Whether to skip individual difficulties and only increment over full groups.</param>
public void SelectNext ( int direction = 1 , bool skipDifficulties = true )
{
2020-03-28 18:21:21 +00:00
if ( beatmapSets . All ( s = > s . Filtered . Value ) )
2018-04-13 09:19:50 +00:00
return ;
2020-03-28 10:54:48 +00:00
if ( skipDifficulties )
selectNextSet ( direction , true ) ;
else
selectNextDifficulty ( direction ) ;
}
2018-04-13 09:19:50 +00:00
2020-03-28 10:54:48 +00:00
private void selectNextSet ( int direction , bool skipDifficulties )
{
2020-03-29 15:07:48 +00:00
var unfilteredSets = beatmapSets . Where ( s = > ! s . Filtered . Value ) . ToList ( ) ;
2018-04-13 09:19:50 +00:00
2020-03-29 15:07:48 +00:00
var nextSet = unfilteredSets [ ( unfilteredSets . IndexOf ( selectedBeatmapSet ) + direction + unfilteredSets . Count ) % unfilteredSets . Count ] ;
2018-04-13 09:19:50 +00:00
2020-03-28 10:54:48 +00:00
if ( skipDifficulties )
2020-03-28 11:23:31 +00:00
select ( nextSet ) ;
2020-03-28 10:54:48 +00:00
else
2020-03-28 11:23:31 +00:00
select ( direction > 0 ? nextSet . Beatmaps . First ( b = > ! b . Filtered . Value ) : nextSet . Beatmaps . Last ( b = > ! b . Filtered . Value ) ) ;
2020-03-28 10:54:48 +00:00
}
2018-04-13 09:19:50 +00:00
2020-03-28 10:54:48 +00:00
private void selectNextDifficulty ( int direction )
{
2020-06-26 12:03:34 +00:00
if ( selectedBeatmap = = null )
return ;
2020-03-29 15:07:48 +00:00
var unfilteredDifficulties = selectedBeatmapSet . Children . Where ( s = > ! s . Filtered . Value ) . ToList ( ) ;
2018-04-13 09:19:50 +00:00
2020-03-29 15:07:48 +00:00
int index = unfilteredDifficulties . IndexOf ( selectedBeatmap ) ;
2018-04-13 09:19:50 +00:00
2020-03-29 15:07:48 +00:00
if ( index + direction < 0 | | index + direction > = unfilteredDifficulties . Count )
2020-03-28 10:54:48 +00:00
selectNextSet ( direction , false ) ;
else
2020-03-29 15:07:48 +00:00
select ( unfilteredDifficulties [ index + direction ] ) ;
2018-04-13 09:19:50 +00:00
}
/// <summary>
/// Select the next beatmap in the random sequence.
/// </summary>
/// <returns>True if a selection could be made, else False.</returns>
public bool SelectNextRandom ( )
{
2019-02-21 09:56:34 +00:00
var visibleSets = beatmapSets . Where ( s = > ! s . Filtered . Value ) . ToList ( ) ;
2018-04-13 09:19:50 +00:00
if ( ! visibleSets . Any ( ) )
return false ;
if ( selectedBeatmap ! = null )
{
randomSelectedBeatmaps . Push ( selectedBeatmap ) ;
// when performing a random, we want to add the current set to the previously visited list
// else the user may be "randomised" to the existing selection.
if ( previouslyVisitedRandomSets . LastOrDefault ( ) ! = selectedBeatmapSet )
previouslyVisitedRandomSets . Add ( selectedBeatmapSet ) ;
}
CarouselBeatmapSet set ;
2019-02-21 09:56:34 +00:00
if ( RandomAlgorithm . Value = = RandomSelectAlgorithm . RandomPermutation )
2018-04-13 09:19:50 +00:00
{
var notYetVisitedSets = visibleSets . Except ( previouslyVisitedRandomSets ) . ToList ( ) ;
2019-04-01 03:16:05 +00:00
2018-04-13 09:19:50 +00:00
if ( ! notYetVisitedSets . Any ( ) )
{
previouslyVisitedRandomSets . RemoveAll ( s = > visibleSets . Contains ( s ) ) ;
notYetVisitedSets = visibleSets ;
}
set = notYetVisitedSets . ElementAt ( RNG . Next ( notYetVisitedSets . Count ) ) ;
previouslyVisitedRandomSets . Add ( set ) ;
}
else
set = visibleSets . ElementAt ( RNG . Next ( visibleSets . Count ) ) ;
2020-03-20 04:01:24 +00:00
select ( set ) ;
2018-04-13 09:19:50 +00:00
return true ;
}
public void SelectPreviousRandom ( )
{
while ( randomSelectedBeatmaps . Any ( ) )
{
var beatmap = randomSelectedBeatmaps . Pop ( ) ;
2019-02-21 09:56:34 +00:00
if ( ! beatmap . Filtered . Value )
2018-04-13 09:19:50 +00:00
{
2019-02-21 09:56:34 +00:00
if ( RandomAlgorithm . Value = = RandomSelectAlgorithm . RandomPermutation )
2018-04-13 09:19:50 +00:00
previouslyVisitedRandomSets . Remove ( selectedBeatmapSet ) ;
select ( beatmap ) ;
break ;
}
}
}
private void select ( CarouselItem item )
{
2019-03-21 12:02:45 +00:00
if ( ! AllowSelection )
return ;
2018-04-13 09:19:50 +00:00
if ( item = = null ) return ;
2019-02-28 04:31:40 +00:00
2018-04-13 09:19:50 +00:00
item . State . Value = CarouselItemState . Selected ;
}
private FilterCriteria activeCriteria = new FilterCriteria ( ) ;
2018-07-18 01:12:14 +00:00
protected ScheduledDelegate PendingFilter ;
2018-04-13 09:19:50 +00:00
public bool AllowSelection = true ;
2019-07-26 04:07:28 +00:00
/// <summary>
2019-07-26 06:22:29 +00:00
/// Half the height of the visible content.
2019-07-26 06:13:10 +00:00
/// <remarks>
2019-11-17 12:55:40 +00:00
/// This is different from the height of <see cref="ScrollContainer{T}"/>.displayableContent, since
2019-07-26 06:13:10 +00:00
/// the beatmap carousel bleeds into the <see cref="FilterControl"/> and the <see cref="Footer"/>
/// </remarks>
2019-07-26 04:07:28 +00:00
/// </summary>
2020-04-19 15:29:06 +00:00
private float visibleHalfHeight = > ( DrawHeight + BleedBottom + BleedTop ) / 2 ;
2019-07-26 04:07:28 +00:00
2019-07-26 06:13:10 +00:00
/// <summary>
/// The position of the lower visible bound with respect to the current scroll position.
/// </summary>
2020-04-19 15:29:06 +00:00
private float visibleBottomBound = > scroll . Current + DrawHeight + BleedBottom ;
2019-07-26 06:13:10 +00:00
/// <summary>
/// The position of the upper visible bound with respect to the current scroll position.
/// </summary>
2020-04-19 15:29:06 +00:00
private float visibleUpperBound = > scroll . Current - BleedTop ;
2019-07-26 06:13:10 +00:00
2018-04-13 09:19:50 +00:00
public void FlushPendingFilterOperations ( )
{
2018-07-18 01:12:14 +00:00
if ( PendingFilter ? . Completed = = false )
2018-04-13 09:19:50 +00:00
{
2020-03-10 10:59:49 +00:00
applyActiveCriteria ( false ) ;
2018-04-13 09:19:50 +00:00
Update ( ) ;
}
}
public void Filter ( FilterCriteria newCriteria , bool debounce = true )
{
if ( newCriteria ! = null )
activeCriteria = newCriteria ;
2020-03-10 10:59:49 +00:00
applyActiveCriteria ( debounce ) ;
2018-04-13 09:19:50 +00:00
}
2020-03-13 02:51:26 +00:00
private void applyActiveCriteria ( bool debounce , bool alwaysResetScrollPosition = true )
2018-04-13 09:19:50 +00:00
{
if ( root . Children . Any ( ) ! = true ) return ;
void perform ( )
{
2018-07-18 01:12:14 +00:00
PendingFilter = null ;
2018-04-13 09:19:50 +00:00
root . Filter ( activeCriteria ) ;
itemsCache . Invalidate ( ) ;
2020-03-13 02:51:26 +00:00
2020-03-13 03:30:27 +00:00
if ( alwaysResetScrollPosition | | ! scroll . UserScrolling )
2020-03-13 02:51:26 +00:00
ScrollToSelected ( ) ;
2018-04-13 09:19:50 +00:00
}
2018-07-18 01:12:14 +00:00
PendingFilter ? . Cancel ( ) ;
PendingFilter = null ;
2018-04-13 09:19:50 +00:00
if ( debounce )
2018-07-18 01:12:14 +00:00
PendingFilter = Scheduler . AddDelayed ( perform , 250 ) ;
2018-04-13 09:19:50 +00:00
else
perform ( ) ;
}
private float? scrollTarget ;
2020-03-13 02:51:26 +00:00
/// <summary>
/// Scroll to the current <see cref="SelectedBeatmap"/>.
/// </summary>
2018-04-13 09:19:50 +00:00
public void ScrollToSelected ( ) = > scrollPositionCache . Invalidate ( ) ;
2020-06-25 10:47:23 +00:00
#region Key / button selection logic
2018-10-02 03:02:47 +00:00
protected override bool OnKeyDown ( KeyDownEvent e )
2018-04-13 09:19:50 +00:00
{
2018-10-02 03:02:47 +00:00
switch ( e . Key )
2018-04-13 09:19:50 +00:00
{
case Key . Left :
2020-06-25 10:47:23 +00:00
if ( ! e . Repeat )
beginRepeatSelection ( ( ) = > SelectNext ( - 1 , true ) , e . Key ) ;
2020-03-02 09:55:28 +00:00
return true ;
2019-04-01 03:16:05 +00:00
2018-04-13 09:19:50 +00:00
case Key . Right :
2020-06-25 10:47:23 +00:00
if ( ! e . Repeat )
beginRepeatSelection ( ( ) = > SelectNext ( 1 , true ) , e . Key ) ;
2020-03-02 09:55:28 +00:00
return true ;
}
return false ;
}
2020-06-25 10:47:23 +00:00
protected override void OnKeyUp ( KeyUpEvent e )
{
switch ( e . Key )
{
case Key . Left :
case Key . Right :
endRepeatSelection ( e . Key ) ;
break ;
}
base . OnKeyUp ( e ) ;
}
2020-03-02 09:55:28 +00:00
public bool OnPressed ( GlobalAction action )
{
switch ( action )
{
case GlobalAction . SelectNext :
2020-06-25 10:47:23 +00:00
beginRepeatSelection ( ( ) = > SelectNext ( 1 , false ) , action ) ;
2020-03-02 09:55:28 +00:00
return true ;
case GlobalAction . SelectPrevious :
2020-06-25 10:47:23 +00:00
beginRepeatSelection ( ( ) = > SelectNext ( - 1 , false ) , action ) ;
2020-03-02 09:55:28 +00:00
return true ;
2018-04-13 09:19:50 +00:00
}
2020-03-02 09:55:28 +00:00
return false ;
}
2018-04-13 09:19:50 +00:00
2020-03-02 09:55:28 +00:00
public void OnReleased ( GlobalAction action )
{
2020-06-25 10:47:23 +00:00
switch ( action )
{
case GlobalAction . SelectNext :
case GlobalAction . SelectPrevious :
endRepeatSelection ( action ) ;
break ;
}
2018-04-13 09:19:50 +00:00
}
2020-06-25 10:47:23 +00:00
private ScheduledDelegate repeatDelegate ;
private object lastRepeatSource ;
/// <summary>
/// Begin repeating the specified selection action.
/// </summary>
/// <param name="action">The action to perform.</param>
/// <param name="source">The source of the action. Used in conjunction with <see cref="endRepeatSelection"/> to only cancel the correct action (most recently pressed key).</param>
private void beginRepeatSelection ( Action action , object source )
{
endRepeatSelection ( ) ;
lastRepeatSource = source ;
2020-06-26 11:14:08 +00:00
repeatDelegate = this . BeginKeyRepeat ( Scheduler , action ) ;
2020-06-25 10:47:23 +00:00
}
private void endRepeatSelection ( object source = null )
{
// only the most recent source should be able to cancel the current action.
if ( source ! = null & & ! EqualityComparer < object > . Default . Equals ( lastRepeatSource , source ) )
return ;
repeatDelegate ? . Cancel ( ) ;
repeatDelegate = null ;
lastRepeatSource = null ;
}
#endregion
2018-04-13 09:19:50 +00:00
protected override void Update ( )
{
base . Update ( ) ;
if ( ! itemsCache . IsValid )
updateItems ( ) ;
// Remove all items that should no longer be on-screen
2019-07-26 06:13:10 +00:00
scrollableContent . RemoveAll ( p = > p . Y < visibleUpperBound - p . DrawHeight | | p . Y > visibleBottomBound | | ! p . IsPresent ) ;
2018-04-13 09:19:50 +00:00
// Find index range of all items that should be on-screen
Trace . Assert ( Items . Count = = yPositions . Count ) ;
2019-07-26 06:13:10 +00:00
int firstIndex = yPositions . BinarySearch ( visibleUpperBound - DrawableCarouselItem . MAX_HEIGHT ) ;
2018-04-13 09:19:50 +00:00
if ( firstIndex < 0 ) firstIndex = ~ firstIndex ;
2019-07-26 06:13:10 +00:00
int lastIndex = yPositions . BinarySearch ( visibleBottomBound ) ;
2018-04-13 09:19:50 +00:00
if ( lastIndex < 0 ) lastIndex = ~ lastIndex ;
int notVisibleCount = 0 ;
// Add those items within the previously found index range that should be displayed.
for ( int i = firstIndex ; i < lastIndex ; + + i )
{
DrawableCarouselItem item = Items [ i ] ;
if ( ! item . Item . Visible )
{
if ( ! item . IsPresent )
notVisibleCount + + ;
continue ;
}
float depth = i + ( item is DrawableCarouselBeatmapSet ? - Items . Count : 0 ) ;
// Only add if we're not already part of the content.
if ( ! scrollableContent . Contains ( item ) )
{
// Makes sure headers are always _below_ items,
// and depth flows downward.
item . Depth = depth ;
switch ( item . LoadState )
{
case LoadState . NotLoaded :
LoadComponentAsync ( item ) ;
break ;
2019-04-01 03:16:05 +00:00
2018-04-13 09:19:50 +00:00
case LoadState . Loading :
break ;
2019-04-01 03:16:05 +00:00
2018-04-13 09:19:50 +00:00
default :
scrollableContent . Add ( item ) ;
break ;
}
}
else
{
scrollableContent . ChangeChildDepth ( item , depth ) ;
}
}
// this is not actually useful right now, but once we have groups may well be.
if ( notVisibleCount > 50 )
itemsCache . Invalidate ( ) ;
// Update externally controlled state of currently visible items
// (e.g. x-offset and opacity).
foreach ( DrawableCarouselItem p in scrollableContent . Children )
2019-07-26 06:22:29 +00:00
updateItem ( p ) ;
2018-04-13 09:19:50 +00:00
}
2019-11-20 10:38:39 +00:00
protected override void UpdateAfterChildren ( )
{
base . UpdateAfterChildren ( ) ;
if ( ! scrollPositionCache . IsValid )
updateScrollPosition ( ) ;
}
2018-09-06 04:27:17 +00:00
protected override void Dispose ( bool isDisposing )
{
base . Dispose ( isDisposing ) ;
// aggressively dispose "off-screen" items to reduce GC pressure.
foreach ( var i in Items )
i . Dispose ( ) ;
}
2020-05-19 07:44:22 +00:00
private void beatmapRemoved ( ValueChangedEvent < WeakReference < BeatmapSetInfo > > weakItem )
{
if ( weakItem . NewValue . TryGetTarget ( out var item ) )
RemoveBeatmapSet ( item ) ;
}
2020-01-29 07:51:14 +00:00
2020-05-27 07:08:47 +00:00
private void beatmapUpdated ( ValueChangedEvent < WeakReference < BeatmapSetInfo > > weakItem )
2020-05-19 07:44:22 +00:00
{
if ( weakItem . NewValue . TryGetTarget ( out var item ) )
UpdateBeatmapSet ( item ) ;
}
2020-01-29 07:51:14 +00:00
2020-05-19 07:44:22 +00:00
private void beatmapRestored ( ValueChangedEvent < WeakReference < BeatmapInfo > > weakItem )
{
if ( weakItem . NewValue . TryGetTarget ( out var b ) )
UpdateBeatmapSet ( beatmaps . QueryBeatmapSet ( s = > s . ID = = b . BeatmapSetInfoID ) ) ;
}
2020-01-29 07:51:14 +00:00
2020-05-19 07:44:22 +00:00
private void beatmapHidden ( ValueChangedEvent < WeakReference < BeatmapInfo > > weakItem )
{
if ( weakItem . NewValue . TryGetTarget ( out var b ) )
UpdateBeatmapSet ( beatmaps . QueryBeatmapSet ( s = > s . ID = = b . BeatmapSetInfoID ) ) ;
}
2020-01-29 07:51:14 +00:00
2018-04-13 09:19:50 +00:00
private CarouselBeatmapSet createCarouselSet ( BeatmapSetInfo beatmapSet )
{
if ( beatmapSet . Beatmaps . All ( b = > b . Hidden ) )
return null ;
// todo: remove the need for this.
foreach ( var b in beatmapSet . Beatmaps )
2020-06-03 07:48:44 +00:00
b . Metadata ? ? = beatmapSet . Metadata ;
2018-04-13 09:19:50 +00:00
2020-04-11 07:58:13 +00:00
var set = new CarouselBeatmapSet ( beatmapSet )
2020-04-09 15:47:28 +00:00
{
2020-04-11 07:58:13 +00:00
GetRecommendedBeatmap = beatmaps = > GetRecommendedBeatmap ? . Invoke ( beatmaps )
} ;
2018-04-13 09:19:50 +00:00
foreach ( var c in set . Beatmaps )
{
2019-02-22 08:51:39 +00:00
c . State . ValueChanged + = state = >
2018-04-13 09:19:50 +00:00
{
2019-02-22 08:51:39 +00:00
if ( state . NewValue = = CarouselItemState . Selected )
2018-04-13 09:19:50 +00:00
{
selectedBeatmapSet = set ;
SelectionChanged ? . Invoke ( c . Beatmap ) ;
itemsCache . Invalidate ( ) ;
2020-03-13 02:51:26 +00:00
ScrollToSelected ( ) ;
2018-04-13 09:19:50 +00:00
}
} ;
}
return set ;
}
/// <summary>
/// Computes the target Y positions for every item in the carousel.
/// </summary>
/// <returns>The Y position of the currently selected item.</returns>
private void updateItems ( )
{
Items = root . Drawables . ToList ( ) ;
yPositions . Clear ( ) ;
2019-07-26 06:22:29 +00:00
float currentY = visibleHalfHeight ;
2018-04-13 09:19:50 +00:00
DrawableCarouselBeatmapSet lastSet = null ;
scrollTarget = null ;
foreach ( DrawableCarouselItem d in Items )
{
if ( d . IsPresent )
{
switch ( d )
{
case DrawableCarouselBeatmapSet set :
2019-12-14 13:28:13 +00:00
{
2018-04-13 09:19:50 +00:00
lastSet = set ;
2019-02-21 09:56:34 +00:00
set . MoveToX ( set . Item . State . Value = = CarouselItemState . Selected ? - 100 : 0 , 500 , Easing . OutExpo ) ;
2018-04-13 09:19:50 +00:00
set . MoveToY ( currentY , 750 , Easing . OutExpo ) ;
break ;
2019-12-14 13:28:13 +00:00
}
2019-04-01 03:16:05 +00:00
2018-04-13 09:19:50 +00:00
case DrawableCarouselBeatmap beatmap :
2019-12-14 13:28:13 +00:00
{
2018-04-13 09:19:50 +00:00
if ( beatmap . Item . State . Value = = CarouselItemState . Selected )
2020-04-19 15:29:06 +00:00
// scroll position at currentY makes the set panel appear at the very top of the carousel's screen space
// move down by half of visible height (height of the carousel's visible extent, including semi-transparent areas)
// then reapply the top semi-transparent area (because carousel's screen space starts below it)
2020-04-12 17:40:08 +00:00
// and finally add half of the panel's own height to achieve vertical centering of the panel itself
2020-04-19 15:29:06 +00:00
scrollTarget = currentY - visibleHalfHeight + BleedTop + beatmap . DrawHeight / 2 ;
2018-04-13 09:19:50 +00:00
void performMove ( float y , float? startY = null )
{
if ( startY ! = null ) beatmap . MoveTo ( new Vector2 ( 0 , startY . Value ) ) ;
2019-02-21 09:56:34 +00:00
beatmap . MoveToX ( beatmap . Item . State . Value = = CarouselItemState . Selected ? - 50 : 0 , 500 , Easing . OutExpo ) ;
2018-04-13 09:19:50 +00:00
beatmap . MoveToY ( y , 750 , Easing . OutExpo ) ;
}
Debug . Assert ( lastSet ! = null ) ;
float? setY = null ;
if ( ! d . IsLoaded | | beatmap . Alpha = = 0 ) // can't use IsPresent due to DrawableCarouselItem override.
setY = lastSet . Y + lastSet . DrawHeight + 5 ;
if ( d . IsLoaded )
performMove ( currentY , setY ) ;
else
{
float y = currentY ;
2019-03-17 04:43:23 +00:00
d . OnLoadComplete + = _ = > performMove ( y , setY ) ;
2018-04-13 09:19:50 +00:00
}
break ;
2019-12-14 13:28:13 +00:00
}
2018-04-13 09:19:50 +00:00
}
}
yPositions . Add ( currentY ) ;
if ( d . Item . Visible )
currentY + = d . DrawHeight + 5 ;
}
2019-07-26 06:22:29 +00:00
currentY + = visibleHalfHeight ;
2018-04-13 09:19:50 +00:00
scrollableContent . Height = currentY ;
2019-03-21 11:51:06 +00:00
if ( BeatmapSetsLoaded & & ( selectedBeatmapSet = = null | | selectedBeatmap = = null | | selectedBeatmapSet . State . Value ! = CarouselItemState . Selected ) )
2018-04-13 09:19:50 +00:00
{
selectedBeatmapSet = null ;
SelectionChanged ? . Invoke ( null ) ;
}
itemsCache . Validate ( ) ;
}
2019-11-22 01:51:49 +00:00
private bool firstScroll = true ;
2018-04-13 09:19:50 +00:00
private void updateScrollPosition ( )
{
2019-11-20 10:38:39 +00:00
if ( scrollTarget ! = null )
{
2019-11-22 01:51:49 +00:00
if ( firstScroll )
{
// reduce movement when first displaying the carousel.
scroll . ScrollTo ( scrollTarget . Value - 200 , false ) ;
firstScroll = false ;
}
2019-11-20 10:38:39 +00:00
scroll . ScrollTo ( scrollTarget . Value ) ;
scrollPositionCache . Validate ( ) ;
}
2018-04-13 09:19:50 +00:00
}
/// <summary>
/// Computes the x-offset of currently visible items. Makes the carousel appear round.
/// </summary>
/// <param name="dist">
/// Vertical distance from the center of the carousel container
/// ranging from -1 to 1.
/// </param>
/// <param name="halfHeight">Half the height of the carousel container.</param>
private static float offsetX ( float dist , float halfHeight )
{
// The radius of the circle the carousel moves on.
const float circle_radius = 3 ;
2019-11-24 23:45:42 +00:00
float discriminant = MathF . Max ( 0 , circle_radius * circle_radius - dist * dist ) ;
float x = ( circle_radius - MathF . Sqrt ( discriminant ) ) * halfHeight ;
2018-04-13 09:19:50 +00:00
return 125 + x ;
}
/// <summary>
/// Update a item's x position and multiplicative alpha based on its y position and
/// the current scroll position.
/// </summary>
/// <param name="p">The item to be updated.</param>
2019-07-26 06:22:29 +00:00
private void updateItem ( DrawableCarouselItem p )
2018-04-13 09:19:50 +00:00
{
2019-07-26 06:13:10 +00:00
float itemDrawY = p . Position . Y - visibleUpperBound + p . DrawHeight / 2 ;
2019-07-26 06:22:29 +00:00
float dist = Math . Abs ( 1f - itemDrawY / visibleHalfHeight ) ;
2018-04-13 09:19:50 +00:00
// Setting the origin position serves as an additive position on top of potential
// local transformation we may want to apply (e.g. when a item gets selected, we
// may want to smoothly transform it leftwards.)
2019-07-26 06:22:29 +00:00
p . OriginPosition = new Vector2 ( - offsetX ( dist , visibleHalfHeight ) , 0 ) ;
2018-04-13 09:19:50 +00:00
// We are applying a multiplicative alpha (which is internally done by nesting an
// additional container and setting that container's alpha) such that we can
// layer transformations on top, with a similar reasoning to the previous comment.
2019-11-20 12:19:49 +00:00
p . SetMultiplicativeAlpha ( Math . Clamp ( 1.75f - 1.5f * dist , 0 , 1 ) ) ;
2018-04-13 09:19:50 +00:00
}
private class CarouselRoot : CarouselGroupEagerSelect
{
private readonly BeatmapCarousel carousel ;
public CarouselRoot ( BeatmapCarousel carousel )
{
2020-03-21 15:32:53 +00:00
// root should always remain selected. if not, PerformSelection will not be called.
2020-03-20 06:01:26 +00:00
State . Value = CarouselItemState . Selected ;
State . ValueChanged + = state = > State . Value = CarouselItemState . Selected ;
2018-04-13 09:19:50 +00:00
this . carousel = carousel ;
}
protected override void PerformSelection ( )
{
2020-03-20 04:01:24 +00:00
if ( LastSelected = = null | | LastSelected . Filtered . Value )
2020-03-20 06:01:26 +00:00
carousel ? . SelectNextRandom ( ) ;
2018-04-13 09:19:50 +00:00
else
base . PerformSelection ( ) ;
}
}
2019-08-15 09:27:45 +00:00
private class CarouselScrollContainer : OsuScrollContainer
{
private bool rightMouseScrollBlocked ;
2020-03-13 03:30:27 +00:00
/// <summary>
/// Whether the last scroll event was user triggered, directly on the scroll container.
/// </summary>
public bool UserScrolling { get ; private set ; }
2020-03-14 18:51:30 +00:00
protected override void OnUserScroll ( float value , bool animated = true , double? distanceDecay = default )
2020-03-13 03:30:27 +00:00
{
UserScrolling = true ;
base . OnUserScroll ( value , animated , distanceDecay ) ;
}
public new void ScrollTo ( float value , bool animated = true , double? distanceDecay = null )
{
UserScrolling = false ;
base . ScrollTo ( value , animated , distanceDecay ) ;
}
2019-08-15 09:27:45 +00:00
protected override bool OnMouseDown ( MouseDownEvent e )
{
if ( e . Button = = MouseButton . Right )
{
// we need to block right click absolute scrolling when hovering a carousel item so context menus can display.
// this can be reconsidered when we have an alternative to right click scrolling.
if ( GetContainingInputManager ( ) . HoveredDrawables . OfType < DrawableCarouselItem > ( ) . Any ( ) )
{
rightMouseScrollBlocked = true ;
return false ;
}
}
2019-08-15 10:25:33 +00:00
rightMouseScrollBlocked = false ;
2019-08-15 09:27:45 +00:00
return base . OnMouseDown ( e ) ;
}
protected override bool OnDragStart ( DragStartEvent e )
{
if ( rightMouseScrollBlocked )
return false ;
return base . OnDragStart ( e ) ;
}
}
2018-04-13 09:19:50 +00:00
}
}