Merge branch 'master' into leaderboards

This commit is contained in:
Seth
2017-03-17 18:23:01 -03:00
committed by GitHub
92 changed files with 1853 additions and 905 deletions

View File

@ -15,46 +15,234 @@ using OpenTK.Input;
using System.Collections;
using osu.Framework.MathUtils;
using System.Diagnostics;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Threading;
namespace osu.Game.Screens.Select
{
internal class CarouselContainer : ScrollContainer, IEnumerable<BeatmapGroup>
internal class BeatmapCarousel : ScrollContainer, IEnumerable<BeatmapGroup>
{
private Container<Panel> scrollableContent;
private List<BeatmapGroup> groups = new List<BeatmapGroup>();
private List<Panel> panels = new List<Panel>();
public BeatmapInfo SelectedBeatmap => selectedPanel?.Beatmap;
public BeatmapGroup SelectedGroup { get; private set; }
public BeatmapPanel SelectedPanel { get; private set; }
public Action BeatmapsChanged;
public IEnumerable<BeatmapSetInfo> Beatmaps
{
get
{
return groups.Select(g => g.BeatmapSet);
}
set
{
scrollableContent.Clear(false);
panels.Clear();
groups.Clear();
List<BeatmapGroup> newGroups = null;
Task.Run(() =>
{
newGroups = value.Select(createGroup).ToList();
criteria.Filter(newGroups);
}).ContinueWith(t =>
{
Schedule(() =>
{
foreach (var g in newGroups)
addGroup(g);
computeYPositions();
BeatmapsChanged?.Invoke();
});
});
}
}
private List<float> yPositions = new List<float>();
public CarouselContainer()
{
DistanceDecayJump = 0.01;
/// <summary>
/// Required for now unfortunately.
/// </summary>
private BeatmapDatabase database;
private Container<Panel> scrollableContent;
private List<BeatmapGroup> groups = new List<BeatmapGroup>();
private List<Panel> panels = new List<Panel>();
private BeatmapGroup selectedGroup;
private BeatmapPanel selectedPanel;
public BeatmapCarousel()
{
Add(scrollableContent = new Container<Panel>
{
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);
}
computeYPositions();
addGroup(group);
computeYPositions();
if (selectedGroup == null)
selectGroup(group);
});
}
public void RemoveGroup(BeatmapGroup 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<BeatmapGroup, BeatmapInfo> 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<BeatmapGroup> 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);
}
private FilterCriteria criteria = new FilterCriteria();
private ScheduledDelegate filterTask;
public void Filter(FilterCriteria newCriteria = null, bool debounce = true)
{
if (!IsLoaded) return;
criteria = newCriteria ?? criteria ?? new FilterCriteria();
Action perform = delegate
{
filterTask = null;
criteria.Filter(groups);
var filtered = new List<BeatmapGroup>(groups);
scrollableContent.Clear(false);
panels.Clear();
groups.Clear();
foreach (var g in filtered)
addGroup(g);
computeYPositions();
if (selectedGroup == null || selectedGroup.State == BeatmapGroupState.Hidden)
SelectNext();
};
filterTask?.Cancel();
if (debounce)
filterTask = Scheduler.AddDelayed(perform, 250);
else
perform();
}
public IEnumerator<BeatmapGroup> 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);
@ -64,18 +252,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;
}
/// <summary>
/// Computes the target Y positions for every panel in the carousel.
/// </summary>
@ -98,7 +280,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);
@ -128,125 +310,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(FilterControl.SortMode mode)
protected override bool OnKeyDown(InputState state, KeyDownEventArgs args)
{
List<BeatmapGroup> sortedGroups = new List<BeatmapGroup>(groups);
switch (mode)
int direction = 0;
bool skipDifficulties = false;
switch (args.Key)
{
case FilterControl.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 FilterControl.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 FilterControl.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 FilterControl.SortMode.Difficulty:
sortedGroups.Sort((x, y) =>
{
float xAverage = 0, yAverage = 0;
int counter = 0;
foreach (BeatmapInfo set in x.BeatmapSet.Beatmaps)
{
xAverage += set.StarDifficulty;
counter++;
}
xAverage /= counter;
counter = 0;
foreach (BeatmapInfo set in y.BeatmapSet.Beatmaps)
{
yAverage += set.StarDifficulty;
counter++;
}
yAverage /= counter;
if (xAverage > yAverage)
return 1;
else
return -1;
});
case Key.Right:
direction = 1;
skipDifficulties = true;
break;
default:
throw new NotImplementedException();
}
scrollableContent.Clear(false);
panels.Clear();
groups.Clear();
if (direction == 0)
return base.OnKeyDown(state, args);
foreach (BeatmapGroup group in sortedGroups)
AddGroup(group);
}
/// <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)
{
// 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;
}
/// <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)
{
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()
@ -295,80 +414,46 @@ namespace osu.Game.Screens.Select
updatePanel(p, halfHeight);
}
protected override bool OnKeyDown(InputState state, KeyDownEventArgs args)
/// <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)
{
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)
/// <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)
{
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<BeatmapGroup> 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<BeatmapGroup> GetEnumerator() => groups.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}

