2017-02-07 12:59:30 +08:00
|
|
|
|
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
|
|
|
|
|
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
2016-12-06 17:56:20 +08:00
|
|
|
|
|
2017-02-02 16:33:39 +08:00
|
|
|
|
using OpenTK;
|
|
|
|
|
using osu.Framework.Graphics;
|
|
|
|
|
using osu.Framework.Graphics.Containers;
|
|
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.Linq;
|
|
|
|
|
using osu.Game.Beatmaps.Drawables;
|
2017-06-01 00:41:15 +08:00
|
|
|
|
using osu.Game.Configuration;
|
2016-12-15 21:57:14 +08:00
|
|
|
|
using osu.Framework.Input;
|
|
|
|
|
using OpenTK.Input;
|
2017-02-02 16:33:39 +08:00
|
|
|
|
using osu.Framework.MathUtils;
|
2017-03-04 15:54:14 +08:00
|
|
|
|
using System.Diagnostics;
|
2017-03-17 18:12:15 +08:00
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
|
using osu.Framework.Allocation;
|
2017-03-17 18:54:51 +08:00
|
|
|
|
using osu.Framework.Threading;
|
2017-06-01 00:41:15 +08:00
|
|
|
|
using osu.Framework.Configuration;
|
2017-07-26 12:22:46 +08:00
|
|
|
|
using osu.Game.Beatmaps;
|
2017-07-21 18:13:53 +08:00
|
|
|
|
using osu.Game.Graphics.Containers;
|
2017-09-14 14:41:32 +08:00
|
|
|
|
using osu.Game.Graphics.Cursor;
|
2016-11-25 17:14:56 +08:00
|
|
|
|
|
2016-11-21 03:34:16 +08:00
|
|
|
|
namespace osu.Game.Screens.Select
|
|
|
|
|
{
|
2017-11-21 10:49:42 +08:00
|
|
|
|
public class BeatmapCarousel : OsuScrollContainer
|
2016-11-21 03:34:16 +08:00
|
|
|
|
{
|
2017-03-17 18:12:15 +08:00
|
|
|
|
public BeatmapInfo SelectedBeatmap => selectedPanel?.Beatmap;
|
|
|
|
|
|
2017-07-20 10:50:31 +08:00
|
|
|
|
public override bool HandleInput => AllowSelection;
|
|
|
|
|
|
2017-03-17 18:12:15 +08:00
|
|
|
|
public Action BeatmapsChanged;
|
|
|
|
|
|
|
|
|
|
public IEnumerable<BeatmapSetInfo> Beatmaps
|
|
|
|
|
{
|
2017-10-18 17:36:35 +08:00
|
|
|
|
get { return groups.Select(g => g.BeatmapSet); }
|
2017-03-17 18:12:15 +08:00
|
|
|
|
|
|
|
|
|
set
|
|
|
|
|
{
|
|
|
|
|
scrollableContent.Clear(false);
|
|
|
|
|
panels.Clear();
|
|
|
|
|
groups.Clear();
|
|
|
|
|
|
2017-03-17 18:54:51 +08:00
|
|
|
|
List<BeatmapGroup> newGroups = null;
|
2017-03-17 18:12:15 +08:00
|
|
|
|
|
|
|
|
|
Task.Run(() =>
|
|
|
|
|
{
|
|
|
|
|
newGroups = value.Select(createGroup).ToList();
|
2017-03-17 18:54:51 +08:00
|
|
|
|
criteria.Filter(newGroups);
|
2017-03-17 18:12:15 +08:00
|
|
|
|
}).ContinueWith(t =>
|
|
|
|
|
{
|
|
|
|
|
Schedule(() =>
|
|
|
|
|
{
|
|
|
|
|
foreach (var g in newGroups)
|
2017-10-20 09:50:00 +08:00
|
|
|
|
if (g != null) addGroup(g);
|
2017-03-17 18:12:15 +08:00
|
|
|
|
|
2017-03-17 18:54:51 +08:00
|
|
|
|
computeYPositions();
|
2017-03-17 18:12:15 +08:00
|
|
|
|
BeatmapsChanged?.Invoke();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-03-23 12:41:50 +08:00
|
|
|
|
private readonly List<float> yPositions = new List<float>();
|
2017-03-17 18:12:15 +08:00
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Required for now unfortunately.
|
|
|
|
|
/// </summary>
|
2017-07-27 15:56:41 +08:00
|
|
|
|
private BeatmapManager manager;
|
2017-03-17 18:12:15 +08:00
|
|
|
|
|
2017-03-23 12:41:50 +08:00
|
|
|
|
private readonly Container<Panel> scrollableContent;
|
2017-03-17 18:12:15 +08:00
|
|
|
|
|
2017-03-23 12:41:50 +08:00
|
|
|
|
private readonly List<BeatmapGroup> groups = new List<BeatmapGroup>();
|
2017-03-17 18:12:15 +08:00
|
|
|
|
|
2017-06-01 00:41:15 +08:00
|
|
|
|
private Bindable<SelectionRandomType> randomType;
|
2017-06-01 14:54:48 +08:00
|
|
|
|
private readonly List<BeatmapGroup> seenGroups = new List<BeatmapGroup>();
|
2017-06-01 00:41:15 +08:00
|
|
|
|
|
2017-03-23 12:41:50 +08:00
|
|
|
|
private readonly List<Panel> panels = new List<Panel>();
|
2016-11-21 14:59:46 +08:00
|
|
|
|
|
2017-06-15 11:37:20 +08:00
|
|
|
|
private readonly Stack<KeyValuePair<BeatmapGroup, BeatmapPanel>> randomSelectedBeatmaps = new Stack<KeyValuePair<BeatmapGroup, BeatmapPanel>>();
|
2016-11-23 03:10:04 +08:00
|
|
|
|
|
2017-06-02 01:54:42 +08:00
|
|
|
|
private BeatmapGroup selectedGroup;
|
2017-03-17 18:12:15 +08:00
|
|
|
|
private BeatmapPanel selectedPanel;
|
2017-01-18 08:18:15 +08:00
|
|
|
|
|
2017-03-17 18:12:54 +08:00
|
|
|
|
public BeatmapCarousel()
|
2017-01-18 08:18:15 +08:00
|
|
|
|
{
|
2017-09-14 14:41:32 +08:00
|
|
|
|
Add(new OsuContextMenuContainer
|
2017-01-18 08:18:15 +08:00
|
|
|
|
{
|
|
|
|
|
RelativeSizeAxes = Axes.X,
|
2017-09-14 14:41:32 +08:00
|
|
|
|
AutoSizeAxes = Axes.Y,
|
|
|
|
|
Child = scrollableContent = new Container<Panel>
|
|
|
|
|
{
|
|
|
|
|
RelativeSizeAxes = Axes.X,
|
|
|
|
|
}
|
2017-01-18 08:18:15 +08:00
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2017-03-17 18:12:15 +08:00
|
|
|
|
public void AddBeatmap(BeatmapSetInfo beatmapSet)
|
2016-11-21 03:34:16 +08:00
|
|
|
|
{
|
2017-10-18 17:36:35 +08:00
|
|
|
|
Schedule(() =>
|
2017-03-17 18:12:15 +08:00
|
|
|
|
{
|
2017-10-18 17:36:35 +08:00
|
|
|
|
var group = createGroup(beatmapSet);
|
|
|
|
|
|
2017-10-20 09:50:00 +08:00
|
|
|
|
if (group == null)
|
|
|
|
|
return;
|
|
|
|
|
|
2017-03-17 18:12:15 +08:00
|
|
|
|
addGroup(group);
|
|
|
|
|
computeYPositions();
|
|
|
|
|
if (selectedGroup == null)
|
|
|
|
|
selectGroup(group);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2017-10-18 17:36:35 +08:00
|
|
|
|
public void RemoveBeatmap(BeatmapSetInfo beatmapSet)
|
|
|
|
|
{
|
|
|
|
|
Schedule(() => removeGroup(groups.Find(b => b.BeatmapSet.ID == beatmapSet.ID)));
|
|
|
|
|
}
|
2017-08-31 14:49:56 +08:00
|
|
|
|
|
2017-11-21 10:49:42 +08:00
|
|
|
|
public void UpdateBeatmap(BeatmapInfo beatmap)
|
2017-07-28 13:22:14 +08:00
|
|
|
|
{
|
2017-08-31 14:49:56 +08:00
|
|
|
|
// todo: this method should not run more than once for the same BeatmapSetInfo.
|
2017-10-25 21:07:32 +08:00
|
|
|
|
var set = manager.QueryBeatmapSet(s => s.ID == beatmap.BeatmapSetInfoID);
|
2017-08-31 14:49:56 +08:00
|
|
|
|
|
|
|
|
|
// todo: this method should be smarter as to not recreate panels that haven't changed, etc.
|
|
|
|
|
var group = groups.Find(b => b.BeatmapSet.ID == set.ID);
|
|
|
|
|
|
|
|
|
|
if (group == null)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
int i = groups.IndexOf(group);
|
|
|
|
|
groups.RemoveAt(i);
|
|
|
|
|
|
2017-10-20 09:50:00 +08:00
|
|
|
|
var newGroup = createGroup(set);
|
|
|
|
|
|
|
|
|
|
if (newGroup != null)
|
|
|
|
|
groups.Insert(i, newGroup);
|
|
|
|
|
|
|
|
|
|
bool hadSelection = selectedGroup == group;
|
|
|
|
|
|
|
|
|
|
if (hadSelection && newGroup == null)
|
2017-08-31 14:49:56 +08:00
|
|
|
|
selectedGroup = null;
|
|
|
|
|
|
|
|
|
|
Filter(null, false);
|
|
|
|
|
|
|
|
|
|
//check if we can/need to maintain our current selection.
|
2017-10-20 09:50:00 +08:00
|
|
|
|
if (hadSelection && newGroup != null)
|
2017-07-28 13:22:14 +08:00
|
|
|
|
{
|
2017-08-31 14:49:56 +08:00
|
|
|
|
var newSelection =
|
|
|
|
|
newGroup.BeatmapPanels.Find(p => p.Beatmap.ID == selectedPanel?.Beatmap.ID) ??
|
|
|
|
|
newGroup.BeatmapPanels[Math.Min(newGroup.BeatmapPanels.Count - 1, group.BeatmapPanels.IndexOf(selectedPanel))];
|
|
|
|
|
|
|
|
|
|
selectGroup(newGroup, newSelection);
|
|
|
|
|
}
|
2017-07-28 13:22:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
2017-03-17 18:12:15 +08:00
|
|
|
|
public void SelectBeatmap(BeatmapInfo beatmap, bool animated = true)
|
|
|
|
|
{
|
2017-09-01 17:13:21 +08:00
|
|
|
|
if (beatmap == null || beatmap.Hidden)
|
2017-03-17 18:12:15 +08:00
|
|
|
|
{
|
|
|
|
|
SelectNext();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-17 16:15:25 +08:00
|
|
|
|
if (beatmap == SelectedBeatmap) return;
|
|
|
|
|
|
2017-03-17 18:12:15 +08:00
|
|
|
|
foreach (BeatmapGroup group in groups)
|
|
|
|
|
{
|
|
|
|
|
var panel = group.BeatmapPanels.FirstOrDefault(p => p.Beatmap.Equals(beatmap));
|
|
|
|
|
if (panel != null)
|
|
|
|
|
{
|
|
|
|
|
selectGroup(group, panel, animated);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-04-28 19:43:42 +08:00
|
|
|
|
public Action<BeatmapInfo> SelectionChanged;
|
2017-03-17 18:12:15 +08:00
|
|
|
|
|
|
|
|
|
public Action StartRequested;
|
|
|
|
|
|
2017-08-30 20:12:46 +08:00
|
|
|
|
public Action<BeatmapSetInfo> DeleteRequested;
|
|
|
|
|
|
2017-08-31 14:49:56 +08:00
|
|
|
|
public Action<BeatmapSetInfo> RestoreRequested;
|
|
|
|
|
|
2017-10-06 23:59:14 +08:00
|
|
|
|
public Action<BeatmapInfo> EditRequested;
|
|
|
|
|
|
2017-09-04 06:59:32 +08:00
|
|
|
|
public Action<BeatmapInfo> HideDifficultyRequested;
|
2017-08-30 19:41:41 +08:00
|
|
|
|
|
2017-10-14 10:05:38 +08:00
|
|
|
|
private void selectNullBeatmap()
|
|
|
|
|
{
|
|
|
|
|
selectedGroup = null;
|
|
|
|
|
selectedPanel = null;
|
|
|
|
|
SelectionChanged?.Invoke(null);
|
|
|
|
|
}
|
|
|
|
|
|
2017-03-17 18:12:15 +08:00
|
|
|
|
public void SelectNext(int direction = 1, bool skipDifficulties = true)
|
|
|
|
|
{
|
2017-07-20 14:34:44 +08:00
|
|
|
|
if (groups.All(g => g.State == BeatmapGroupState.Hidden))
|
2017-03-17 18:12:15 +08:00
|
|
|
|
{
|
2017-10-14 10:05:38 +08:00
|
|
|
|
selectNullBeatmap();
|
2017-03-17 18:12:15 +08:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!skipDifficulties && selectedGroup != null)
|
|
|
|
|
{
|
|
|
|
|
int i = selectedGroup.BeatmapPanels.IndexOf(selectedPanel) + direction;
|
|
|
|
|
|
|
|
|
|
if (i >= 0 && i < selectedGroup.BeatmapPanels.Count)
|
|
|
|
|
{
|
|
|
|
|
//changing difficulty panel, not set.
|
|
|
|
|
selectGroup(selectedGroup, selectedGroup.BeatmapPanels[i]);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-04-18 09:04:32 +08:00
|
|
|
|
int startIndex = Math.Max(0, groups.IndexOf(selectedGroup));
|
2017-03-17 18:12:15 +08:00
|
|
|
|
int index = startIndex;
|
|
|
|
|
|
|
|
|
|
do
|
|
|
|
|
{
|
|
|
|
|
index = (index + direction + groups.Count) % groups.Count;
|
|
|
|
|
if (groups[index].State != BeatmapGroupState.Hidden)
|
|
|
|
|
{
|
2017-05-22 07:53:36 +08:00
|
|
|
|
if (skipDifficulties)
|
|
|
|
|
SelectBeatmap(groups[index].SelectedPanel != null ? groups[index].SelectedPanel.Beatmap : groups[index].BeatmapPanels.First().Beatmap);
|
|
|
|
|
else
|
|
|
|
|
SelectBeatmap(direction == 1 ? groups[index].BeatmapPanels.First().Beatmap : groups[index].BeatmapPanels.Last().Beatmap);
|
|
|
|
|
|
2017-03-17 18:12:15 +08:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
} while (index != startIndex);
|
|
|
|
|
}
|
|
|
|
|
|
2017-06-12 03:04:35 +08:00
|
|
|
|
private IEnumerable<BeatmapGroup> getVisibleGroups() => groups.Where(selectGroup => selectGroup.State != BeatmapGroupState.Hidden);
|
|
|
|
|
|
2017-06-05 17:24:28 +08:00
|
|
|
|
public void SelectNextRandom()
|
2017-03-17 18:12:15 +08:00
|
|
|
|
{
|
2017-07-24 15:57:12 +08:00
|
|
|
|
if (groups.Count == 0)
|
|
|
|
|
return;
|
|
|
|
|
|
2017-06-02 01:54:42 +08:00
|
|
|
|
var visibleGroups = getVisibleGroups();
|
2017-06-01 14:54:48 +08:00
|
|
|
|
if (!visibleGroups.Any())
|
2017-03-17 18:12:15 +08:00
|
|
|
|
return;
|
|
|
|
|
|
2017-09-01 17:44:51 +08:00
|
|
|
|
if (selectedGroup != null)
|
|
|
|
|
randomSelectedBeatmaps.Push(new KeyValuePair<BeatmapGroup, BeatmapPanel>(selectedGroup, selectedGroup.SelectedPanel));
|
2017-07-29 22:42:32 +08:00
|
|
|
|
|
2017-06-01 00:41:15 +08:00
|
|
|
|
BeatmapGroup group;
|
2017-06-02 01:54:42 +08:00
|
|
|
|
|
2017-06-01 00:41:15 +08:00
|
|
|
|
if (randomType == SelectionRandomType.RandomPermutation)
|
|
|
|
|
{
|
2017-06-02 01:54:42 +08:00
|
|
|
|
var notSeenGroups = visibleGroups.Except(seenGroups);
|
2017-06-01 00:41:15 +08:00
|
|
|
|
if (!notSeenGroups.Any())
|
2017-06-01 02:31:05 +08:00
|
|
|
|
{
|
2017-06-01 00:41:15 +08:00
|
|
|
|
seenGroups.Clear();
|
|
|
|
|
notSeenGroups = visibleGroups;
|
|
|
|
|
}
|
|
|
|
|
|
2017-06-01 14:54:48 +08:00
|
|
|
|
group = notSeenGroups.ElementAt(RNG.Next(notSeenGroups.Count()));
|
2017-06-01 00:41:15 +08:00
|
|
|
|
seenGroups.Add(group);
|
|
|
|
|
}
|
|
|
|
|
else
|
2017-06-01 14:54:48 +08:00
|
|
|
|
group = visibleGroups.ElementAt(RNG.Next(visibleGroups.Count()));
|
2017-06-01 00:41:15 +08:00
|
|
|
|
|
2017-05-22 07:59:10 +08:00
|
|
|
|
BeatmapPanel panel = group.BeatmapPanels[RNG.Next(group.BeatmapPanels.Count)];
|
2017-03-17 18:12:15 +08:00
|
|
|
|
|
|
|
|
|
selectGroup(group, panel);
|
|
|
|
|
}
|
|
|
|
|
|
2017-06-05 17:24:28 +08:00
|
|
|
|
public void SelectPreviousRandom()
|
2017-06-02 01:54:42 +08:00
|
|
|
|
{
|
|
|
|
|
if (!randomSelectedBeatmaps.Any())
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
var visibleGroups = getVisibleGroups();
|
|
|
|
|
if (!visibleGroups.Any())
|
|
|
|
|
return;
|
|
|
|
|
|
2017-06-05 17:24:28 +08:00
|
|
|
|
while (randomSelectedBeatmaps.Any())
|
2017-06-02 01:54:42 +08:00
|
|
|
|
{
|
2017-06-15 11:37:20 +08:00
|
|
|
|
var beatmapCoordinates = randomSelectedBeatmaps.Pop();
|
|
|
|
|
var group = beatmapCoordinates.Key;
|
2017-06-05 17:24:28 +08:00
|
|
|
|
if (visibleGroups.Contains(group))
|
2017-06-02 01:54:42 +08:00
|
|
|
|
{
|
2017-06-15 11:37:20 +08:00
|
|
|
|
selectGroup(group, beatmapCoordinates.Value);
|
2017-06-02 01:54:42 +08:00
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-03-17 18:54:51 +08:00
|
|
|
|
private FilterCriteria criteria = new FilterCriteria();
|
|
|
|
|
|
|
|
|
|
private ScheduledDelegate filterTask;
|
|
|
|
|
|
2017-07-20 10:50:31 +08:00
|
|
|
|
public bool AllowSelection = true;
|
|
|
|
|
|
2017-07-21 16:20:52 +08:00
|
|
|
|
public void FlushPendingFilters()
|
|
|
|
|
{
|
|
|
|
|
if (filterTask?.Completed == false)
|
|
|
|
|
Filter(null, false);
|
|
|
|
|
}
|
2017-07-21 04:58:58 +08:00
|
|
|
|
|
2017-03-17 18:54:51 +08:00
|
|
|
|
public void Filter(FilterCriteria newCriteria = null, bool debounce = true)
|
2017-03-17 18:12:15 +08:00
|
|
|
|
{
|
2017-03-29 18:47:53 +08:00
|
|
|
|
if (newCriteria != null)
|
|
|
|
|
criteria = newCriteria;
|
2017-03-17 18:54:51 +08:00
|
|
|
|
|
2017-04-05 16:27:01 +08:00
|
|
|
|
if (!IsLoaded) return;
|
|
|
|
|
|
2017-03-17 18:54:51 +08:00
|
|
|
|
Action perform = delegate
|
2016-11-25 17:14:56 +08:00
|
|
|
|
{
|
2017-03-17 18:54:51 +08:00
|
|
|
|
filterTask = null;
|
|
|
|
|
|
|
|
|
|
criteria.Filter(groups);
|
2016-11-21 03:34:16 +08:00
|
|
|
|
|
2017-03-17 18:54:51 +08:00
|
|
|
|
var filtered = new List<BeatmapGroup>(groups);
|
|
|
|
|
|
|
|
|
|
scrollableContent.Clear(false);
|
|
|
|
|
panels.Clear();
|
|
|
|
|
groups.Clear();
|
|
|
|
|
|
|
|
|
|
foreach (var g in filtered)
|
|
|
|
|
addGroup(g);
|
|
|
|
|
|
|
|
|
|
computeYPositions();
|
2017-03-17 18:12:15 +08:00
|
|
|
|
|
2017-03-17 19:09:23 +08:00
|
|
|
|
if (selectedGroup == null || selectedGroup.State == BeatmapGroupState.Hidden)
|
2017-03-17 18:54:51 +08:00
|
|
|
|
SelectNext();
|
2017-03-29 19:01:46 +08:00
|
|
|
|
else
|
2017-05-17 16:15:25 +08:00
|
|
|
|
selectGroup(selectedGroup, selectedPanel);
|
2017-03-17 18:54:51 +08:00
|
|
|
|
};
|
2017-03-17 18:12:15 +08:00
|
|
|
|
|
2017-03-17 18:54:51 +08:00
|
|
|
|
filterTask?.Cancel();
|
2017-07-21 04:58:58 +08:00
|
|
|
|
filterTask = null;
|
|
|
|
|
|
2017-03-17 18:54:51 +08:00
|
|
|
|
if (debounce)
|
|
|
|
|
filterTask = Scheduler.AddDelayed(perform, 250);
|
|
|
|
|
else
|
|
|
|
|
perform();
|
2016-11-21 03:34:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
2017-07-29 21:03:17 +08:00
|
|
|
|
public void ScrollToSelected(bool animated = true)
|
|
|
|
|
{
|
|
|
|
|
float selectedY = computeYPositions(animated);
|
|
|
|
|
ScrollTo(selectedY, animated);
|
|
|
|
|
}
|
|
|
|
|
|
2017-03-17 18:12:15 +08:00
|
|
|
|
private BeatmapGroup createGroup(BeatmapSetInfo beatmapSet)
|
|
|
|
|
{
|
2017-10-20 09:50:00 +08:00
|
|
|
|
if (beatmapSet.Beatmaps.All(b => b.Hidden))
|
|
|
|
|
return null;
|
|
|
|
|
|
2017-06-07 19:53:37 +08:00
|
|
|
|
foreach (var b in beatmapSet.Beatmaps)
|
2017-04-18 09:04:32 +08:00
|
|
|
|
{
|
|
|
|
|
if (b.Metadata == null)
|
|
|
|
|
b.Metadata = beatmapSet.Metadata;
|
2017-05-06 15:37:53 +08:00
|
|
|
|
}
|
2017-03-17 18:12:15 +08:00
|
|
|
|
|
2017-07-27 15:56:41 +08:00
|
|
|
|
return new BeatmapGroup(beatmapSet, manager)
|
2017-03-17 18:12:15 +08:00
|
|
|
|
{
|
2017-04-28 19:43:42 +08:00
|
|
|
|
SelectionChanged = (g, p) => selectGroup(g, p),
|
2017-03-17 18:12:15 +08:00
|
|
|
|
StartRequested = b => StartRequested?.Invoke(),
|
2017-08-30 19:41:41 +08:00
|
|
|
|
DeleteRequested = b => DeleteRequested?.Invoke(b),
|
2017-08-31 14:49:56 +08:00
|
|
|
|
RestoreHiddenRequested = s => RestoreRequested?.Invoke(s),
|
2017-10-06 23:59:14 +08:00
|
|
|
|
EditRequested = b => EditRequested?.Invoke(b),
|
2017-09-04 06:59:32 +08:00
|
|
|
|
HideDifficultyRequested = b => HideDifficultyRequested?.Invoke(b),
|
2017-03-17 18:12:15 +08:00
|
|
|
|
State = BeatmapGroupState.Collapsed
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[BackgroundDependencyLoader(permitNulls: true)]
|
2017-07-27 15:56:41 +08:00
|
|
|
|
private void load(BeatmapManager manager, OsuConfigManager config)
|
2017-03-17 18:12:15 +08:00
|
|
|
|
{
|
2017-07-27 15:56:41 +08:00
|
|
|
|
this.manager = manager;
|
2017-07-18 17:43:28 +08:00
|
|
|
|
|
2017-06-01 00:41:15 +08:00
|
|
|
|
randomType = config.GetBindable<SelectionRandomType>(OsuSetting.SelectionRandomType);
|
2017-03-17 18:12:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void addGroup(BeatmapGroup group)
|
|
|
|
|
{
|
|
|
|
|
groups.Add(group);
|
|
|
|
|
panels.Add(group.Header);
|
|
|
|
|
panels.AddRange(group.BeatmapPanels);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void removeGroup(BeatmapGroup group)
|
2017-02-24 13:37:54 +08:00
|
|
|
|
{
|
2017-07-24 16:25:33 +08:00
|
|
|
|
if (group == null)
|
|
|
|
|
return;
|
|
|
|
|
|
2017-10-12 01:25:12 +08:00
|
|
|
|
if (selectedGroup == group)
|
2017-10-14 10:05:38 +08:00
|
|
|
|
{
|
|
|
|
|
if (getVisibleGroups().Count() == 1)
|
|
|
|
|
selectNullBeatmap();
|
|
|
|
|
else
|
|
|
|
|
SelectNext();
|
|
|
|
|
}
|
2017-10-12 01:25:12 +08:00
|
|
|
|
|
2017-02-24 13:37:54 +08:00
|
|
|
|
groups.Remove(group);
|
2017-03-14 04:35:43 +08:00
|
|
|
|
panels.Remove(group.Header);
|
2017-03-04 16:34:28 +08:00
|
|
|
|
foreach (var p in group.BeatmapPanels)
|
|
|
|
|
panels.Remove(p);
|
|
|
|
|
|
2017-02-24 13:37:54 +08:00
|
|
|
|
scrollableContent.Remove(group.Header);
|
2017-07-11 21:58:06 +08:00
|
|
|
|
scrollableContent.RemoveRange(group.BeatmapPanels);
|
2017-02-24 13:37:54 +08:00
|
|
|
|
|
2017-03-17 18:12:15 +08:00
|
|
|
|
computeYPositions();
|
2016-11-23 03:10:04 +08:00
|
|
|
|
}
|
|
|
|
|
|
2016-11-24 00:42:21 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Computes the target Y positions for every panel in the carousel.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <returns>The Y position of the currently selected panel.</returns>
|
2017-02-02 16:33:39 +08:00
|
|
|
|
private float computeYPositions(bool animated = true)
|
2016-11-21 03:34:16 +08:00
|
|
|
|
{
|
2016-11-23 04:40:47 +08:00
|
|
|
|
yPositions.Clear();
|
|
|
|
|
|
2016-11-21 14:59:46 +08:00
|
|
|
|
float currentY = DrawHeight / 2;
|
|
|
|
|
float selectedY = currentY;
|
|
|
|
|
|
2016-11-21 03:34:16 +08:00
|
|
|
|
foreach (BeatmapGroup group in groups)
|
|
|
|
|
{
|
2017-02-02 16:33:39 +08:00
|
|
|
|
movePanel(group.Header, group.State != BeatmapGroupState.Hidden, animated, ref currentY);
|
2016-11-21 03:34:16 +08:00
|
|
|
|
|
|
|
|
|
if (group.State == BeatmapGroupState.Expanded)
|
|
|
|
|
{
|
2017-07-23 02:50:25 +08:00
|
|
|
|
group.Header.MoveToX(-100, 500, Easing.OutExpo);
|
2016-11-25 11:50:18 +08:00
|
|
|
|
var headerY = group.Header.Position.Y;
|
2016-11-23 03:10:04 +08:00
|
|
|
|
|
2016-11-21 03:34:16 +08:00
|
|
|
|
foreach (BeatmapPanel panel in group.BeatmapPanels)
|
|
|
|
|
{
|
2017-03-17 18:12:15 +08:00
|
|
|
|
if (panel == selectedPanel)
|
2016-11-23 04:40:47 +08:00
|
|
|
|
selectedY = currentY + panel.DrawHeight / 2 - DrawHeight / 2;
|
2016-11-21 14:59:46 +08:00
|
|
|
|
|
2017-07-23 02:50:25 +08:00
|
|
|
|
panel.MoveToX(-50, 500, Easing.OutExpo);
|
2016-11-25 03:25:29 +08:00
|
|
|
|
|
2016-11-25 11:50:18 +08:00
|
|
|
|
//on first display we want to begin hidden under our group's header.
|
2017-02-02 08:15:22 +08:00
|
|
|
|
if (panel.Alpha == 0)
|
2016-11-25 11:50:18 +08:00
|
|
|
|
panel.MoveToY(headerY);
|
|
|
|
|
|
2017-02-02 16:33:39 +08:00
|
|
|
|
movePanel(panel, true, animated, ref currentY);
|
2016-11-21 03:34:16 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2016-11-23 03:10:04 +08:00
|
|
|
|
else
|
|
|
|
|
{
|
2017-07-23 02:50:25 +08:00
|
|
|
|
group.Header.MoveToX(0, 500, Easing.OutExpo);
|
2016-11-23 04:40:47 +08:00
|
|
|
|
|
|
|
|
|
foreach (BeatmapPanel panel in group.BeatmapPanels)
|
2016-11-25 03:25:29 +08:00
|
|
|
|
{
|
2017-07-23 02:50:25 +08:00
|
|
|
|
panel.MoveToX(0, 500, Easing.OutExpo);
|
2017-02-02 16:33:39 +08:00
|
|
|
|
movePanel(panel, false, animated, ref currentY);
|
2016-11-25 03:25:29 +08:00
|
|
|
|
}
|
2016-11-23 03:10:04 +08:00
|
|
|
|
}
|
2016-11-21 03:34:16 +08:00
|
|
|
|
}
|
2016-11-21 14:59:46 +08:00
|
|
|
|
|
|
|
|
|
currentY += DrawHeight / 2;
|
|
|
|
|
scrollableContent.Height = currentY;
|
|
|
|
|
|
2016-11-24 00:42:21 +08:00
|
|
|
|
return selectedY;
|
2016-11-21 14:59:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
2017-03-17 18:12:15 +08:00
|
|
|
|
private void movePanel(Panel panel, bool advance, bool animated, ref float currentY)
|
2016-11-21 03:34:16 +08:00
|
|
|
|
{
|
2017-03-17 18:12:15 +08:00
|
|
|
|
yPositions.Add(currentY);
|
2017-07-23 02:50:25 +08:00
|
|
|
|
panel.MoveToY(currentY, animated ? 750 : 0, Easing.OutExpo);
|
2017-03-17 18:12:15 +08:00
|
|
|
|
|
|
|
|
|
if (advance)
|
|
|
|
|
currentY += panel.DrawHeight + 5;
|
2016-11-21 03:34:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
2017-03-17 18:12:15 +08:00
|
|
|
|
private void selectGroup(BeatmapGroup group, BeatmapPanel panel = null, bool animated = true)
|
2016-11-21 03:34:16 +08:00
|
|
|
|
{
|
2017-04-28 21:17:35 +08:00
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
if (panel == null)
|
|
|
|
|
panel = group.BeatmapPanels.First();
|
2017-03-17 18:12:15 +08:00
|
|
|
|
|
2017-04-28 21:17:35 +08:00
|
|
|
|
if (selectedPanel == panel) return;
|
2017-04-28 19:31:09 +08:00
|
|
|
|
|
2017-04-28 21:17:35 +08:00
|
|
|
|
Trace.Assert(group.BeatmapPanels.Contains(panel), @"Selected panel must be in provided group");
|
2017-03-04 21:05:02 +08:00
|
|
|
|
|
2017-04-28 21:17:35 +08:00
|
|
|
|
if (selectedGroup != null && selectedGroup != group && selectedGroup.State != BeatmapGroupState.Hidden)
|
|
|
|
|
selectedGroup.State = BeatmapGroupState.Collapsed;
|
2016-11-21 03:34:16 +08:00
|
|
|
|
|
2017-04-28 21:17:35 +08:00
|
|
|
|
group.State = BeatmapGroupState.Expanded;
|
2017-05-17 16:15:25 +08:00
|
|
|
|
group.SelectedPanel = panel;
|
|
|
|
|
|
2017-04-28 21:17:35 +08:00
|
|
|
|
panel.State = PanelSelectedState.Selected;
|
2016-11-21 03:34:16 +08:00
|
|
|
|
|
2017-05-01 13:13:00 +08:00
|
|
|
|
if (selectedPanel == panel) return;
|
|
|
|
|
|
2017-04-28 21:17:35 +08:00
|
|
|
|
selectedPanel = panel;
|
|
|
|
|
selectedGroup = group;
|
2017-04-28 19:43:42 +08:00
|
|
|
|
|
2017-04-28 21:17:35 +08:00
|
|
|
|
SelectionChanged?.Invoke(panel.Beatmap);
|
|
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
2017-07-29 21:03:17 +08:00
|
|
|
|
ScrollToSelected(animated);
|
2017-04-28 21:17:35 +08:00
|
|
|
|
}
|
2016-11-21 03:34:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
2017-03-17 18:12:15 +08:00
|
|
|
|
protected override bool OnKeyDown(InputState state, KeyDownEventArgs args)
|
2017-03-02 21:01:53 +08:00
|
|
|
|
{
|
2017-03-17 18:12:15 +08:00
|
|
|
|
int direction = 0;
|
|
|
|
|
bool skipDifficulties = false;
|
|
|
|
|
|
|
|
|
|
switch (args.Key)
|
2017-03-02 21:01:53 +08:00
|
|
|
|
{
|
2017-03-17 18:12:15 +08:00
|
|
|
|
case Key.Up:
|
|
|
|
|
direction = -1;
|
2017-02-18 00:41:53 +08:00
|
|
|
|
break;
|
2017-03-17 18:12:15 +08:00
|
|
|
|
case Key.Down:
|
|
|
|
|
direction = 1;
|
2017-02-18 00:41:53 +08:00
|
|
|
|
break;
|
2017-03-17 18:12:15 +08:00
|
|
|
|
case Key.Left:
|
|
|
|
|
direction = -1;
|
|
|
|
|
skipDifficulties = true;
|
2017-02-18 00:41:53 +08:00
|
|
|
|
break;
|
2017-03-17 18:12:15 +08:00
|
|
|
|
case Key.Right:
|
|
|
|
|
direction = 1;
|
|
|
|
|
skipDifficulties = true;
|
2017-03-16 08:18:20 +08:00
|
|
|
|
break;
|
2017-02-18 00:41:53 +08:00
|
|
|
|
}
|
2017-02-18 06:32:14 +08:00
|
|
|
|
|
2017-03-17 18:12:15 +08:00
|
|
|
|
if (direction == 0)
|
|
|
|
|
return base.OnKeyDown(state, args);
|
2016-12-10 18:30:22 +08:00
|
|
|
|
|
2017-03-17 18:12:15 +08:00
|
|
|
|
SelectNext(direction, skipDifficulties);
|
|
|
|
|
return true;
|
2016-12-10 18:30:22 +08:00
|
|
|
|
}
|
|
|
|
|
|
2016-11-21 03:34:16 +08:00
|
|
|
|
protected override void Update()
|
|
|
|
|
{
|
|
|
|
|
base.Update();
|
|
|
|
|
|
2016-11-23 04:40:47 +08:00
|
|
|
|
float drawHeight = DrawHeight;
|
2016-11-25 17:14:56 +08:00
|
|
|
|
|
2017-03-04 15:54:14 +08:00
|
|
|
|
// Remove all panels that should no longer be on-screen
|
2017-10-18 17:36:35 +08:00
|
|
|
|
scrollableContent.RemoveAll(delegate(Panel p)
|
2017-01-18 08:18:15 +08:00
|
|
|
|
{
|
|
|
|
|
float panelPosY = p.Position.Y;
|
2017-03-04 15:54:14 +08:00
|
|
|
|
bool remove = panelPosY < Current - p.DrawHeight || panelPosY > Current + drawHeight || !p.IsPresent;
|
|
|
|
|
return remove;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Find index range of all panels that should be on-screen
|
|
|
|
|
Trace.Assert(panels.Count == yPositions.Count);
|
2016-11-21 03:34:16 +08:00
|
|
|
|
|
2016-11-23 04:40:47 +08:00
|
|
|
|
int firstIndex = yPositions.BinarySearch(Current - Panel.MAX_HEIGHT);
|
|
|
|
|
if (firstIndex < 0) firstIndex = ~firstIndex;
|
|
|
|
|
int lastIndex = yPositions.BinarySearch(Current + drawHeight);
|
2017-05-21 09:04:12 +08:00
|
|
|
|
if (lastIndex < 0)
|
|
|
|
|
{
|
|
|
|
|
lastIndex = ~lastIndex;
|
|
|
|
|
|
|
|
|
|
// Add the first panel of the last visible beatmap group to preload its data.
|
2017-05-21 20:08:04 +08:00
|
|
|
|
if (lastIndex != 0 && panels[lastIndex - 1] is BeatmapSetHeader)
|
2017-05-21 09:04:12 +08:00
|
|
|
|
lastIndex++;
|
|
|
|
|
}
|
2016-11-21 03:34:16 +08:00
|
|
|
|
|
2017-03-04 15:54:14 +08:00
|
|
|
|
// Add those panels within the previously found index range that should be displayed.
|
2016-11-25 17:14:56 +08:00
|
|
|
|
for (int i = firstIndex; i < lastIndex; ++i)
|
2016-11-21 03:34:16 +08:00
|
|
|
|
{
|
2017-03-04 15:54:14 +08:00
|
|
|
|
Panel panel = panels[i];
|
|
|
|
|
if (panel.State == PanelSelectedState.Hidden)
|
|
|
|
|
continue;
|
|
|
|
|
|
2017-03-04 16:34:39 +08:00
|
|
|
|
// Only add if we're not already part of the content.
|
2017-03-04 15:54:14 +08:00
|
|
|
|
if (!scrollableContent.Contains(panel))
|
|
|
|
|
{
|
2017-03-04 16:34:39 +08:00
|
|
|
|
// Makes sure headers are always _below_ panels,
|
|
|
|
|
// and depth flows downward.
|
|
|
|
|
panel.Depth = i + (panel is BeatmapSetHeader ? panels.Count : 0);
|
2017-11-21 21:27:56 +08:00
|
|
|
|
|
|
|
|
|
switch (panel.LoadState)
|
|
|
|
|
{
|
|
|
|
|
case LoadState.NotLoaded:
|
|
|
|
|
LoadComponentAsync(panel);
|
|
|
|
|
break;
|
|
|
|
|
case LoadState.Loading:
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
scrollableContent.Add(panel);
|
|
|
|
|
break;
|
|
|
|
|
}
|
2017-03-04 15:54:14 +08:00
|
|
|
|
}
|
2016-11-21 03:34:16 +08:00
|
|
|
|
}
|
2017-03-04 15:54:14 +08:00
|
|
|
|
|
2017-03-04 16:34:39 +08:00
|
|
|
|
// Update externally controlled state of currently visible panels
|
|
|
|
|
// (e.g. x-offset and opacity).
|
2017-03-04 15:54:14 +08:00
|
|
|
|
float halfHeight = drawHeight / 2;
|
|
|
|
|
foreach (Panel p in scrollableContent.Children)
|
|
|
|
|
updatePanel(p, halfHeight);
|
2016-11-21 03:34:16 +08:00
|
|
|
|
}
|
2016-12-15 21:57:14 +08:00
|
|
|
|
|
2017-03-17 18:12:15 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Computes the x-offset of currently visible panels. 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)
|
2016-12-15 21:57:14 +08:00
|
|
|
|
{
|
2017-03-17 18:12:15 +08:00
|
|
|
|
// The radius of the circle the carousel moves on.
|
|
|
|
|
const float circle_radius = 3;
|
|
|
|
|
double discriminant = Math.Max(0, circle_radius * circle_radius - dist * dist);
|
|
|
|
|
float x = (circle_radius - (float)Math.Sqrt(discriminant)) * halfHeight;
|
2017-02-02 18:37:35 +08:00
|
|
|
|
|
2017-03-17 18:12:15 +08:00
|
|
|
|
return 125 + x;
|
2017-02-24 13:37:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
2017-03-17 18:12:15 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Update a panel's x position and multiplicative alpha based on its y position and
|
|
|
|
|
/// the current scroll position.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="p">The panel to be updated.</param>
|
|
|
|
|
/// <param name="halfHeight">Half the draw height of the carousel container.</param>
|
|
|
|
|
private void updatePanel(Panel p, float halfHeight)
|
2017-02-24 13:37:54 +08:00
|
|
|
|
{
|
2017-03-17 18:12:15 +08:00
|
|
|
|
var height = p.IsPresent ? p.DrawHeight : 0;
|
2017-02-02 16:33:39 +08:00
|
|
|
|
|
2017-03-17 18:12:15 +08:00
|
|
|
|
float panelDrawY = p.Position.Y - Current + height / 2;
|
|
|
|
|
float dist = Math.Abs(1f - panelDrawY / halfHeight);
|
2017-03-04 21:05:02 +08:00
|
|
|
|
|
2017-03-17 18:12:15 +08: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 panel gets selected, we
|
|
|
|
|
// may want to smoothly transform it leftwards.)
|
|
|
|
|
p.OriginPosition = new Vector2(-offsetX(dist, halfHeight), 0);
|
2017-03-04 21:05:02 +08:00
|
|
|
|
|
2017-03-17 18:12:15 +08: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.
|
|
|
|
|
p.SetMultiplicativeAlpha(MathHelper.Clamp(1.75f - 1.5f * dist, 0, 1));
|
2016-12-15 21:57:14 +08:00
|
|
|
|
}
|
2016-11-21 03:34:16 +08:00
|
|
|
|
}
|
|
|
|
|
}
|