diff --git a/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs b/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs index 7c070fd3df..16c757702c 100644 --- a/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs @@ -26,10 +26,16 @@ namespace osu.Game.Tests.Visual private DependencyContainer dependencies; + public override IReadOnlyList RequiredTypes => new[] + { + typeof(BeatmapCarousel), + typeof(SongSelect), + }; + protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent) => dependencies = new DependencyContainer(parent); [BackgroundDependencyLoader] - private void load() + private void load(BeatmapManager baseMaanger) { PlaySongSelect songSelect; @@ -43,7 +49,10 @@ namespace osu.Game.Tests.Visual Func contextFactory = () => context; dependencies.Cache(rulesets = new RulesetStore(contextFactory)); - dependencies.Cache(manager = new BeatmapManager(storage, contextFactory, rulesets, null)); + dependencies.Cache(manager = new BeatmapManager(storage, contextFactory, rulesets, null) + { + DefaultBeatmap = baseMaanger.GetWorkingBeatmap(null) + }); for (int i = 0; i < 100; i += 10) manager.Import(createTestBeatmapSet(i)); diff --git a/osu.Game/Beatmaps/Drawables/BeatmapGroup.cs b/osu.Game/Beatmaps/Drawables/BeatmapGroup.cs deleted file mode 100644 index 9eb84421d4..0000000000 --- a/osu.Game/Beatmaps/Drawables/BeatmapGroup.cs +++ /dev/null @@ -1,147 +0,0 @@ -// Copyright (c) 2007-2017 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using System; -using System.Collections.Generic; -using System.Linq; -using osu.Framework; -using osu.Framework.Graphics; - -namespace osu.Game.Beatmaps.Drawables -{ - public class BeatmapGroup : IStateful - { - public event Action StateChanged; - - public BeatmapPanel SelectedPanel; - - /// - /// Fires when one of our difficulties was selected. Will fire on first expand. - /// - public Action SelectionChanged; - - /// - /// Fires when one of our difficulties is clicked when already selected. Should start playing the map. - /// - public Action StartRequested; - - public Action DeleteRequested; - - public Action RestoreHiddenRequested; - - public Action HideDifficultyRequested; - - public Action EditRequested; - - public BeatmapSetHeader Header; - - public List BeatmapPanels; - - public BeatmapSetInfo BeatmapSet; - - private BeatmapGroupState state; - - public BeatmapGroupState State - { - get { return state; } - set - { - state = value; - UpdateState(); - StateChanged?.Invoke(state); - } - } - - public void UpdateState() - { - switch (state) - { - case BeatmapGroupState.Expanded: - Header.State = PanelSelectedState.Selected; - foreach (BeatmapPanel panel in BeatmapPanels) - if (panel == SelectedPanel) - panel.State = PanelSelectedState.Selected; - else if (panel.Filtered) - panel.State = PanelSelectedState.Hidden; - else - panel.State = PanelSelectedState.NotSelected; - break; - case BeatmapGroupState.Collapsed: - Header.State = PanelSelectedState.NotSelected; - foreach (BeatmapPanel panel in BeatmapPanels) - panel.State = PanelSelectedState.Hidden; - break; - case BeatmapGroupState.Hidden: - Header.State = PanelSelectedState.Hidden; - foreach (BeatmapPanel panel in BeatmapPanels) - panel.State = PanelSelectedState.Hidden; - break; - } - } - - public BeatmapGroup(BeatmapSetInfo beatmapSet, BeatmapManager manager) - { - if (beatmapSet == null) - throw new ArgumentNullException(nameof(beatmapSet)); - if (manager == null) - throw new ArgumentNullException(nameof(manager)); - - BeatmapSet = beatmapSet; - WorkingBeatmap beatmap = manager.GetWorkingBeatmap(BeatmapSet.Beatmaps.FirstOrDefault()); - - Header = new BeatmapSetHeader(beatmap) - { - GainedSelection = headerGainedSelection, - DeleteRequested = b => DeleteRequested(b), - RestoreHiddenRequested = b => RestoreHiddenRequested(b), - RelativeSizeAxes = Axes.X, - }; - - BeatmapPanels = BeatmapSet.Beatmaps.Where(b => !b.Hidden).OrderBy(b => b.RulesetID).ThenBy(b => b.StarDifficulty).Select(b => new BeatmapPanel(b) - { - Alpha = 0, - GainedSelection = panelGainedSelection, - HideRequested = p => HideDifficultyRequested?.Invoke(p), - StartRequested = p => StartRequested?.Invoke(p.Beatmap), - EditRequested = p => EditRequested?.Invoke(p.Beatmap), - RelativeSizeAxes = Axes.X, - }).ToList(); - - Header.AddDifficultyIcons(BeatmapPanels); - } - - - private void headerGainedSelection(BeatmapSetHeader panel) - { - State = BeatmapGroupState.Expanded; - - //we want to make sure one of our children is selected in the case none have been selected yet. - if (SelectedPanel == null) - BeatmapPanels.First(p => !p.Filtered).State = PanelSelectedState.Selected; - } - - private void panelGainedSelection(BeatmapPanel panel) - { - try - { - if (SelectedPanel == panel) return; - - if (SelectedPanel != null) - SelectedPanel.State = PanelSelectedState.NotSelected; - SelectedPanel = panel; - } - finally - { - State = BeatmapGroupState.Expanded; - SelectionChanged?.Invoke(this, SelectedPanel); - } - } - } - - public enum BeatmapGroupState - { - Collapsed, - Expanded, - Hidden, - } -} diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index f6b832d0f7..cede452402 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -7,7 +7,6 @@ using osu.Framework.Graphics.Containers; using System; using System.Collections.Generic; using System.Linq; -using osu.Game.Beatmaps.Drawables; using osu.Game.Configuration; using osu.Framework.Input; using OpenTK.Input; @@ -15,17 +14,19 @@ using osu.Framework.MathUtils; using System.Diagnostics; using System.Threading.Tasks; using osu.Framework.Allocation; +using osu.Framework.Caching; using osu.Framework.Threading; using osu.Framework.Configuration; using osu.Game.Beatmaps; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Cursor; +using osu.Game.Screens.Select.Carousel; namespace osu.Game.Screens.Select { public class BeatmapCarousel : OsuScrollContainer { - public BeatmapInfo SelectedBeatmap => selectedPanel?.Beatmap; + public BeatmapInfo SelectedBeatmap => selectedBeatmap?.Beatmap; public override bool HandleInput => AllowSelection; @@ -33,27 +34,30 @@ namespace osu.Game.Screens.Select public IEnumerable Beatmaps { - get { return groups.Select(g => g.BeatmapSet); } + get { return carouselSets.Select(g => g.BeatmapSet); } set { scrollableContent.Clear(false); - panels.Clear(); - groups.Clear(); + items.Clear(); + carouselSets.Clear(); - List newGroups = null; + List newSets = null; Task.Run(() => { - newGroups = value.Select(createGroup).Where(g => g != null).ToList(); - criteria.Filter(newGroups); + newSets = value.Select(createGroup).Where(g => g != null).ToList(); + newSets.ForEach(g => g.Filter(criteria)); }).ContinueWith(t => { Schedule(() => { - foreach (var g in newGroups) + foreach (var g in newSets) addGroup(g); - computeYPositions(); + root = new CarouselGroup(newSets.OfType().ToList()); + items = root.Drawables.Value.ToList(); + + yPositionsCache.Invalidate(); BeatmapsChanged?.Invoke(); }); }); @@ -62,24 +66,19 @@ namespace osu.Game.Screens.Select private readonly List yPositions = new List(); - /// - /// Required for now unfortunately. - /// - private BeatmapManager manager; + private readonly Container scrollableContent; - private readonly Container scrollableContent; - - private readonly List groups = new List(); + private readonly List carouselSets = new List(); private Bindable randomType; - private readonly List seenGroups = new List(); + private readonly List seenSets = new List(); - private readonly List panels = new List(); + private List items = new List(); + private CarouselGroup root = new CarouselGroup(); - private readonly Stack> randomSelectedBeatmaps = new Stack>(); + private readonly Stack randomSelectedBeatmaps = new Stack(); - private BeatmapGroup selectedGroup; - private BeatmapPanel selectedPanel; + private CarouselBeatmap selectedBeatmap; public BeatmapCarousel() { @@ -87,7 +86,7 @@ namespace osu.Game.Screens.Select { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Child = scrollableContent = new Container + Child = scrollableContent = new Container { RelativeSizeAxes = Axes.X, } @@ -96,45 +95,44 @@ namespace osu.Game.Screens.Select public void RemoveBeatmap(BeatmapSetInfo beatmapSet) { - Schedule(() => removeGroup(groups.Find(b => b.BeatmapSet.ID == beatmapSet.ID))); + Schedule(() => removeGroup(carouselSets.Find(b => b.BeatmapSet.ID == beatmapSet.ID))); } public void UpdateBeatmapSet(BeatmapSetInfo beatmapSet) { - // todo: this method should be smarter as to not recreate panels that haven't changed, etc. - var oldGroup = groups.Find(b => b.BeatmapSet.ID == beatmapSet.ID); + // todo: this method should be smarter as to not recreate items that haven't changed, etc. + var oldGroup = carouselSets.Find(b => b.BeatmapSet.ID == beatmapSet.ID); - var newGroup = createGroup(beatmapSet); + bool hadSelection = oldGroup?.State == CarouselItemState.Selected; - int index = groups.IndexOf(oldGroup); + var newSet = createGroup(beatmapSet); + + int index = carouselSets.IndexOf(oldGroup); if (index >= 0) - groups.RemoveAt(index); + carouselSets.RemoveAt(index); - if (newGroup != null) + if (newSet != null) { if (index >= 0) - groups.Insert(index, newGroup); + carouselSets.Insert(index, newSet); else - addGroup(newGroup); + addGroup(newSet); } - bool hadSelection = selectedGroup == oldGroup; - - if (hadSelection && newGroup == null) - selectedGroup = null; + if (hadSelection && newSet == null) + SelectNext(); Filter(null, false); //check if we can/need to maintain our current selection. - if (hadSelection && newGroup != null) + if (hadSelection && newSet != null) { - var newSelection = - newGroup.BeatmapPanels.Find(p => p.Beatmap.ID == selectedPanel?.Beatmap.ID); + var newSelection = newSet.Beatmaps.Find(b => b.Beatmap.ID == selectedBeatmap?.Beatmap.ID); - if (newSelection == null && oldGroup != null && selectedPanel != null) - newSelection = newGroup.BeatmapPanels[Math.Min(newGroup.BeatmapPanels.Count - 1, oldGroup.BeatmapPanels.IndexOf(selectedPanel))]; + if (newSelection == null && selectedBeatmap != null) + newSelection = newSet.Beatmaps[Math.Min(newSet.Beatmaps.Count - 1, oldGroup.Beatmaps.IndexOf(selectedBeatmap))]; - selectGroup(newGroup, newSelection); + select(newSelection); } } @@ -148,12 +146,12 @@ namespace osu.Game.Screens.Select if (beatmap == SelectedBeatmap) return; - foreach (BeatmapGroup group in groups) + foreach (CarouselBeatmapSet group in carouselSets) { - var panel = group.BeatmapPanels.FirstOrDefault(p => p.Beatmap.Equals(beatmap)); - if (panel != null) + var item = group.Beatmaps.FirstOrDefault(p => p.Beatmap.Equals(beatmap)); + if (item != null) { - selectGroup(group, panel, animated); + select(item, animated); return; } } @@ -161,20 +159,9 @@ namespace osu.Game.Screens.Select public Action SelectionChanged; - public Action StartRequested; - - public Action DeleteRequested; - - public Action RestoreRequested; - - public Action EditRequested; - - public Action HideDifficultyRequested; - private void selectNullBeatmap() { - selectedGroup = null; - selectedPanel = null; + selectedBeatmap = null; SelectionChanged?.Invoke(null); } @@ -186,90 +173,71 @@ namespace osu.Game.Screens.Select public void SelectNext(int direction = 1, bool skipDifficulties = true) { // todo: we may want to refactor and remove this as an optimisation in the future. - if (groups.All(g => g.State == BeatmapGroupState.Hidden)) + if (carouselSets.All(g => g.State == CarouselItemState.Hidden)) { selectNullBeatmap(); return; } - int originalIndex = Math.Max(0, groups.IndexOf(selectedGroup)); + int originalIndex = Math.Max(0, items.IndexOf(selectedBeatmap?.Drawables.Value.First())); int currentIndex = originalIndex; // local function to increment the index in the required direction, wrapping over extremities. - int incrementIndex() => currentIndex = (currentIndex + direction + groups.Count) % groups.Count; + int incrementIndex() => currentIndex = (currentIndex + direction + items.Count) % items.Count; - // in the case we are skipping difficulties, we want to increment the index once before starting to find out new target - // (we don't care about the currently selected group). - if (skipDifficulties) - incrementIndex(); - - do + while (incrementIndex() != originalIndex) { - var group = groups[currentIndex]; + var item = items[currentIndex].Item; - if (group.State == BeatmapGroupState.Hidden) continue; + if (item.Filtered || item.State == CarouselItemState.Selected) continue; - // we are only interested in non-filtered panels. - IEnumerable validPanels = group.BeatmapPanels.Where(p => !p.Filtered); - - // if we are considering difficulties, we need to do a few extrea steps. - if (!skipDifficulties) + switch (item) { - // we want to reverse the panel order if we are searching backwards. - if (direction < 0) - validPanels = validPanels.Reverse(); - - // if we are currently on the selected panel, let's try to find a valid difficulty before leaving to the next group. - // the first valid difficulty is found by skipping to the selected panel and then one further. - if (currentIndex == originalIndex) - validPanels = validPanels.SkipWhile(p => p != selectedPanel).Skip(1); + case CarouselBeatmap beatmap: + if (skipDifficulties) continue; + select(beatmap); + return; + case CarouselBeatmapSet set: + select(set); + return; } - - var next = validPanels.FirstOrDefault(); - - // at this point, we can perform the selection change if we have a valid new target, else continue to increment in the specified direction. - if (next != null) - { - selectGroup(group, next); - return; - } - } while (incrementIndex() != originalIndex); + } } - private IEnumerable getVisibleGroups() => groups.Where(selectGroup => selectGroup.State != BeatmapGroupState.Hidden); + private IEnumerable getVisibleGroups() => carouselSets.Where(select => select.State != CarouselItemState.NotSelected); public void SelectNextRandom() { - if (groups.Count == 0) + if (carouselSets.Count == 0) return; var visibleGroups = getVisibleGroups(); if (!visibleGroups.Any()) return; - if (selectedGroup != null) - randomSelectedBeatmaps.Push(new KeyValuePair(selectedGroup, selectedGroup.SelectedPanel)); + if (selectedBeatmap != null) + randomSelectedBeatmaps.Push(selectedBeatmap); - BeatmapGroup group; + CarouselBeatmapSet group; if (randomType == SelectionRandomType.RandomPermutation) { - var notSeenGroups = visibleGroups.Except(seenGroups); + var notSeenGroups = visibleGroups.Except(seenSets); if (!notSeenGroups.Any()) { - seenGroups.Clear(); + seenSets.Clear(); notSeenGroups = visibleGroups; } group = notSeenGroups.ElementAt(RNG.Next(notSeenGroups.Count())); - seenGroups.Add(group); + seenSets.Add(group); } else group = visibleGroups.ElementAt(RNG.Next(visibleGroups.Count())); - BeatmapPanel panel = group.BeatmapPanels[RNG.Next(group.BeatmapPanels.Count)]; + CarouselBeatmap item = group.Beatmaps[RNG.Next(group.Beatmaps.Count)]; - selectGroup(group, panel); + select(item); } public void SelectPreviousRandom() @@ -277,17 +245,13 @@ namespace osu.Game.Screens.Select if (!randomSelectedBeatmaps.Any()) return; - var visibleGroups = getVisibleGroups(); - if (!visibleGroups.Any()) - return; - while (randomSelectedBeatmaps.Any()) { - var beatmapCoordinates = randomSelectedBeatmaps.Pop(); - var group = beatmapCoordinates.Key; - if (visibleGroups.Contains(group)) + var beatmap = randomSelectedBeatmaps.Pop(); + + if (beatmap.Visible) { - selectGroup(group, beatmapCoordinates.Value); + select(beatmap); break; } } @@ -314,25 +278,14 @@ namespace osu.Game.Screens.Select { filterTask = null; - criteria.Filter(groups); + carouselSets.ForEach(s => s.Filter(criteria)); - var filtered = new List(groups); + yPositionsCache.Invalidate(); - scrollableContent.Clear(false); - panels.Clear(); - groups.Clear(); - - foreach (var g in filtered) - addGroup(g); - - computeYPositions(); - - selectedGroup?.UpdateState(); - - if (selectedGroup == null || selectedGroup.State == BeatmapGroupState.Hidden) + if (selectedBeatmap?.Visible != true) SelectNext(); else - selectGroup(selectedGroup, selectedPanel); + select(selectedBeatmap); }; filterTask?.Cancel(); @@ -350,54 +303,60 @@ namespace osu.Game.Screens.Select ScrollTo(selectedY, animated); } - private BeatmapGroup createGroup(BeatmapSetInfo beatmapSet) + private CarouselBeatmapSet createGroup(BeatmapSetInfo beatmapSet) { if (beatmapSet.Beatmaps.All(b => b.Hidden)) return null; + // todo: remove the need for this. foreach (var b in beatmapSet.Beatmaps) { if (b.Metadata == null) b.Metadata = beatmapSet.Metadata; } - return new BeatmapGroup(beatmapSet, manager) + var set = new CarouselBeatmapSet(beatmapSet); + + foreach (var c in set.Beatmaps) { - SelectionChanged = (g, p) => selectGroup(g, p), - StartRequested = b => StartRequested?.Invoke(), - DeleteRequested = b => DeleteRequested?.Invoke(b), - RestoreHiddenRequested = s => RestoreRequested?.Invoke(s), - EditRequested = b => EditRequested?.Invoke(b), - HideDifficultyRequested = b => HideDifficultyRequested?.Invoke(b), - State = BeatmapGroupState.Collapsed - }; + c.State.ValueChanged += v => + { + if (v == CarouselItemState.Selected) + { + selectedBeatmap = c; + SelectionChanged?.Invoke(c.Beatmap); + yPositionsCache.Invalidate(); + Schedule(() => ScrollToSelected()); + } + }; + } + + return set; } [BackgroundDependencyLoader(permitNulls: true)] - private void load(BeatmapManager manager, OsuConfigManager config) + private void load(OsuConfigManager config) { - this.manager = manager; - randomType = config.GetBindable(OsuSetting.SelectionRandomType); } - private void addGroup(BeatmapGroup group) + private void addGroup(CarouselBeatmapSet set) { // prevent duplicates by concurrent independent actions trying to add a group - if (groups.Any(g => g.BeatmapSet.ID == group.BeatmapSet.ID)) + //todo: check this + if (carouselSets.Any(g => g.BeatmapSet.ID == set.BeatmapSet.ID)) return; - groups.Add(group); - panels.Add(group.Header); - panels.AddRange(group.BeatmapPanels); + //todo: add to root + carouselSets.Add(set); } - private void removeGroup(BeatmapGroup group) + private void removeGroup(CarouselBeatmapSet set) { - if (group == null) + if (set == null) return; - if (selectedGroup == group) + if (set.State == CarouselItemState.Selected) { if (getVisibleGroups().Count() == 1) selectNullBeatmap(); @@ -405,21 +364,23 @@ namespace osu.Game.Screens.Select SelectNext(); } - groups.Remove(group); - panels.Remove(group.Header); - foreach (var p in group.BeatmapPanels) - panels.Remove(p); + carouselSets.Remove(set); - scrollableContent.Remove(group.Header); - scrollableContent.RemoveRange(group.BeatmapPanels); + foreach (var d in set.Drawables.Value) + { + items.Remove(d); + scrollableContent.Remove(d); + } - computeYPositions(); + yPositionsCache.Invalidate(); } + private Cached yPositionsCache = new Cached(); + /// - /// Computes the target Y positions for every panel in the carousel. + /// Computes the target Y positions for every item in the carousel. /// - /// The Y position of the currently selected panel. + /// The Y position of the currently selected item. private float computeYPositions(bool animated = true) { yPositions.Clear(); @@ -427,88 +388,61 @@ namespace osu.Game.Screens.Select float currentY = DrawHeight / 2; float selectedY = currentY; - foreach (BeatmapGroup group in groups) + float lastSetY = 0; + + foreach (DrawableCarouselItem d in items) { - movePanel(group.Header, group.State != BeatmapGroupState.Hidden, animated, ref currentY); - - if (group.State == BeatmapGroupState.Expanded) + switch (d) { - group.Header.MoveToX(-100, 500, Easing.OutExpo); - var headerY = group.Header.Position.Y; + case DrawableCarouselBeatmapSet set: + set.MoveToX(set.Item.State == CarouselItemState.Selected ? -100 : 0, 500, Easing.OutExpo); - foreach (BeatmapPanel panel in group.BeatmapPanels) - { - if (panel == selectedPanel) - selectedY = currentY + panel.DrawHeight / 2 - DrawHeight / 2; + lastSetY = set.Position.Y; - panel.MoveToX(-50, 500, Easing.OutExpo); + movePanel(set, set.Item.Visible, animated, ref currentY); + break; + case DrawableCarouselBeatmap beatmap: + beatmap.MoveToX(beatmap.Item.State == CarouselItemState.Selected ? -50 : 0, 500, Easing.OutExpo); - bool isHidden = panel.State == PanelSelectedState.Hidden; + if (beatmap.Item == selectedBeatmap) + selectedY = currentY + beatmap.DrawHeight / 2 - DrawHeight / 2; - //on first display we want to begin hidden under our group's header. - if (isHidden || panel.Alpha == 0) - panel.MoveToY(headerY); + // on first display we want to begin hidden under our group's header. + if (animated && !beatmap.IsPresent) + beatmap.MoveToY(lastSetY); - movePanel(panel, !isHidden, animated, ref currentY); - } - } - else - { - group.Header.MoveToX(0, 500, Easing.OutExpo); - - foreach (BeatmapPanel panel in group.BeatmapPanels) - { - panel.MoveToX(0, 500, Easing.OutExpo); - movePanel(panel, false, animated, ref currentY); - } + movePanel(beatmap, beatmap.Item.Visible, animated, ref currentY); + break; } } currentY += DrawHeight / 2; scrollableContent.Height = currentY; + yPositionsCache.Validate(); + return selectedY; } - private void movePanel(Panel panel, bool advance, bool animated, ref float currentY) + private void movePanel(DrawableCarouselItem item, bool advance, bool animated, ref float currentY) { yPositions.Add(currentY); - panel.MoveToY(currentY, animated ? 750 : 0, Easing.OutExpo); + item.MoveToY(currentY, animated ? 750 : 0, Easing.OutExpo); if (advance) - currentY += panel.DrawHeight + 5; + currentY += item.DrawHeight + 5; } - private void selectGroup(BeatmapGroup group, BeatmapPanel panel = null, bool animated = true) + private void select(CarouselBeatmapSet beatmapSet = null) { - try - { - if (panel == null || panel.Filtered == true) - panel = group.BeatmapPanels.First(p => !p.Filtered); + if (beatmapSet == null) return; + beatmapSet.State.Value = CarouselItemState.Selected; + } - if (selectedPanel == panel) return; - - 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; - - group.State = BeatmapGroupState.Expanded; - group.SelectedPanel = panel; - - panel.State = PanelSelectedState.Selected; - - if (selectedPanel == panel) return; - - selectedPanel = panel; - selectedGroup = group; - - SelectionChanged?.Invoke(panel.Beatmap); - } - finally - { - ScrollToSelected(animated); - } + private void select(CarouselBeatmap beatmap = null, bool animated = true) + { + if (beatmap == null) return; + beatmap.State.Value = CarouselItemState.Selected; } protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) @@ -547,66 +481,67 @@ namespace osu.Game.Screens.Select float drawHeight = DrawHeight; - // Remove all panels that should no longer be on-screen - scrollableContent.RemoveAll(delegate(Panel p) + if (!yPositionsCache.IsValid) + computeYPositions(); + + // Remove all items that should no longer be on-screen + scrollableContent.RemoveAll(delegate (DrawableCarouselItem p) { - float panelPosY = p.Position.Y; - bool remove = panelPosY < Current - p.DrawHeight || panelPosY > Current + drawHeight || !p.IsPresent; + float itemPosY = p.Position.Y; + bool remove = itemPosY < Current - p.DrawHeight || itemPosY > Current + drawHeight || !p.IsPresent; return remove; }); - // Find index range of all panels that should be on-screen - Trace.Assert(panels.Count == yPositions.Count); + // Find index range of all items that should be on-screen + Trace.Assert(items.Count == yPositions.Count); - int firstIndex = yPositions.BinarySearch(Current - Panel.MAX_HEIGHT); + int firstIndex = yPositions.BinarySearch(Current - DrawableCarouselItem.MAX_HEIGHT); if (firstIndex < 0) firstIndex = ~firstIndex; int lastIndex = yPositions.BinarySearch(Current + drawHeight); if (lastIndex < 0) { lastIndex = ~lastIndex; - // Add the first panel of the last visible beatmap group to preload its data. - if (lastIndex != 0 && panels[lastIndex - 1] is BeatmapSetHeader) + // Add the first item of the last visible beatmap group to preload its data. + if (lastIndex != 0 && items[lastIndex - 1] is DrawableCarouselBeatmapSet) lastIndex++; } - // Add those panels within the previously found index range that should be displayed. + // Add those items within the previously found index range that should be displayed. for (int i = firstIndex; i < lastIndex; ++i) { - Panel panel = panels[i]; - if (panel.State == PanelSelectedState.Hidden) - continue; + DrawableCarouselItem item = items[i]; // Only add if we're not already part of the content. - if (!scrollableContent.Contains(panel)) + if (!scrollableContent.Contains(item)) { - // Makes sure headers are always _below_ panels, + // Makes sure headers are always _below_ items, // and depth flows downward. - panel.Depth = i + (panel is BeatmapSetHeader ? panels.Count : 0); + item.Depth = i + (item is DrawableCarouselBeatmapSet ? items.Count : 0); - switch (panel.LoadState) + switch (item.LoadState) { case LoadState.NotLoaded: - LoadComponentAsync(panel); + LoadComponentAsync(item); break; case LoadState.Loading: break; default: - scrollableContent.Add(panel); + scrollableContent.Add(item); break; } } } - // Update externally controlled state of currently visible panels + // Update externally controlled state of currently visible items // (e.g. x-offset and opacity). float halfHeight = drawHeight / 2; - foreach (Panel p in scrollableContent.Children) - updatePanel(p, halfHeight); + foreach (DrawableCarouselItem p in scrollableContent.Children) + updateItem(p, halfHeight); } /// - /// Computes the x-offset of currently visible panels. Makes the carousel appear round. + /// Computes the x-offset of currently visible items. Makes the carousel appear round. /// /// /// Vertical distance from the center of the carousel container @@ -624,20 +559,20 @@ namespace osu.Game.Screens.Select } /// - /// Update a panel's x position and multiplicative alpha based on its y position and + /// Update a item's x position and multiplicative alpha based on its y position and /// the current scroll position. /// - /// The panel to be updated. + /// The item to be updated. /// Half the draw height of the carousel container. - private void updatePanel(Panel p, float halfHeight) + private void updateItem(DrawableCarouselItem 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); + float itemDrawY = p.Position.Y - Current + height / 2; + float dist = Math.Abs(1f - itemDrawY / 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 + // local transformation we may want to apply (e.g. when a item gets selected, we // may want to smoothly transform it leftwards.) p.OriginPosition = new Vector2(-offsetX(dist, halfHeight), 0); diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs new file mode 100644 index 0000000000..f1bc24db50 --- /dev/null +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs @@ -0,0 +1,42 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Linq; +using osu.Game.Beatmaps; + +namespace osu.Game.Screens.Select.Carousel +{ + public class CarouselBeatmap : CarouselItem + { + public readonly BeatmapInfo Beatmap; + + public CarouselBeatmap(BeatmapInfo beatmap) + { + Beatmap = beatmap; + State.Value = CarouselItemState.Hidden; + } + + protected override DrawableCarouselItem CreateDrawableRepresentation() => new DrawableCarouselBeatmap(this) + { + /*GainedSelection = panelGainedSelection, + HideRequested = p => HideDifficultyRequested?.Invoke(p), + StartRequested = p => StartRequested?.Invoke(p.beatmap), + EditRequested = p => EditRequested?.Invoke(p.beatmap),*/ + }; + + public override void Filter(FilterCriteria criteria) + { + base.Filter(criteria); + + bool match = criteria.Ruleset == null || (Beatmap.RulesetID == criteria.Ruleset.ID || Beatmap.RulesetID == 0 && criteria.Ruleset.ID > 0 && criteria.AllowConvertedBeatmaps); + + if (!string.IsNullOrEmpty(criteria.SearchText)) + match &= + Beatmap.Metadata.SearchableTerms.Any(term => term.IndexOf(criteria.SearchText, StringComparison.InvariantCultureIgnoreCase) >= 0) || + Beatmap.Version.IndexOf(criteria.SearchText, StringComparison.InvariantCultureIgnoreCase) >= 0; + + Filtered.Value = !match; + } + } +} diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs new file mode 100644 index 0000000000..525bb6c600 --- /dev/null +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs @@ -0,0 +1,62 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Game.Beatmaps; + +namespace osu.Game.Screens.Select.Carousel +{ + public class CarouselBeatmapSet : CarouselGroupEagerSelect + { + public readonly List Beatmaps; + + public BeatmapSetInfo BeatmapSet; + + public CarouselBeatmapSet(BeatmapSetInfo beatmapSet) + { + if (beatmapSet == null) throw new ArgumentNullException(nameof(beatmapSet)); + + BeatmapSet = beatmapSet; + + Children = Beatmaps = beatmapSet.Beatmaps + .Where(b => !b.Hidden) + .OrderBy(b => b.RulesetID).ThenBy(b => b.StarDifficulty) + .Select(b => new CarouselBeatmap(b)) + .ToList(); + } + + protected override DrawableCarouselItem CreateDrawableRepresentation() => new DrawableCarouselBeatmapSet(this); + + public override void Filter(FilterCriteria criteria) + { + base.Filter(criteria); + Filtered.Value = Children.All(i => i.Filtered); + + /*switch (criteria.Sort) + { + default: + case SortMode.Artist: + groups.Sort((x, y) => string.Compare(x.BeatmapSet.Metadata.Artist, y.BeatmapSet.Metadata.Artist, StringComparison.InvariantCultureIgnoreCase)); + break; + case SortMode.Title: + groups.Sort((x, y) => string.Compare(x.BeatmapSet.Metadata.Title, y.BeatmapSet.Metadata.Title, StringComparison.InvariantCultureIgnoreCase)); + break; + case SortMode.Author: + groups.Sort((x, y) => string.Compare(x.BeatmapSet.Metadata.Author.Username, y.BeatmapSet.Metadata.Author.Username, StringComparison.InvariantCultureIgnoreCase)); + break; + case SortMode.Difficulty: + groups.Sort((x, y) => x.BeatmapSet.MaxStarDifficulty.CompareTo(y.BeatmapSet.MaxStarDifficulty)); + break; + }*/ + } + } + + public enum CarouselItemState + { + Hidden, + NotSelected, + Selected, + } +} diff --git a/osu.Game/Screens/Select/Carousel/CarouselGroup.cs b/osu.Game/Screens/Select/Carousel/CarouselGroup.cs new file mode 100644 index 0000000000..597fed7a34 --- /dev/null +++ b/osu.Game/Screens/Select/Carousel/CarouselGroup.cs @@ -0,0 +1,54 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Collections.Generic; +using osu.Framework.Configuration; +using osu.Framework.Extensions.IEnumerableExtensions; + +namespace osu.Game.Screens.Select.Carousel +{ + /// + /// A group which ensures only one child is selected. + /// + public class CarouselGroup : CarouselItem + { + private readonly List items; + + public readonly Bindable Selected = new Bindable(); + + protected override DrawableCarouselItem CreateDrawableRepresentation() => null; + + protected override IEnumerable Children + { + get { return base.Children; } + set + { + base.Children = value; + value.ForEach(i => i.State.ValueChanged += v => itemStateChanged(i, v)); + } + } + + public CarouselGroup(List items = null) + { + if (items != null) Children = items; + } + + private void itemStateChanged(CarouselItem item, CarouselItemState value) + { + // todo: check state of selected item. + + // ensure we are the only item selected + if (value == CarouselItemState.Selected) + { + foreach (var b in Children) + { + if (item == b) continue; + b.State.Value = CarouselItemState.NotSelected; + } + + State.Value = CarouselItemState.Selected; + Selected.Value = item; + } + } + } +} diff --git a/osu.Game/Screens/Select/Carousel/CarouselGroupEagerSelect.cs b/osu.Game/Screens/Select/Carousel/CarouselGroupEagerSelect.cs new file mode 100644 index 0000000000..351f0ed55b --- /dev/null +++ b/osu.Game/Screens/Select/Carousel/CarouselGroupEagerSelect.cs @@ -0,0 +1,28 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Linq; + +namespace osu.Game.Screens.Select.Carousel +{ + /// + /// A group which ensures at least one child is selected (if the group itself is selected). + /// + public class CarouselGroupEagerSelect : CarouselGroup + { + public CarouselGroupEagerSelect() + { + State.ValueChanged += v => + { + if (v == CarouselItemState.Selected) + { + foreach (var c in Children.Where(c => c.State.Value == CarouselItemState.Hidden)) + c.State.Value = CarouselItemState.NotSelected; + + if (Children.Any(c => c.Visible) && Children.All(c => c.State != CarouselItemState.Selected)) + Children.First(c => c.Visible).State.Value = CarouselItemState.Selected; + } + }; + } + } +} diff --git a/osu.Game/Screens/Select/Carousel/CarouselItem.cs b/osu.Game/Screens/Select/Carousel/CarouselItem.cs new file mode 100644 index 0000000000..d6d0615b6e --- /dev/null +++ b/osu.Game/Screens/Select/Carousel/CarouselItem.cs @@ -0,0 +1,57 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using osu.Framework.Configuration; +using osu.Framework.Extensions.IEnumerableExtensions; + +namespace osu.Game.Screens.Select.Carousel +{ + public abstract class CarouselItem + { + public readonly BindableBool Filtered = new BindableBool(); + + public readonly Bindable State = new Bindable(CarouselItemState.NotSelected); + + protected virtual IEnumerable Children { get; set; } + + public bool Visible => State.Value != CarouselItemState.Hidden && !Filtered.Value; + + public readonly Lazy> Drawables; + + protected CarouselItem() + { + Drawables = new Lazy>(() => + { + List items = new List(); + + var self = CreateDrawableRepresentation(); + if (self != null) items.Add(self); + + if (Children != null) + foreach (var c in Children) + items.AddRange(c.Drawables.Value); + + return items; + }); + + State.ValueChanged += v => + { + if (Children == null) return; + + switch (v) + { + case CarouselItemState.Hidden: + case CarouselItemState.NotSelected: + Children.ForEach(c => c.State.Value = CarouselItemState.Hidden); + break; + } + }; + } + + protected abstract DrawableCarouselItem CreateDrawableRepresentation(); + + public virtual void Filter(FilterCriteria criteria) => Children?.ForEach(c => c.Filter(criteria)); + } +} diff --git a/osu.Game/Beatmaps/Drawables/BeatmapPanel.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs similarity index 81% rename from osu.Game/Beatmaps/Drawables/BeatmapPanel.cs rename to osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index d8ba1e9195..5170e0b98b 100644 --- a/osu.Game/Beatmaps/Drawables/BeatmapPanel.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -2,84 +2,50 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; -using osu.Framework.Configuration; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; +using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using OpenTK; using OpenTK.Graphics; -using osu.Framework.Input; -using osu.Game.Graphics.Sprites; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.UserInterface; -namespace osu.Game.Beatmaps.Drawables +namespace osu.Game.Screens.Select.Carousel { - public class BeatmapPanel : Panel, IHasContextMenu + public class DrawableCarouselBeatmap : DrawableCarouselItem, IHasContextMenu { - public BeatmapInfo Beatmap; + private readonly BeatmapInfo beatmap; + private readonly Sprite background; - public Action GainedSelection; - public Action StartRequested; - public Action EditRequested; + public Action StartRequested; + public Action EditRequested; public Action HideRequested; private readonly Triangles triangles; private readonly StarCounter starCounter; - protected override void Selected() + [BackgroundDependencyLoader] + private void load(SongSelect songSelect) { - base.Selected(); - - GainedSelection?.Invoke(this); - - background.Colour = ColourInfo.GradientVertical( - new Color4(20, 43, 51, 255), - new Color4(40, 86, 102, 255)); - - triangles.Colour = Color4.White; + StartRequested = songSelect.Start; + EditRequested = songSelect.Edit; } - protected override void Deselected() + public DrawableCarouselBeatmap(CarouselBeatmap panel) + : base(panel) { - base.Deselected(); - - background.Colour = new Color4(20, 43, 51, 255); - triangles.Colour = OsuColour.Gray(0.5f); - } - - protected override bool OnClick(InputState state) - { - if (State == PanelSelectedState.Selected) - StartRequested?.Invoke(this); - - return base.OnClick(state); - } - - public BindableBool Filtered = new BindableBool(); - - protected override void ApplyState(PanelSelectedState last = PanelSelectedState.Hidden) - { - if (!IsLoaded) return; - - base.ApplyState(last); - - if (last == PanelSelectedState.Hidden && State != last) - starCounter.ReplayAnimation(); - } - - public BeatmapPanel(BeatmapInfo beatmap) - { - if (beatmap == null) - throw new ArgumentNullException(nameof(beatmap)); - - Beatmap = beatmap; + beatmap = panel.Beatmap; Height *= 0.60f; Children = new Drawable[] @@ -160,11 +126,46 @@ namespace osu.Game.Beatmaps.Drawables }; } + protected override void Selected() + { + base.Selected(); + + background.Colour = ColourInfo.GradientVertical( + new Color4(20, 43, 51, 255), + new Color4(40, 86, 102, 255)); + + triangles.Colour = Color4.White; + } + + protected override void Deselected() + { + base.Deselected(); + + background.Colour = new Color4(20, 43, 51, 255); + triangles.Colour = OsuColour.Gray(0.5f); + } + + protected override bool OnClick(InputState state) + { + if (Item.State == CarouselItemState.Selected) + StartRequested?.Invoke(beatmap); + + return base.OnClick(state); + } + + protected override void ApplyState() + { + if (Item.State.Value != CarouselItemState.Hidden && Alpha == 0) + starCounter.ReplayAnimation(); + + base.ApplyState(); + } + public MenuItem[] ContextMenuItems => new MenuItem[] { - new OsuMenuItem("Play", MenuItemType.Highlighted, () => StartRequested?.Invoke(this)), - new OsuMenuItem("Edit", MenuItemType.Standard, () => EditRequested?.Invoke(this)), - new OsuMenuItem("Hide", MenuItemType.Destructive, () => HideRequested?.Invoke(Beatmap)), + new OsuMenuItem("Play", MenuItemType.Highlighted, () => StartRequested?.Invoke(beatmap)), + new OsuMenuItem("Edit", MenuItemType.Standard, () => EditRequested?.Invoke(beatmap)), + new OsuMenuItem("Hide", MenuItemType.Destructive, () => HideRequested?.Invoke(beatmap)), }; } } diff --git a/osu.Game/Beatmaps/Drawables/BeatmapSetHeader.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs similarity index 75% rename from osu.Game/Beatmaps/Drawables/BeatmapSetHeader.cs rename to osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index 9bb7b5c737..f4e43d14aa 100644 --- a/osu.Game/Beatmaps/Drawables/BeatmapSetHeader.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -4,46 +4,40 @@ using System; using System.Collections.Generic; using System.Linq; -using OpenTK; -using OpenTK.Graphics; using osu.Framework.Allocation; using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; -using osu.Framework.Localisation; -using osu.Game.Graphics.Sprites; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Localisation; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Drawables; +using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using OpenTK; +using OpenTK.Graphics; -namespace osu.Game.Beatmaps.Drawables +namespace osu.Game.Screens.Select.Carousel { - public class BeatmapSetHeader : Panel, IHasContextMenu + public class DrawableCarouselBeatmapSet : DrawableCarouselItem, IHasContextMenu { - public Action GainedSelection; + public Action GainedSelection; public Action DeleteRequested; public Action RestoreHiddenRequested; - private readonly WorkingBeatmap beatmap; + private readonly BeatmapSetInfo beatmapSet; private readonly FillFlowContainer difficultyIcons; - public BeatmapSetHeader(WorkingBeatmap beatmap) + public DrawableCarouselBeatmapSet(CarouselBeatmapSet set) + : base(set) { - if (beatmap == null) - throw new ArgumentNullException(nameof(beatmap)); - - this.beatmap = beatmap; - - difficultyIcons = new FillFlowContainer - { - Margin = new MarginPadding { Top = 5 }, - AutoSizeAxes = Axes.Both, - }; + beatmapSet = set.BeatmapSet; } protected override void Selected() @@ -53,15 +47,17 @@ namespace osu.Game.Beatmaps.Drawables } [BackgroundDependencyLoader] - private void load(LocalisationEngine localisation) + private void load(LocalisationEngine localisation, BeatmapManager manager) { if (localisation == null) throw new ArgumentNullException(nameof(localisation)); + var working = manager.GetWorkingBeatmap(beatmapSet.Beatmaps.FirstOrDefault()); + Children = new Drawable[] { new DelayedLoadWrapper( - new PanelBackground(beatmap) + new PanelBackground(working) { RelativeSizeAxes = Axes.Both, OnLoadComplete = d => d.FadeInFromZero(400, Easing.Out), @@ -76,18 +72,23 @@ namespace osu.Game.Beatmaps.Drawables new OsuSpriteText { Font = @"Exo2.0-BoldItalic", - Current = localisation.GetUnicodePreference(beatmap.Metadata.TitleUnicode, beatmap.Metadata.Title), + Current = localisation.GetUnicodePreference(beatmapSet.Metadata.TitleUnicode, beatmapSet.Metadata.Title), TextSize = 22, Shadow = true, }, new OsuSpriteText { Font = @"Exo2.0-SemiBoldItalic", - Current = localisation.GetUnicodePreference(beatmap.Metadata.ArtistUnicode, beatmap.Metadata.Artist), + Current = localisation.GetUnicodePreference(beatmapSet.Metadata.ArtistUnicode, beatmapSet.Metadata.Artist), TextSize = 17, Shadow = true, }, - difficultyIcons + new FillFlowContainer + { + Margin = new MarginPadding { Top = 5 }, + AutoSizeAxes = Axes.Both, + Children = ((CarouselBeatmapSet)Item).Beatmaps.Select(b => new FilterableDifficultyIcon(b)).ToList() + } } } }; @@ -153,27 +154,19 @@ namespace osu.Game.Beatmaps.Drawables } } - public void AddDifficultyIcons(IEnumerable panels) - { - if (panels == null) - throw new ArgumentNullException(nameof(panels)); - - difficultyIcons.AddRange(panels.Select(p => new FilterableDifficultyIcon(p))); - } - public MenuItem[] ContextMenuItems { get { List items = new List(); - if (State == PanelSelectedState.NotSelected) - items.Add(new OsuMenuItem("Expand", MenuItemType.Highlighted, () => State = PanelSelectedState.Selected)); + if (Item.State == CarouselItemState.NotSelected) + items.Add(new OsuMenuItem("Expand", MenuItemType.Highlighted, () => Item.State.Value = CarouselItemState.Selected)); - if (beatmap.BeatmapSetInfo.Beatmaps.Any(b => b.Hidden)) - items.Add(new OsuMenuItem("Restore all hidden", MenuItemType.Standard, () => RestoreHiddenRequested?.Invoke(beatmap.BeatmapSetInfo))); + if (beatmapSet.Beatmaps.Any(b => b.Hidden)) + items.Add(new OsuMenuItem("Restore all hidden", MenuItemType.Standard, () => RestoreHiddenRequested?.Invoke(beatmapSet))); - items.Add(new OsuMenuItem("Delete", MenuItemType.Destructive, () => DeleteRequested?.Invoke(beatmap.BeatmapSetInfo))); + items.Add(new OsuMenuItem("Delete", MenuItemType.Destructive, () => DeleteRequested?.Invoke(beatmapSet))); return items.ToArray(); } @@ -183,11 +176,11 @@ namespace osu.Game.Beatmaps.Drawables { private readonly BindableBool filtered = new BindableBool(); - public FilterableDifficultyIcon(BeatmapPanel panel) - : base(panel.Beatmap) + public FilterableDifficultyIcon(CarouselBeatmap item) + : base(item.Beatmap) { - filtered.BindTo(panel.Filtered); - filtered.ValueChanged += v => this.FadeTo(v ? 0.1f : 1, 100); + filtered.BindTo(item.Filtered); + filtered.ValueChanged += v => Schedule(() => this.FadeTo(v ? 0.1f : 1, 100)); filtered.TriggerChange(); } } diff --git a/osu.Game/Beatmaps/Drawables/Panel.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs similarity index 71% rename from osu.Game/Beatmaps/Drawables/Panel.cs rename to osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs index c990a0ea46..040ff22f4e 100644 --- a/osu.Game/Beatmaps/Drawables/Panel.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs @@ -1,41 +1,44 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using System; -using osu.Framework; +using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; -using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; using osu.Framework.Input; +using osu.Framework.MathUtils; +using osu.Game.Graphics; using OpenTK; using OpenTK.Graphics; -using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.MathUtils; -using osu.Framework.Graphics.Shapes; -using osu.Game.Graphics; -namespace osu.Game.Beatmaps.Drawables +namespace osu.Game.Screens.Select.Carousel { - public class Panel : Container, IStateful + public abstract class DrawableCarouselItem : Container { public const float MAX_HEIGHT = 80; - public event Action StateChanged; - public override bool RemoveWhenNotAlive => false; - private readonly Container nestedContainer; + public override bool IsPresent => base.IsPresent || Item.Visible; + public readonly CarouselItem Item; + + private readonly Container nestedContainer; private readonly Container borderContainer; private readonly Box hoverLayer; protected override Container Content => nestedContainer; - protected Panel() + protected DrawableCarouselItem(CarouselItem item) { + Item = item; + Item.Filtered.ValueChanged += v => Schedule(ApplyState); + Item.State.ValueChanged += v => Schedule(ApplyState); + Height = MAX_HEIGHT; RelativeSizeAxes = Axes.X; @@ -59,8 +62,6 @@ namespace osu.Game.Beatmaps.Drawables }, } }); - - Alpha = 0; } private SampleChannel sampleHover; @@ -86,10 +87,7 @@ namespace osu.Game.Beatmaps.Drawables base.OnHoverLost(state); } - public void SetMultiplicativeAlpha(float alpha) - { - borderContainer.Alpha = alpha; - } + public void SetMultiplicativeAlpha(float alpha) => borderContainer.Alpha = alpha; protected override void LoadComplete() { @@ -97,49 +95,30 @@ namespace osu.Game.Beatmaps.Drawables ApplyState(); } - protected virtual void ApplyState(PanelSelectedState last = PanelSelectedState.Hidden) + protected virtual void ApplyState() { if (!IsLoaded) return; - switch (state) + switch (Item.State.Value) { - case PanelSelectedState.Hidden: - case PanelSelectedState.NotSelected: + case CarouselItemState.NotSelected: Deselected(); break; - case PanelSelectedState.Selected: + case CarouselItemState.Selected: Selected(); break; } - if (state == PanelSelectedState.Hidden) + if (!Item.Visible) this.FadeOut(300, Easing.OutQuint); else this.FadeIn(250); } - private PanelSelectedState state = PanelSelectedState.NotSelected; - - public PanelSelectedState State - { - get { return state; } - - set - { - if (state == value) - return; - - var last = state; - state = value; - - ApplyState(last); - - StateChanged?.Invoke(State); - } - } - protected virtual void Selected() { + Item.State.Value = CarouselItemState.Selected; + borderContainer.BorderThickness = 2.5f; borderContainer.EdgeEffect = new EdgeEffectParameters { @@ -152,6 +131,8 @@ namespace osu.Game.Beatmaps.Drawables protected virtual void Deselected() { + Item.State.Value = CarouselItemState.NotSelected; + borderContainer.BorderThickness = 0; borderContainer.EdgeEffect = new EdgeEffectParameters { @@ -164,15 +145,8 @@ namespace osu.Game.Beatmaps.Drawables protected override bool OnClick(InputState state) { - State = PanelSelectedState.Selected; + Item.State.Value = CarouselItemState.Selected; return true; } } - - public enum PanelSelectedState - { - Hidden, - NotSelected, - Selected - } } diff --git a/osu.Game/Screens/Select/EditSongSelect.cs b/osu.Game/Screens/Select/EditSongSelect.cs index 907c080729..f02d25501e 100644 --- a/osu.Game/Screens/Select/EditSongSelect.cs +++ b/osu.Game/Screens/Select/EditSongSelect.cs @@ -1,14 +1,12 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using osu.Framework.Input; - namespace osu.Game.Screens.Select { public class EditSongSelect : SongSelect { protected override bool ShowFooter => false; - protected override void OnSelected(InputState state) => Exit(); + protected override void Start() => Exit(); } } diff --git a/osu.Game/Screens/Select/FilterCriteria.cs b/osu.Game/Screens/Select/FilterCriteria.cs index f410c69212..8e99e29c1f 100644 --- a/osu.Game/Screens/Select/FilterCriteria.cs +++ b/osu.Game/Screens/Select/FilterCriteria.cs @@ -1,11 +1,6 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using System; -using System.Collections.Generic; -using System.Linq; -using osu.Game.Beatmaps; -using osu.Game.Beatmaps.Drawables; using osu.Game.Rulesets; using osu.Game.Screens.Select.Filter; @@ -18,54 +13,5 @@ namespace osu.Game.Screens.Select public string SearchText; public RulesetInfo Ruleset; public bool AllowConvertedBeatmaps; - - private bool canConvert(BeatmapInfo beatmapInfo) => beatmapInfo.RulesetID == Ruleset.ID || beatmapInfo.RulesetID == 0 && Ruleset.ID > 0 && AllowConvertedBeatmaps; - - public void Filter(List groups) - { - foreach (var g in groups) - { - var set = g.BeatmapSet; - - // we only support converts from osu! mode to other modes for now. - // in the future this will have to change, at which point this condition will become a touch more complicated. - bool hasCurrentMode = set.Beatmaps.Any(canConvert); - - bool match = hasCurrentMode; - - if (!string.IsNullOrEmpty(SearchText)) - match &= set.Metadata.SearchableTerms.Any(term => term.IndexOf(SearchText, StringComparison.InvariantCultureIgnoreCase) >= 0); - - foreach (var panel in g.BeatmapPanels) - panel.Filtered.Value = !canConvert(panel.Beatmap); - - switch (g.State) - { - case BeatmapGroupState.Hidden: - if (match) g.State = BeatmapGroupState.Collapsed; - break; - default: - if (!match) g.State = BeatmapGroupState.Hidden; - break; - } - } - - switch (Sort) - { - default: - case SortMode.Artist: - groups.Sort((x, y) => string.Compare(x.BeatmapSet.Metadata.Artist, y.BeatmapSet.Metadata.Artist, StringComparison.InvariantCultureIgnoreCase)); - break; - case SortMode.Title: - groups.Sort((x, y) => string.Compare(x.BeatmapSet.Metadata.Title, y.BeatmapSet.Metadata.Title, StringComparison.InvariantCultureIgnoreCase)); - break; - case SortMode.Author: - groups.Sort((x, y) => string.Compare(x.BeatmapSet.Metadata.Author.Username, y.BeatmapSet.Metadata.Author.Username, StringComparison.InvariantCultureIgnoreCase)); - break; - case SortMode.Difficulty: - groups.Sort((x, y) => x.BeatmapSet.MaxStarDifficulty.CompareTo(y.BeatmapSet.MaxStarDifficulty)); - break; - } - } } } diff --git a/osu.Game/Screens/Select/MatchSongSelect.cs b/osu.Game/Screens/Select/MatchSongSelect.cs index 2d3b198478..898c195432 100644 --- a/osu.Game/Screens/Select/MatchSongSelect.cs +++ b/osu.Game/Screens/Select/MatchSongSelect.cs @@ -1,12 +1,10 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using osu.Framework.Input; - namespace osu.Game.Screens.Select { public class MatchSongSelect : SongSelect { - protected override void OnSelected(InputState state) => Exit(); + protected override void Start() => Exit(); } } diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs index bba6ddf577..565a7a0a01 100644 --- a/osu.Game/Screens/Select/PlaySongSelect.cs +++ b/osu.Game/Screens/Select/PlaySongSelect.cs @@ -8,7 +8,6 @@ using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Input; using osu.Framework.Screens; using osu.Game.Beatmaps; using osu.Game.Graphics; @@ -114,22 +113,22 @@ namespace osu.Game.Screens.Select return false; } - protected override void OnSelected(InputState state) + protected override void Start() { if (player != null) return; - if (state?.Keyboard.ControlPressed == true) - { - var auto = Ruleset.Value.CreateInstance().GetAutoplayMod(); - var autoType = auto.GetType(); + //if (state?.Keyboard.ControlPressed == true) + //{ + // var auto = Ruleset.Value.CreateInstance().GetAutoplayMod(); + // var autoType = auto.GetType(); - var mods = modSelect.SelectedMods.Value; - if (mods.All(m => m.GetType() != autoType)) - { - modSelect.SelectedMods.Value = mods.Concat(new[] { auto }); - removeAutoModOnResume = true; - } - } + // var mods = modSelect.SelectedMods.Value; + // if (mods.All(m => m.GetType() != autoType)) + // { + // modSelect.SelectedMods.Value = mods.Concat(new[] { auto }); + // removeAutoModOnResume = true; + // } + //} Beatmap.Value.Track.Looping = false; Beatmap.Disabled = true; diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 7fb6a82981..b2e2c8bac6 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -67,6 +67,10 @@ namespace osu.Game.Screens.Select public readonly FilterControl FilterControl; + private DependencyContainer dependencies; + + protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent) => dependencies = new DependencyContainer(parent); + protected SongSelect() { const float carousel_width = 640; @@ -106,13 +110,11 @@ namespace osu.Game.Screens.Select Size = new Vector2(carousel_width, 1), Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, + + //todo: clicking play on another map doesn't work bindable disabled SelectionChanged = carouselSelectionChanged, BeatmapsChanged = carouselBeatmapsLoaded, - DeleteRequested = promptDelete, - RestoreRequested = s => { foreach (var b in s.Beatmaps) beatmaps.Restore(b); }, - EditRequested = editRequested, - HideDifficultyRequested = b => beatmaps.Hide(b), - StartRequested = () => carouselRaisedStart(), + //RestoreRequested = s => { foreach (var b in s.Beatmaps) beatmaps.Restore(b); }, }); Add(FilterControl = new FilterControl { @@ -163,12 +165,14 @@ namespace osu.Game.Screens.Select [BackgroundDependencyLoader(permitNulls: true)] private void load(BeatmapManager beatmaps, AudioManager audio, DialogOverlay dialog, OsuGame osu, OsuColour colours) { + dependencies.Cache(this); + if (Footer != null) { Footer.AddButton(@"random", colours.Green, triggerRandom, Key.F2); Footer.AddButton(@"options", colours.Blue, BeatmapOptions, Key.F3); - BeatmapOptions.AddButton(@"Delete", @"Beatmap", FontAwesome.fa_trash, colours.Pink, () => promptDelete(Beatmap.Value.BeatmapSetInfo), Key.Number4, float.MaxValue); + BeatmapOptions.AddButton(@"Delete", @"Beatmap", FontAwesome.fa_trash, colours.Pink, () => Delete(Beatmap.Value.BeatmapSetInfo), Key.Number4, float.MaxValue); } if (this.beatmaps == null) @@ -197,7 +201,7 @@ namespace osu.Game.Screens.Select carousel.AllowSelection = !Beatmap.Disabled; } - private void editRequested(BeatmapInfo beatmap) + public void Edit(BeatmapInfo beatmap) { Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap, Beatmap); Push(new Editor()); @@ -229,7 +233,7 @@ namespace osu.Game.Screens.Select carousel.SelectNextRandom(); } - private void carouselRaisedStart(InputState state = null) + public void Start(BeatmapInfo beatmap) { // if we have a pending filter operation, we want to run it now. // it could change selection (ie. if the ruleset has been changed). @@ -242,7 +246,9 @@ namespace osu.Game.Screens.Select selectionChangedDebounce = null; } - OnSelected(state); + carousel.SelectBeatmap(beatmap); + + Start(); } private ScheduledDelegate selectionChangedDebounce; @@ -261,7 +267,7 @@ namespace osu.Game.Screens.Select // In these cases, the other component has already loaded the beatmap, so we don't need to do so again. if (beatmap?.Equals(Beatmap.Value.BeatmapInfo) != true) { - bool preview = beatmap?.BeatmapSetInfoID != Beatmap.Value.BeatmapInfo.BeatmapSetInfoID; + bool preview = beatmap?.BeatmapSetInfoID != Beatmap.Value?.BeatmapInfo.BeatmapSetInfoID; Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap, Beatmap); ensurePlayingSelected(preview); @@ -301,7 +307,7 @@ namespace osu.Game.Screens.Select carousel.SelectNextRandom(); } - protected abstract void OnSelected(InputState state); + protected abstract void Start(); private void filterChanged(FilterCriteria criteria, bool debounce = true) { @@ -346,7 +352,7 @@ namespace osu.Game.Screens.Select logo.Action = () => { - carouselRaisedStart(); + Start(); return false; }; } @@ -458,7 +464,7 @@ namespace osu.Game.Screens.Select Beatmap.SetDefault(); } - private void promptDelete(BeatmapSetInfo beatmap) + public void Delete(BeatmapSetInfo beatmap) { if (beatmap == null) return; @@ -474,13 +480,13 @@ namespace osu.Game.Screens.Select { case Key.KeypadEnter: case Key.Enter: - carouselRaisedStart(state); + Start(); return true; case Key.Delete: if (state.Keyboard.ShiftPressed) { if (!Beatmap.IsDefault) - promptDelete(Beatmap.Value.BeatmapSetInfo); + Delete(Beatmap.Value.BeatmapSetInfo); return true; } break; diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 53ad323134..0bc7fa3f67 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -262,10 +262,7 @@ - - - @@ -316,7 +313,6 @@ - @@ -756,6 +752,14 @@ + + + + + + + + @@ -855,4 +859,4 @@ - + \ No newline at end of file