View File

@ -217,12 +217,12 @@ namespace osu.Game.Screens.Select
private string getBPMRange(Beatmap beatmap)
{
double bpmMax = beatmap.BPMMaximum;
double bpmMin = beatmap.BPMMinimum;
double bpmMax = beatmap.TimingInfo.BPMMaximum;
double bpmMin = beatmap.TimingInfo.BPMMinimum;
if (Precision.AlmostEquals(bpmMin, bpmMax)) return Math.Round(bpmMin) + "bpm";
return Math.Round(bpmMin) + "-" + Math.Round(bpmMax) + "bpm (mostly " + Math.Round(beatmap.BPMMode) + "bpm)";
return Math.Round(bpmMin) + "-" + Math.Round(bpmMax) + "bpm (mostly " + Math.Round(beatmap.TimingInfo.BPMMode) + "bpm)";
}
public class InfoLabel : Container

View File

@ -0,0 +1,41 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.ComponentModel;
namespace osu.Game.Screens.Select.Filter
{
public enum GroupMode
{
[Description("All")]
All,
[Description("Artist")]
Artist,
[Description("Author")]
Author,
[Description("BPM")]
BPM,
[Description("Collections")]
Collections,
[Description("Date Added")]
DateAdded,
[Description("Difficulty")]
Difficulty,
[Description("Favorites")]
Favorites,
[Description("Length")]
Length,
[Description("My Maps")]
MyMaps,
[Description("No Grouping")]
NoGrouping,
[Description("Rank Achieved")]
RankAchieved,
[Description("Ranked Status")]
RankedStatus,
[Description("Recently Played")]
RecentlyPlayed,
[Description("Title")]
Title
}
}

View File

@ -0,0 +1,27 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.ComponentModel;
namespace osu.Game.Screens.Select.Filter
{
public enum SortMode
{
[Description("Artist")]
Artist,
[Description("Author")]
Author,
[Description("BPM")]
BPM,
[Description("Date Added")]
DateAdded,
[Description("Difficulty")]
Difficulty,
[Description("Length")]
Length,
[Description("Rank Achieved")]
RankAchieved,
[Description("Title")]
Title
}
}

View File

