2025-01-14 18:18:02 +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;
|
|
|
|
|
using System.Collections.Specialized;
|
|
|
|
|
using System.Linq;
|
|
|
|
|
using System.Threading;
|
|
|
|
|
using osu.Framework.Allocation;
|
|
|
|
|
using osu.Framework.Bindables;
|
|
|
|
|
using osu.Framework.Graphics;
|
|
|
|
|
using osu.Framework.Graphics.Pooling;
|
|
|
|
|
using osu.Game.Beatmaps;
|
|
|
|
|
using osu.Game.Database;
|
2025-01-14 19:23:53 +08:00
|
|
|
|
using osu.Game.Graphics.UserInterface;
|
2025-01-14 18:18:02 +08:00
|
|
|
|
using osu.Game.Screens.Select;
|
|
|
|
|
|
|
|
|
|
namespace osu.Game.Screens.SelectV2
|
|
|
|
|
{
|
|
|
|
|
[Cached]
|
|
|
|
|
public partial class BeatmapCarousel : Carousel<BeatmapInfo>
|
|
|
|
|
{
|
|
|
|
|
private IBindableList<BeatmapSetInfo> detachedBeatmaps = null!;
|
|
|
|
|
|
2025-01-14 19:23:53 +08:00
|
|
|
|
private readonly LoadingLayer loading;
|
|
|
|
|
|
2025-01-23 15:11:02 +08:00
|
|
|
|
private readonly BeatmapCarouselFilterGrouping grouping;
|
|
|
|
|
|
2025-01-14 18:18:02 +08:00
|
|
|
|
public BeatmapCarousel()
|
|
|
|
|
{
|
|
|
|
|
DebounceDelay = 100;
|
|
|
|
|
DistanceOffscreenToPreload = 100;
|
|
|
|
|
|
|
|
|
|
Filters = new ICarouselFilter[]
|
|
|
|
|
{
|
2025-01-14 18:37:28 +08:00
|
|
|
|
new BeatmapCarouselFilterSorting(() => Criteria),
|
2025-01-23 15:11:02 +08:00
|
|
|
|
grouping = new BeatmapCarouselFilterGrouping(() => Criteria),
|
2025-01-14 18:18:02 +08:00
|
|
|
|
};
|
|
|
|
|
|
2025-01-14 19:23:53 +08:00
|
|
|
|
AddInternal(loading = new LoadingLayer(dimBackground: true));
|
2025-01-14 18:18:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[BackgroundDependencyLoader]
|
|
|
|
|
private void load(BeatmapStore beatmapStore, CancellationToken? cancellationToken)
|
2025-01-23 22:53:09 +08:00
|
|
|
|
{
|
|
|
|
|
setupPools();
|
|
|
|
|
setupBeatmaps(beatmapStore, cancellationToken);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#region Beatmap source hookup
|
|
|
|
|
|
|
|
|
|
private void setupBeatmaps(BeatmapStore beatmapStore, CancellationToken? cancellationToken)
|
2025-01-14 18:18:02 +08:00
|
|
|
|
{
|
|
|
|
|
detachedBeatmaps = beatmapStore.GetBeatmapSets(cancellationToken);
|
|
|
|
|
detachedBeatmaps.BindCollectionChanged(beatmapSetsChanged, true);
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-23 22:53:09 +08:00
|
|
|
|
private void beatmapSetsChanged(object? beatmaps, NotifyCollectionChangedEventArgs changed)
|
|
|
|
|
{
|
|
|
|
|
// TODO: moving management of BeatmapInfo tracking to BeatmapStore might be something we want to consider.
|
|
|
|
|
// right now we are managing this locally which is a bit of added overhead.
|
|
|
|
|
IEnumerable<BeatmapSetInfo>? newBeatmapSets = changed.NewItems?.Cast<BeatmapSetInfo>();
|
|
|
|
|
IEnumerable<BeatmapSetInfo>? beatmapSetInfos = changed.OldItems?.Cast<BeatmapSetInfo>();
|
|
|
|
|
|
|
|
|
|
switch (changed.Action)
|
|
|
|
|
{
|
|
|
|
|
case NotifyCollectionChangedAction.Add:
|
|
|
|
|
Items.AddRange(newBeatmapSets!.SelectMany(s => s.Beatmaps));
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case NotifyCollectionChangedAction.Remove:
|
|
|
|
|
|
|
|
|
|
foreach (var set in beatmapSetInfos!)
|
|
|
|
|
{
|
|
|
|
|
foreach (var beatmap in set.Beatmaps)
|
|
|
|
|
Items.RemoveAll(i => i is BeatmapInfo bi && beatmap.Equals(bi));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case NotifyCollectionChangedAction.Move:
|
|
|
|
|
case NotifyCollectionChangedAction.Replace:
|
|
|
|
|
throw new NotImplementedException();
|
|
|
|
|
|
|
|
|
|
case NotifyCollectionChangedAction.Reset:
|
|
|
|
|
Items.Clear();
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Selection handling
|
2025-01-14 18:18:02 +08:00
|
|
|
|
|
2025-01-24 18:29:14 +08:00
|
|
|
|
private GroupDefinition? lastSelectedGroup;
|
|
|
|
|
private BeatmapInfo? lastSelectedBeatmap;
|
|
|
|
|
|
2025-02-06 15:21:18 +08:00
|
|
|
|
protected override void HandleItemActivated(CarouselItem item)
|
2025-01-23 15:11:02 +08:00
|
|
|
|
{
|
2025-02-06 15:21:18 +08:00
|
|
|
|
switch (item.Model)
|
2025-01-23 15:11:02 +08:00
|
|
|
|
{
|
2025-01-24 18:29:14 +08:00
|
|
|
|
case GroupDefinition group:
|
2025-01-31 19:58:32 +08:00
|
|
|
|
// Special case – collapsing an open group.
|
2025-01-28 21:53:17 +08:00
|
|
|
|
if (lastSelectedGroup == group)
|
|
|
|
|
{
|
2025-02-04 16:11:09 +08:00
|
|
|
|
setExpansionStateOfGroup(lastSelectedGroup, false);
|
2025-01-28 21:53:17 +08:00
|
|
|
|
lastSelectedGroup = null;
|
2025-02-06 15:21:18 +08:00
|
|
|
|
return;
|
2025-01-28 21:53:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-02-04 16:11:09 +08:00
|
|
|
|
setExpandedGroup(group);
|
2025-02-06 15:21:18 +08:00
|
|
|
|
return;
|
2025-01-23 15:11:02 +08:00
|
|
|
|
|
2025-01-24 18:29:14 +08:00
|
|
|
|
case BeatmapSetInfo setInfo:
|
|
|
|
|
// Selecting a set isn't valid – let's re-select the first difficulty.
|
|
|
|
|
CurrentSelection = setInfo.Beatmaps.First();
|
2025-02-06 15:21:18 +08:00
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
case BeatmapInfo beatmapInfo:
|
|
|
|
|
CurrentSelection = beatmapInfo;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected override void HandleItemSelected(object? model)
|
|
|
|
|
{
|
|
|
|
|
base.HandleItemSelected(model);
|
|
|
|
|
|
|
|
|
|
switch (model)
|
|
|
|
|
{
|
|
|
|
|
case BeatmapSetInfo:
|
|
|
|
|
case GroupDefinition:
|
|
|
|
|
throw new InvalidOperationException("Groups should never become selected");
|
2025-01-24 18:29:14 +08:00
|
|
|
|
|
|
|
|
|
case BeatmapInfo beatmapInfo:
|
2025-02-05 17:59:29 +08:00
|
|
|
|
// Find any containing group. There should never be too many groups so iterating is efficient enough.
|
|
|
|
|
GroupDefinition? containingGroup = grouping.GroupItems.SingleOrDefault(kvp => kvp.Value.Any(i => ReferenceEquals(i.Model, beatmapInfo))).Key;
|
2025-01-24 18:29:14 +08:00
|
|
|
|
|
2025-02-05 17:59:29 +08:00
|
|
|
|
if (containingGroup != null)
|
|
|
|
|
setExpandedGroup(containingGroup);
|
|
|
|
|
setExpandedSet(beatmapInfo);
|
2025-02-06 15:21:18 +08:00
|
|
|
|
break;
|
2025-01-24 18:29:14 +08:00
|
|
|
|
}
|
2025-01-23 22:58:51 +08:00
|
|
|
|
}
|
2025-01-23 15:11:02 +08:00
|
|
|
|
|
2025-01-31 19:58:32 +08:00
|
|
|
|
protected override bool CheckValidForGroupSelection(CarouselItem item)
|
|
|
|
|
{
|
|
|
|
|
switch (item.Model)
|
|
|
|
|
{
|
|
|
|
|
case BeatmapSetInfo:
|
|
|
|
|
return true;
|
|
|
|
|
|
|
|
|
|
case BeatmapInfo:
|
2025-02-06 13:30:15 +08:00
|
|
|
|
return !grouping.BeatmapSetsGroupedTogether;
|
2025-01-31 19:58:32 +08:00
|
|
|
|
|
|
|
|
|
case GroupDefinition:
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
throw new ArgumentException($"Unsupported model type {item.Model}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-04 16:11:09 +08:00
|
|
|
|
private void setExpandedGroup(GroupDefinition group)
|
2025-01-31 19:58:32 +08:00
|
|
|
|
{
|
|
|
|
|
if (lastSelectedGroup != null)
|
2025-02-04 16:11:09 +08:00
|
|
|
|
setExpansionStateOfGroup(lastSelectedGroup, false);
|
2025-01-31 19:58:32 +08:00
|
|
|
|
lastSelectedGroup = group;
|
2025-02-04 16:11:09 +08:00
|
|
|
|
setExpansionStateOfGroup(group, true);
|
2025-01-31 19:58:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-02-04 16:11:09 +08:00
|
|
|
|
private void setExpansionStateOfGroup(GroupDefinition group, bool expanded)
|
2025-01-23 22:58:51 +08:00
|
|
|
|
{
|
2025-01-24 18:29:14 +08:00
|
|
|
|
if (grouping.GroupItems.TryGetValue(group, out var items))
|
|
|
|
|
{
|
2025-02-06 16:09:58 +08:00
|
|
|
|
if (expanded)
|
2025-02-04 16:11:09 +08:00
|
|
|
|
{
|
2025-02-06 16:09:58 +08:00
|
|
|
|
foreach (var i in items)
|
|
|
|
|
{
|
|
|
|
|
switch (i.Model)
|
|
|
|
|
{
|
|
|
|
|
case GroupDefinition:
|
|
|
|
|
i.IsExpanded = true;
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case BeatmapSetInfo set:
|
|
|
|
|
// Case where there are set headers, header should be visible
|
|
|
|
|
// and items should use the set's expanded state.
|
|
|
|
|
i.IsVisible = true;
|
|
|
|
|
setExpansionStateOfSetItems(set, i.IsExpanded);
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
// Case where there are no set headers, all items should be visible.
|
|
|
|
|
if (!grouping.BeatmapSetsGroupedTogether)
|
|
|
|
|
i.IsVisible = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-02-04 16:11:09 +08:00
|
|
|
|
}
|
2025-02-06 16:09:58 +08:00
|
|
|
|
else
|
2025-02-05 17:59:29 +08:00
|
|
|
|
{
|
|
|
|
|
foreach (var i in items)
|
|
|
|
|
{
|
2025-02-06 16:09:58 +08:00
|
|
|
|
switch (i.Model)
|
|
|
|
|
{
|
|
|
|
|
case GroupDefinition:
|
|
|
|
|
i.IsExpanded = false;
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
i.IsVisible = false;
|
|
|
|
|
break;
|
|
|
|
|
}
|
2025-02-05 17:59:29 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2025-01-24 18:29:14 +08:00
|
|
|
|
}
|
2025-01-23 22:58:51 +08:00
|
|
|
|
}
|
2025-01-23 15:11:02 +08:00
|
|
|
|
|
2025-02-04 16:11:09 +08:00
|
|
|
|
private void setExpandedSet(BeatmapInfo beatmapInfo)
|
2025-01-31 19:58:32 +08:00
|
|
|
|
{
|
|
|
|
|
if (lastSelectedBeatmap != null)
|
2025-02-04 16:11:09 +08:00
|
|
|
|
setExpansionStateOfSetItems(lastSelectedBeatmap.BeatmapSet!, false);
|
2025-01-31 19:58:32 +08:00
|
|
|
|
lastSelectedBeatmap = beatmapInfo;
|
2025-02-04 16:11:09 +08:00
|
|
|
|
setExpansionStateOfSetItems(beatmapInfo.BeatmapSet!, true);
|
2025-01-31 19:58:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-02-04 16:11:09 +08:00
|
|
|
|
private void setExpansionStateOfSetItems(BeatmapSetInfo set, bool expanded)
|
2025-01-23 22:58:51 +08:00
|
|
|
|
{
|
2025-01-24 18:29:14 +08:00
|
|
|
|
if (grouping.SetItems.TryGetValue(set, out var items))
|
2025-01-23 15:11:02 +08:00
|
|
|
|
{
|
2025-01-24 18:29:14 +08:00
|
|
|
|
foreach (var i in items)
|
2025-02-04 16:11:09 +08:00
|
|
|
|
{
|
|
|
|
|
if (i.Model is BeatmapSetInfo)
|
|
|
|
|
i.IsExpanded = expanded;
|
|
|
|
|
else
|
|
|
|
|
i.IsVisible = expanded;
|
|
|
|
|
}
|
2025-01-23 15:11:02 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-23 22:53:09 +08:00
|
|
|
|
#endregion
|
2025-01-14 18:18:02 +08:00
|
|
|
|
|
2025-01-23 22:53:09 +08:00
|
|
|
|
#region Filtering
|
2025-01-14 18:18:02 +08:00
|
|
|
|
|
|
|
|
|
public FilterCriteria Criteria { get; private set; } = new FilterCriteria();
|
|
|
|
|
|
|
|
|
|
public void Filter(FilterCriteria criteria)
|
|
|
|
|
{
|
|
|
|
|
Criteria = criteria;
|
2025-01-14 19:23:53 +08:00
|
|
|
|
loading.Show();
|
2025-01-23 03:03:43 +08:00
|
|
|
|
FilterAsync().ContinueWith(_ => Schedule(() => loading.Hide()));
|
2025-01-14 18:18:02 +08:00
|
|
|
|
}
|
2025-01-23 22:53:09 +08:00
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Drawable pooling
|
|
|
|
|
|
2025-01-24 18:02:47 +08:00
|
|
|
|
private readonly DrawablePool<BeatmapPanel> beatmapPanelPool = new DrawablePool<BeatmapPanel>(100);
|
|
|
|
|
private readonly DrawablePool<BeatmapSetPanel> setPanelPool = new DrawablePool<BeatmapSetPanel>(100);
|
2025-01-24 18:29:14 +08:00
|
|
|
|
private readonly DrawablePool<GroupPanel> groupPanelPool = new DrawablePool<GroupPanel>(100);
|
2025-01-23 22:53:09 +08:00
|
|
|
|
|
|
|
|
|
private void setupPools()
|
|
|
|
|
{
|
2025-01-24 18:29:14 +08:00
|
|
|
|
AddInternal(groupPanelPool);
|
2025-01-24 18:02:47 +08:00
|
|
|
|
AddInternal(beatmapPanelPool);
|
|
|
|
|
AddInternal(setPanelPool);
|
2025-01-23 22:53:09 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-01-24 18:02:47 +08:00
|
|
|
|
protected override Drawable GetDrawableForDisplay(CarouselItem item)
|
|
|
|
|
{
|
|
|
|
|
switch (item.Model)
|
|
|
|
|
{
|
2025-01-24 18:29:14 +08:00
|
|
|
|
case GroupDefinition:
|
|
|
|
|
return groupPanelPool.Get();
|
|
|
|
|
|
2025-01-24 18:02:47 +08:00
|
|
|
|
case BeatmapInfo:
|
2025-01-24 18:29:14 +08:00
|
|
|
|
// TODO: if beatmap is a group selection target, it needs to be a different drawable
|
|
|
|
|
// with more information attached.
|
2025-01-24 18:02:47 +08:00
|
|
|
|
return beatmapPanelPool.Get();
|
|
|
|
|
|
|
|
|
|
case BeatmapSetInfo:
|
|
|
|
|
return setPanelPool.Get();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
throw new InvalidOperationException();
|
|
|
|
|
}
|
2025-01-23 22:53:09 +08:00
|
|
|
|
|
|
|
|
|
#endregion
|
2025-01-14 18:18:02 +08:00
|
|
|
|
}
|
2025-01-24 18:29:14 +08:00
|
|
|
|
|
|
|
|
|
public record GroupDefinition(string Title);
|
2025-01-14 18:18:02 +08:00
|
|
|
|
}
|