From dc28f8c79ea7d0636a00b593140211cd196c9381 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Mar 2017 19:12:15 +0900 Subject: [PATCH] Remove all external access to BeatmapGroup. --- osu.Game/Screens/Select/CarouselContainer.cs | 431 ++++++++++++------- osu.Game/Screens/Select/SongSelect.cs | 100 +---- 2 files changed, 287 insertions(+), 244 deletions(-) diff --git a/osu.Game/Screens/Select/CarouselContainer.cs b/osu.Game/Screens/Select/CarouselContainer.cs index 092e4461e0..6277abd3cb 100644 --- a/osu.Game/Screens/Select/CarouselContainer.cs +++ b/osu.Game/Screens/Select/CarouselContainer.cs @@ -15,47 +15,227 @@ using OpenTK.Input; using System.Collections; using osu.Framework.MathUtils; using System.Diagnostics; +using System.Threading.Tasks; +using osu.Framework.Allocation; using osu.Game.Screens.Select.Filter; namespace osu.Game.Screens.Select { internal class CarouselContainer : ScrollContainer, IEnumerable { - private Container scrollableContent; - private List groups = new List(); - private List panels = new List(); + public BeatmapInfo SelectedBeatmap => selectedPanel?.Beatmap; - public BeatmapGroup SelectedGroup { get; private set; } - public BeatmapPanel SelectedPanel { get; private set; } + public Action BeatmapsChanged; + + public IEnumerable Beatmaps + { + get + { + return groups.Select(g => g.BeatmapSet); + } + + set + { + scrollableContent.Clear(false); + panels.Clear(); + groups.Clear(); + + IEnumerable newGroups = null; + + Task.Run(() => + { + newGroups = value.Select(createGroup).ToList(); + }).ContinueWith(t => + { + Schedule(() => + { + foreach (var g in newGroups) + addGroup(g); + computeYPositions(); + + BeatmapsChanged?.Invoke(); + }); + }); + } + } private List yPositions = new List(); + /// + /// Required for now unfortunately. + /// + private BeatmapDatabase database; + + private Container scrollableContent; + + private List groups = new List(); + + private List panels = new List(); + + private BeatmapGroup selectedGroup; + + private BeatmapPanel selectedPanel; + public CarouselContainer() { - DistanceDecayJump = 0.01; - Add(scrollableContent = new Container { RelativeSizeAxes = Axes.X, }); } - public void AddGroup(BeatmapGroup group) + public void AddBeatmap(BeatmapSetInfo beatmapSet) { - groups.Add(group); + var group = createGroup(beatmapSet); - panels.Add(group.Header); - group.Header.UpdateClock(Clock); - foreach (BeatmapPanel panel in group.BeatmapPanels) + //for the time being, let's completely load the difficulty panels in the background. + //this likely won't scale so well, but allows us to completely async the loading flow. + Schedule(delegate { - panels.Add(panel); - panel.UpdateClock(Clock); + addGroup(group); + computeYPositions(); + if (selectedGroup == null) + selectGroup(group); + }); + } + + public void SelectBeatmap(BeatmapInfo beatmap, bool animated = true) + { + if (beatmap == null) + { + SelectNext(); + return; } + foreach (BeatmapGroup group in groups) + { + var panel = group.BeatmapPanels.FirstOrDefault(p => p.Beatmap.Equals(beatmap)); + if (panel != null) + { + selectGroup(group, panel, animated); + return; + } + } + } + + public void RemoveBeatmap(BeatmapSetInfo info) => removeGroup(groups.Find(b => b.BeatmapSet.ID == info.ID)); + + public Action SelectionChanged; + + public Action StartRequested; + + public void SelectNext(int direction = 1, bool skipDifficulties = true) + { + if (groups.Count == 0) + { + selectedGroup = null; + selectedPanel = null; + 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; + } + } + + int startIndex = groups.IndexOf(selectedGroup); + int index = startIndex; + + do + { + index = (index + direction + groups.Count) % groups.Count; + if (groups[index].State != BeatmapGroupState.Hidden) + { + SelectBeatmap(groups[index].BeatmapPanels.First().Beatmap); + return; + } + } while (index != startIndex); + } + + public void SelectRandom() + { + List visibleGroups = groups.Where(selectGroup => selectGroup.State != BeatmapGroupState.Hidden).ToList(); + if (visibleGroups.Count < 1) + return; + BeatmapGroup group = visibleGroups[RNG.Next(visibleGroups.Count)]; + BeatmapPanel panel = group?.BeatmapPanels.First(); + + if (panel == null) + return; + + selectGroup(group, panel); + } + + public void Sort(SortMode mode) + { + List sortedGroups = new List(groups); + switch (mode) + { + case SortMode.Artist: + sortedGroups.Sort((x, y) => string.Compare(x.BeatmapSet.Metadata.Artist, y.BeatmapSet.Metadata.Artist, StringComparison.InvariantCultureIgnoreCase)); + break; + case SortMode.Title: + sortedGroups.Sort((x, y) => string.Compare(x.BeatmapSet.Metadata.Title, y.BeatmapSet.Metadata.Title, StringComparison.InvariantCultureIgnoreCase)); + break; + case SortMode.Author: + sortedGroups.Sort((x, y) => string.Compare(x.BeatmapSet.Metadata.Author, y.BeatmapSet.Metadata.Author, StringComparison.InvariantCultureIgnoreCase)); + break; + case SortMode.Difficulty: + sortedGroups.Sort((x, y) => x.BeatmapSet.MaxStarDifficulty.CompareTo(y.BeatmapSet.MaxStarDifficulty)); + break; + default: + Sort(SortMode.Artist); // Temporary + break; + } + + scrollableContent.Clear(false); + panels.Clear(); + groups.Clear(); + + foreach (var g in sortedGroups) + addGroup(g); + computeYPositions(); } - public void RemoveGroup(BeatmapGroup group) + public IEnumerator GetEnumerator() => groups.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + private BeatmapGroup createGroup(BeatmapSetInfo beatmapSet) + { + database.GetChildren(beatmapSet); + beatmapSet.Beatmaps.ForEach(b => { if (b.Metadata == null) b.Metadata = beatmapSet.Metadata; }); + + return new BeatmapGroup(beatmapSet, database) + { + SelectionChanged = SelectionChanged, + StartRequested = b => StartRequested?.Invoke(), + State = BeatmapGroupState.Collapsed + }; + } + + [BackgroundDependencyLoader(permitNulls: true)] + private void load(BeatmapDatabase database) + { + this.database = database; + } + + private void addGroup(BeatmapGroup group) + { + groups.Add(group); + panels.Add(group.Header); + panels.AddRange(group.BeatmapPanels); + } + + private void removeGroup(BeatmapGroup group) { groups.Remove(group); panels.Remove(group.Header); @@ -65,18 +245,12 @@ namespace osu.Game.Screens.Select scrollableContent.Remove(group.Header); scrollableContent.Remove(group.BeatmapPanels); + if (selectedGroup == group) + SelectNext(); + computeYPositions(); } - private void movePanel(Panel panel, bool advance, bool animated, ref float currentY) - { - yPositions.Add(currentY); - panel.MoveToY(currentY, animated ? 750 : 0, EasingTypes.OutExpo); - - if (advance) - currentY += panel.DrawHeight + 5; - } - /// /// Computes the target Y positions for every panel in the carousel. /// @@ -99,7 +273,7 @@ namespace osu.Game.Screens.Select foreach (BeatmapPanel panel in group.BeatmapPanels) { - if (panel == SelectedPanel) + if (panel == selectedPanel) selectedY = currentY + panel.DrawHeight / 2 - DrawHeight / 2; panel.MoveToX(-50, 500, EasingTypes.OutExpo); @@ -129,105 +303,62 @@ namespace osu.Game.Screens.Select return selectedY; } - public void SelectBeatmap(BeatmapInfo beatmap, bool animated = true) + private void movePanel(Panel panel, bool advance, bool animated, ref float currentY) { - foreach (BeatmapGroup group in groups) - { - var panel = group.BeatmapPanels.FirstOrDefault(p => p.Beatmap.Equals(beatmap)); - if (panel != null) - { - selectGroup(group, panel, animated); - return; - } - } + yPositions.Add(currentY); + panel.MoveToY(currentY, animated ? 750 : 0, EasingTypes.OutExpo); + + if (advance) + currentY += panel.DrawHeight + 5; } - private void selectGroup(BeatmapGroup group, BeatmapPanel panel, bool animated = true) + private void selectGroup(BeatmapGroup group, BeatmapPanel panel = null, bool animated = true) { + if (panel == null) + panel = group.BeatmapPanels.First(); + Trace.Assert(group.BeatmapPanels.Contains(panel), @"Selected panel must be in provided group"); - if (SelectedGroup != null && SelectedGroup != group && SelectedGroup.State != BeatmapGroupState.Hidden) - SelectedGroup.State = BeatmapGroupState.Collapsed; + if (selectedGroup != null && selectedGroup != group && selectedGroup.State != BeatmapGroupState.Hidden) + selectedGroup.State = BeatmapGroupState.Collapsed; group.State = BeatmapGroupState.Expanded; - SelectedGroup = group; + selectedGroup = group; panel.State = PanelSelectedState.Selected; - SelectedPanel = panel; + selectedPanel = panel; float selectedY = computeYPositions(animated); ScrollTo(selectedY, animated); } - public void Sort(SortMode mode) + protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) { - List sortedGroups = new List(groups); - switch (mode) + int direction = 0; + bool skipDifficulties = false; + + switch (args.Key) { - case SortMode.Artist: - sortedGroups.Sort((x, y) => string.Compare(x.BeatmapSet.Metadata.Artist, y.BeatmapSet.Metadata.Artist, StringComparison.InvariantCultureIgnoreCase)); + case Key.Up: + direction = -1; break; - case SortMode.Title: - sortedGroups.Sort((x, y) => string.Compare(x.BeatmapSet.Metadata.Title, y.BeatmapSet.Metadata.Title, StringComparison.InvariantCultureIgnoreCase)); + case Key.Down: + direction = 1; break; - case SortMode.Author: - sortedGroups.Sort((x, y) => string.Compare(x.BeatmapSet.Metadata.Author, y.BeatmapSet.Metadata.Author, StringComparison.InvariantCultureIgnoreCase)); + case Key.Left: + direction = -1; + skipDifficulties = true; break; - case SortMode.Difficulty: - sortedGroups.Sort((x, y) => x.BeatmapSet.MaxStarDifficulty.CompareTo(y.BeatmapSet.MaxStarDifficulty)); - break; - default: - Sort(SortMode.Artist); // Temporary + case Key.Right: + direction = 1; + skipDifficulties = true; break; } - scrollableContent.Clear(false); - panels.Clear(); - groups.Clear(); + if (direction == 0) + return base.OnKeyDown(state, args); - foreach (BeatmapGroup group in sortedGroups) - AddGroup(group); - } - - /// - /// Computes the x-offset of currently visible panels. Makes the carousel appear round. - /// - /// - /// Vertical distance from the center of the carousel container - /// ranging from -1 to 1. - /// - /// Half the height of the carousel container. - private static float offsetX(float dist, float halfHeight) - { - // 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; - - return 125 + x; - } - - /// - /// Update a panel's x position and multiplicative alpha based on its y position and - /// the current scroll position. - /// - /// The panel to be updated. - /// Half the draw height of the carousel container. - private void updatePanel(Panel p, float halfHeight) - { - var height = p.IsPresent ? p.DrawHeight : 0; - - float panelDrawY = p.Position.Y - Current + height / 2; - float dist = Math.Abs(1f - panelDrawY / halfHeight); - - // 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); - - // 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)); + SelectNext(direction, skipDifficulties); + return true; } protected override void Update() @@ -276,80 +407,46 @@ namespace osu.Game.Screens.Select updatePanel(p, halfHeight); } - protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) + /// + /// Computes the x-offset of currently visible panels. Makes the carousel appear round. + /// + /// + /// Vertical distance from the center of the carousel container + /// ranging from -1 to 1. + /// + /// Half the height of the carousel container. + private static float offsetX(float dist, float halfHeight) { - int direction = 0; - bool skipDifficulties = false; + // 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; - switch (args.Key) - { - case Key.Up: - direction = -1; - break; - case Key.Down: - direction = 1; - break; - case Key.Left: - direction = -1; - skipDifficulties = true; - break; - case Key.Right: - direction = 1; - skipDifficulties = true; - break; - } - - if (direction == 0) - return base.OnKeyDown(state, args); - - SelectNext(direction, skipDifficulties); - return true; + return 125 + x; } - public void SelectNext(int direction = 1, bool skipDifficulties = true) + /// + /// Update a panel's x position and multiplicative alpha based on its y position and + /// the current scroll position. + /// + /// The panel to be updated. + /// Half the draw height of the carousel container. + private void updatePanel(Panel p, float halfHeight) { - if (!skipDifficulties && SelectedGroup != null) - { - int i = SelectedGroup.BeatmapPanels.IndexOf(SelectedPanel) + direction; + var height = p.IsPresent ? p.DrawHeight : 0; - if (i >= 0 && i < SelectedGroup.BeatmapPanels.Count) - { - //changing difficulty panel, not set. - selectGroup(SelectedGroup, SelectedGroup.BeatmapPanels[i]); - return; - } - } + float panelDrawY = p.Position.Y - Current + height / 2; + float dist = Math.Abs(1f - panelDrawY / halfHeight); - int startIndex = groups.IndexOf(SelectedGroup); - int index = startIndex; + // 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); - do - { - index = (index + direction + groups.Count) % groups.Count; - if (groups[index].State != BeatmapGroupState.Hidden) - { - SelectBeatmap(groups[index].BeatmapPanels.First().Beatmap); - return; - } - } while (index != startIndex); + // 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)); } - - public void SelectRandom() - { - List visibleGroups = groups.Where(selectGroup => selectGroup.State != BeatmapGroupState.Hidden).ToList(); - if (visibleGroups.Count < 1) - return; - BeatmapGroup group = visibleGroups[RNG.Next(visibleGroups.Count)]; - BeatmapPanel panel = group?.BeatmapPanels.First(); - - if (panel == null) - return; - - selectGroup(group, panel); - } - - public IEnumerator GetEnumerator() => groups.GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } } diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 45485ecebc..fec1dc89c9 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -2,10 +2,8 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; -using System.Collections.Generic; using System.Linq; using System.Threading; -using System.Threading.Tasks; using OpenTK; using OpenTK.Input; using osu.Framework.Allocation; @@ -51,8 +49,6 @@ namespace osu.Game.Screens.Select private SampleChannel sampleChangeDifficulty; private SampleChannel sampleChangeBeatmap; - private List beatmapGroups; - protected virtual bool ShowFooter => true; /// @@ -72,7 +68,6 @@ namespace osu.Game.Screens.Select const float carousel_width = 640; const float filter_height = 100; - beatmapGroups = new List(); Add(new ParallaxContainer { Padding = new MarginPadding { Top = filter_height }, @@ -93,6 +88,8 @@ namespace osu.Game.Screens.Select Size = new Vector2(carousel_width, 1), Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, + SelectionChanged = selectionChanged, + StartRequested = raiseSelect }); Add(FilterControl = new FilterControl { @@ -132,8 +129,7 @@ namespace osu.Game.Screens.Select } [BackgroundDependencyLoader(permitNulls: true)] - private void load(BeatmapDatabase beatmaps, AudioManager audio, DialogOverlay dialog, Framework.Game game, - OsuGame osu, OsuColour colours) + private void load(BeatmapDatabase beatmaps, AudioManager audio, DialogOverlay dialog, OsuGame osu, OsuColour colours) { if (Footer != null) { @@ -161,7 +157,16 @@ namespace osu.Game.Screens.Select initialAddSetsTask = new CancellationTokenSource(); - Task.Factory.StartNew(() => addBeatmapSets(game, initialAddSetsTask.Token), initialAddSetsTask.Token); + carousel.BeatmapsChanged = beatmapsLoaded; + carousel.Beatmaps = database.Query().Where(b => !b.DeletePending); + } + + private void beatmapsLoaded() + { + if (Beatmap != null) + carousel.SelectBeatmap(Beatmap.BeatmapInfo, false); + else + carousel.SelectNext(); } private void raiseSelect() @@ -173,18 +178,24 @@ namespace osu.Game.Screens.Select } public void SelectRandom() => carousel.SelectRandom(); + protected abstract void OnSelected(); private ScheduledDelegate filterTask; private void filterChanged(bool debounce = true, bool eagerSelection = true) { + if (!carousel.IsLoaded) return; + + if (Beatmap == null) return; + filterTask?.Cancel(); filterTask = Scheduler.AddDelayed(() => { filterTask = null; var search = FilterControl.Search; BeatmapGroup newSelection = null; + carousel.Sort(FilterControl.Sort); foreach (var beatmapGroup in carousel) { @@ -227,7 +238,7 @@ namespace osu.Game.Screens.Select }, debounce ? 250 : 0); } - private void onBeatmapSetAdded(BeatmapSetInfo s) => Schedule(() => addBeatmapSet(s, Game, true)); + private void onBeatmapSetAdded(BeatmapSetInfo s) => carousel.AddBeatmap(s); private void onBeatmapSetRemoved(BeatmapSetInfo s) => Schedule(() => removeBeatmapSet(s)); @@ -352,80 +363,15 @@ namespace osu.Game.Screens.Select } } - private BeatmapGroup prepareBeatmapSet(BeatmapSetInfo beatmapSet) - { - database.GetChildren(beatmapSet); - beatmapSet.Beatmaps.ForEach(b => { if (b.Metadata == null) b.Metadata = beatmapSet.Metadata; }); - - return new BeatmapGroup(beatmapSet, database) - { - SelectionChanged = selectionChanged, - StartRequested = b => raiseSelect() - }; - } - - private void addBeatmapSet(BeatmapSetInfo beatmapSet, Framework.Game game, bool select = false) - { - var group = prepareBeatmapSet(beatmapSet); - - //for the time being, let's completely load the difficulty panels in the background. - //this likely won't scale so well, but allows us to completely async the loading flow. - Task.WhenAll(group.BeatmapPanels.Select(panel => panel.LoadAsync(game))).ContinueWith(task => Schedule(delegate - { - addGroup(group); - - if (Beatmap == null || select) - selectBeatmap(beatmapSet); - else - selectBeatmap(); - })); - } - - private void addGroup(BeatmapGroup group) - { - beatmapGroups.Add(group); - - group.State = BeatmapGroupState.Collapsed; - carousel.AddGroup(group); - - filterChanged(false, false); - } - private void selectBeatmap(BeatmapSetInfo beatmapSet = null) { - carousel.SelectBeatmap(beatmapSet != null ? beatmapSet.Beatmaps.First() : Beatmap.BeatmapInfo); - } - - private void addBeatmapSets(Framework.Game game, CancellationToken token) - { - List groups = new List(); - - foreach (var beatmapSet in database.Query().Where(b => !b.DeletePending)) - { - if (token.IsCancellationRequested) return; - - groups.Add(prepareBeatmapSet(beatmapSet)); - } - - Schedule(() => - { - groups.ForEach(addGroup); - selectBeatmap(Beatmap?.BeatmapSetInfo ?? groups.First().BeatmapSet); - }); + carousel.SelectBeatmap(beatmapSet != null ? beatmapSet.Beatmaps.First() : Beatmap?.BeatmapInfo); } private void removeBeatmapSet(BeatmapSetInfo beatmapSet) { - var group = beatmapGroups.Find(b => b.BeatmapSet.ID == beatmapSet.ID); - if (group == null) return; - - if (carousel.SelectedGroup == group) - carousel.SelectNext(); - - beatmapGroups.Remove(group); - carousel.RemoveGroup(group); - - if (beatmapGroups.Count == 0) + carousel.RemoveBeatmap(beatmapSet); + if (carousel.SelectedBeatmap == null) Beatmap = null; }