@ -5,37 +5,71 @@ using System;
using OpenTK;
using OpenTK.Graphics;
using osu.Framework.Allocation;
using osu.Framework.Configuration;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Screens.Select.Filter;
using Container = osu.Framework.Graphics.Containers.Container;
using osu.Framework.Input;
using osu.Game.Modes;
namespace osu.Game.Screens.Select
{
public class FilterControl : Container
{
public Action FilterChanged;
public Action<FilterCriteria> FilterChanged;
private OsuTabControl<SortMode> sortTabs;
private TabControl<GroupMode> groupTabs;
public string Search => searchTextBox.Text;
private SortMode sort = SortMode.Title;
public SortMode Sort {
get { return sort; }
set {
public SortMode Sort
{
get { return sort; }
set
{
if (sort != value)
{
sort = value;
FilterChanged?.Invoke();
FilterChanged?.Invoke(CreateCriteria());
}
}
}
}
private GroupMode group = GroupMode.All;
public GroupMode Group
{
get { return group; }
set
{
if (group != value)
{
group = value;
FilterChanged?.Invoke(CreateCriteria());
}
}
}
public FilterCriteria CreateCriteria() => new FilterCriteria
{
Group = group,
Sort = sort,
SearchText = searchTextBox.Text,
Mode = playMode
};
public Action Exit;
private SearchTextBox searchTextBox;
protected override bool InternalContains(Vector2 screenSpacePos) => base.InternalContains(screenSpacePos) || groupTabs.Contains(screenSpacePos) || sortTabs.Contains(screenSpacePos);
public FilterControl()
{
Children = new Drawable[]
@ -46,30 +80,79 @@ namespace osu.Game.Screens.Select
Alpha = 0.8f,
RelativeSizeAxes = Axes.Both,
},
new FillFlowContainer
new Container
{
Padding = new MarginPadding(20),
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
AlwaysReceiveInput = true,
RelativeSizeAxes = Axes.Both,
Width = 0.5f,
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Width = 0.4f, // TODO: InnerWidth property or something
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
searchTextBox = new SearchTextBox {
searchTextBox = new SearchTextBox
{
RelativeSizeAxes = Axes.X,
OnChange = (sender, newText) =>
{
if (newText)
FilterChanged?.Invoke();
FilterChanged?.Invoke(CreateCriteria());
},
Exit = () => Exit?.Invoke(),
},
new GroupSortTabs()
new Box
{
RelativeSizeAxes = Axes.X,
Height = 1,
Colour = OsuColour.Gray(80),
Origin = Anchor.BottomLeft,
Anchor = Anchor.BottomLeft,
},
new FillFlowContainer
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
Direction = FillDirection.Horizontal,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
AlwaysReceiveInput = true,
Children = new Drawable[]
{
groupTabs = new OsuTabControl<GroupMode>
{
RelativeSizeAxes = Axes.X,
Height = 24,
Width = 0.5f,
AutoSort = true
},
//spriteText = new OsuSpriteText
//{
// Font = @"Exo2.0-Bold",
// Text = "Sort results by",
// TextSize = 14,
// Margin = new MarginPadding
// {
// Top = 5,
// Bottom = 5
// },
//},
sortTabs = new OsuTabControl<SortMode>()
{
RelativeSizeAxes = Axes.X,
Width = 0.5f,
Height = 24,
AutoSort = true,
}
}
},
}
}
};
groupTabs.PinItem(GroupMode.All);
groupTabs.PinItem(GroupMode.RecentlyPlayed);
groupTabs.ItemChanged += (sender, value) => Group = value;
sortTabs.ItemChanged += (sender, value) => Sort = value;
}
public void Deactivate()
@ -77,214 +160,29 @@ namespace osu.Game.Screens.Select
searchTextBox.HoldFocus = false;
searchTextBox.TriggerFocusLost();
}
public void Activate()
{
searchTextBox.HoldFocus = true;
}
private class TabItem : ClickableContainer
private readonly Bindable<PlayMode> playMode = new Bindable<PlayMode>();
[BackgroundDependencyLoader(permitNulls:true)]
private void load(OsuColour colours, OsuGame osu)
{
public string Text
{
get { return text.Text; }
set { text.Text = value; }
}
sortTabs.AccentColour = colours.GreenLight;
private void fadeActive()
{
box.FadeIn(300);
text.FadeColour(Color4.White, 300);
}
private void fadeInactive()
{
box.FadeOut(300);
text.FadeColour(fadeColour, 300);
}
private bool active;
public bool Active
{
get { return active; }
set
{
active = value;
if (active)
fadeActive();
else
fadeInactive();
}
}
private SpriteText text;
private Box box;
private Color4 fadeColour;
protected override bool OnHover(InputState state)
{
if (!active)
fadeActive();
return true;
}
protected override void OnHoverLost(InputState state)
{
if (!active)
fadeInactive();
}
public TabItem()
{
AutoSizeAxes = Axes.Both;
Children = new Drawable[]
{
text = new OsuSpriteText
{
Margin = new MarginPadding(5),
TextSize = 14,
Font = @"Exo2.0-Bold",
},
box = new Box
{
RelativeSizeAxes = Axes.X,
Height = 1,
Alpha = 0,
Colour = Color4.White,
Origin = Anchor.BottomLeft,
Anchor = Anchor.BottomLeft,
}
};
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
text.Colour = colours.Blue;
fadeColour = colours.Blue;
}
if (osu != null)
playMode.BindTo(osu.PlayMode);
}
private class GroupSortTabs : Container
{
private TextAwesome groupsEllipsis, sortEllipsis;
private SpriteText sortLabel;
public GroupSortTabs()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.X,
Height = 1,
Colour = OsuColour.Gray(80),
Origin = Anchor.BottomLeft,
Anchor = Anchor.BottomLeft,
},
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(10, 0),
Children = new Drawable[]
{
new TabItem
{
Text = "All",
Active = true,
},
new TabItem
{
Text = "Recently Played",
Active = false,
},
new TabItem
{
Text = "Collections",
Active = false,
},
groupsEllipsis = new TextAwesome
{
Icon = FontAwesome.fa_ellipsis_h,
Origin = Anchor.TopLeft,
TextSize = 14,
Margin = new MarginPadding { Top = 5, Bottom = 5 },
}
}
},
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(10, 0),
Origin = Anchor.TopRight,
Anchor = Anchor.TopRight,
Children = new Drawable[]
{
sortLabel = new OsuSpriteText
{
Font = @"Exo2.0-Bold",
Text = "Sort results by",
TextSize = 14,
Margin = new MarginPadding { Top = 5, Bottom = 5 },
},
new TabItem
{
Text = "Artist",
Active = true,
},
sortEllipsis = new TextAwesome
{
Icon = FontAwesome.fa_ellipsis_h,
Origin = Anchor.TopLeft,
TextSize = 14,
Margin = new MarginPadding { Top = 5, Bottom = 5 },
}
}
},
};
}
protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) => true;
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
groupsEllipsis.Colour = colours.Blue;
sortLabel.Colour = colours.GreenLight;
sortEllipsis.Colour = colours.GreenLight;
}
}
public enum SortMode
{
Artist,
BPM,
Author,
DateAdded,
Difficulty,
Length,
RankAchieved,
Title
}
protected override bool OnMouseMove(InputState state) => true;
public enum GroupMode
{
NoGrouping,
Artist,
BPM,
Author,
DateAdded,
Difficulty,
Length,
RankAchieved,
Title,
Collections,
Favorites,
MyMaps,
RankedStatus,
RecentlyPlayed
}
protected override bool OnClick(InputState state) => true;
protected override bool OnDragStart(InputState state) => true;
}
}

View File

@ -0,0 +1,65 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// 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.Drawables;
using osu.Game.Modes;
using osu.Game.Screens.Select.Filter;
namespace osu.Game.Screens.Select
{
public class FilterCriteria
{
public GroupMode Group;
public SortMode Sort;
public string SearchText;
public PlayMode Mode;
public void Filter(List<BeatmapGroup> groups)
{
foreach (var g in groups)
{
var set = g.BeatmapSet;
bool hasCurrentMode = set.Beatmaps.Any(bm => bm.Mode == Mode);
bool match = hasCurrentMode;
match &= string.IsNullOrEmpty(SearchText)
|| (set.Metadata.Artist ?? string.Empty).IndexOf(SearchText, StringComparison.InvariantCultureIgnoreCase) != -1
|| (set.Metadata.ArtistUnicode ?? string.Empty).IndexOf(SearchText, StringComparison.InvariantCultureIgnoreCase) != -1
|| (set.Metadata.Title ?? string.Empty).IndexOf(SearchText, StringComparison.InvariantCultureIgnoreCase) != -1
|| (set.Metadata.TitleUnicode ?? string.Empty).IndexOf(SearchText, StringComparison.InvariantCultureIgnoreCase) != -1;
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, y.BeatmapSet.Metadata.Author, StringComparison.InvariantCultureIgnoreCase));
break;
case SortMode.Difficulty:
groups.Sort((x, y) => x.BeatmapSet.MaxStarDifficulty.CompareTo(y.BeatmapSet.MaxStarDifficulty));
break;
}
}
}
}

View File

@ -26,8 +26,6 @@ namespace osu.Game.Screens.Select
private const float padding = 80;
public override bool Contains(Vector2 screenSpacePos) => true;
public Action OnBack;
public Action OnStart;
@ -69,6 +67,8 @@ namespace osu.Game.Screens.Select
public Footer()
{
AlwaysReceiveInput = true;
const float bottom_tool_height = 50;
RelativeSizeAxes = Axes.X;

View File

@ -83,7 +83,7 @@ namespace osu.Game.Screens.Select.Options
return false;
}
public override bool Contains(Vector2 screenSpacePos) => box.Contains(screenSpacePos);
protected override bool InternalContains(Vector2 screenSpacePos) => box.Contains(screenSpacePos);
public BeatmapOptionsButton()
{

View File

@ -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;
@ -19,7 +17,6 @@ using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Transforms;
using osu.Framework.Input;
using osu.Framework.Screens;
using osu.Framework.Threading;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Database;
@ -38,7 +35,7 @@ namespace osu.Game.Screens.Select
private BeatmapDatabase database;
protected override BackgroundScreen CreateBackground() => new BackgroundScreenBeatmap(Beatmap);
private CarouselContainer carousel;
private BeatmapCarousel carousel;
private TrackManager trackManager;
private DialogOverlay dialogOverlay;
@ -57,43 +54,25 @@ namespace osu.Game.Screens.Select
private SampleChannel sampleChangeDifficulty;
private SampleChannel sampleChangeBeatmap;
private List<BeatmapGroup> beatmapGroups;
protected virtual bool ShowFooter => true;
/// <summary>
/// Can be null if <see cref="ShowFooter"/> == false
/// Can be null if <see cref="ShowFooter"/> is false.
/// </summary>
protected readonly BeatmapOptionsOverlay BeatmapOptions;
/// <summary>
/// Can be null if <see cref="ShowFooter"/> == false
/// Can be null if <see cref="ShowFooter"/> is false.
/// </summary>
protected readonly Footer Footer;
private FilterControl filter;
public FilterControl Filter
{
get
{
return filter;
}
private set
{
if (filter != value)
{
filter = value;
filterChanged();
}
}
}
public readonly FilterControl FilterControl;
protected SongSelect()
{
const float carousel_width = 640;
const float filter_height = 100;
beatmapGroups = new List<BeatmapGroup>();
Add(new ParallaxContainer
{
Padding = new MarginPadding { Top = filter_height },
@ -122,18 +101,20 @@ namespace osu.Game.Screens.Select
Right = left_area_padding * 2,
}
});
Add(carousel = new CarouselContainer
Add(carousel = new BeatmapCarousel
{
RelativeSizeAxes = Axes.Y,
Size = new Vector2(carousel_width, 1),
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
SelectionChanged = selectionChanged,
StartRequested = raiseSelect
});
Add(filter = new FilterControl
Add(FilterControl = new FilterControl
{
RelativeSizeAxes = Axes.X,
Height = filter_height,
FilterChanged = () => filterChanged(),
FilterChanged = criteria => filterChanged(criteria),
Exit = Exit,
});
Add(beatmapInfoWedge = new BeatmapInfoWedge
@ -167,8 +148,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)
{
@ -196,7 +176,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<BeatmapSetInfo>().Where(b => !b.DeletePending);
}
private void beatmapsLoaded()
{
if (Beatmap != null)
carousel.SelectBeatmap(Beatmap.BeatmapInfo, false);
else
carousel.SelectNext();
}
private void raiseSelect()
@ -208,61 +197,15 @@ 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)
private void filterChanged(FilterCriteria criteria, bool debounce = true)
{
filterTask?.Cancel();
filterTask = Scheduler.AddDelayed(() =>
{
filterTask = null;
var search = filter.Search;
BeatmapGroup newSelection = null;
carousel.Sort(filter.Sort);
foreach (var beatmapGroup in carousel)
{
var set = beatmapGroup.BeatmapSet;
bool hasCurrentMode = set.Beatmaps.Any(bm => bm.Mode == playMode);
bool match = hasCurrentMode;
match &= string.IsNullOrEmpty(search)
|| (set.Metadata.Artist ?? "").IndexOf(search, StringComparison.InvariantCultureIgnoreCase) != -1
|| (set.Metadata.ArtistUnicode ?? "").IndexOf(search, StringComparison.InvariantCultureIgnoreCase) != -1
|| (set.Metadata.Title ?? "").IndexOf(search, StringComparison.InvariantCultureIgnoreCase) != -1
|| (set.Metadata.TitleUnicode ?? "").IndexOf(search, StringComparison.InvariantCultureIgnoreCase) != -1;
if (match)
{
if (newSelection == null || beatmapGroup.BeatmapSet.OnlineBeatmapSetID == Beatmap.BeatmapSetInfo.OnlineBeatmapSetID)
{
if (newSelection != null)
newSelection.State = BeatmapGroupState.Collapsed;
newSelection = beatmapGroup;
}
else
beatmapGroup.State = BeatmapGroupState.Collapsed;
}
else
{
beatmapGroup.State = BeatmapGroupState.Hidden;
}
}
if (newSelection != null)
{
if (newSelection.BeatmapPanels.Any(b => b.Beatmap.ID == Beatmap.BeatmapInfo.ID))
carousel.SelectBeatmap(Beatmap.BeatmapInfo, false);
else if (eagerSelection)
carousel.SelectBeatmap(newSelection.BeatmapSet.Beatmaps[0], false);
}
}, debounce ? 250 : 0);
carousel.Filter(criteria, debounce);
}
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));
@ -277,7 +220,7 @@ namespace osu.Game.Screens.Select
beatmapInfoWedge.State = Visibility.Visible;
filter.Activate();
FilterControl.Activate();
}
protected override void OnResuming(Screen last)
@ -290,7 +233,7 @@ namespace osu.Game.Screens.Select
Content.ScaleTo(1, 250, EasingTypes.OutSine);
filter.Activate();
FilterControl.Activate();
}
protected override void OnSuspending(Screen next)
@ -299,7 +242,7 @@ namespace osu.Game.Screens.Select
Content.FadeOut(250);
filter.Deactivate();
FilterControl.Deactivate();
base.OnSuspending(next);
}
@ -309,7 +252,7 @@ namespace osu.Game.Screens.Select
Content.FadeOut(100);
filter.Deactivate();
FilterControl.Deactivate();
return base.OnExiting(next);
}
@ -323,10 +266,7 @@ namespace osu.Game.Screens.Select
initialAddSetsTask.Cancel();
}
private void playMode_ValueChanged(object sender, EventArgs e)
{
filterChanged(false);
}
private void playMode_ValueChanged(object sender, EventArgs e) => carousel.Filter();
private void changeBackground(WorkingBeatmap beatmap)
{
@ -387,63 +327,18 @@ namespace osu.Game.Screens.Select
}
}
private void addBeatmapSet(BeatmapSetInfo beatmapSet, Framework.Game game, bool select = false)
private void selectBeatmap(BeatmapSetInfo beatmapSet = null)
{
beatmapSet = database.GetWithChildren<BeatmapSetInfo>(beatmapSet.ID);
beatmapSet.Beatmaps.ForEach(b =>
{
database.GetChildren(b);
if (b.Metadata == null) b.Metadata = beatmapSet.Metadata;
});
var group = new BeatmapGroup(beatmapSet, database)
{
SelectionChanged = selectionChanged,
StartRequested = b => raiseSelect()
};
//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
{
beatmapGroups.Add(group);
group.State = BeatmapGroupState.Collapsed;
carousel.AddGroup(group);
filterChanged(false, false);
if (Beatmap == null || select)
carousel.SelectBeatmap(beatmapSet.Beatmaps.First());
else
carousel.SelectBeatmap(Beatmap.BeatmapInfo);
}));
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;
}
private void addBeatmapSets(Framework.Game game, CancellationToken token)
{
foreach (var beatmapSet in database.Query<BeatmapSetInfo>().Where(b => !b.DeletePending))
{
if (token.IsCancellationRequested) return;
addBeatmapSet(beatmapSet, game);
}
}
private void promptDelete()
{
if (Beatmap != null)