Merge branch 'master' of https://github.com/ppy/osu into present-recommended

 Conflicts:
	osu.Game/OsuGameBase.cs
	osu.Game/Screens/Select/DifficultyRecommender.cs
	osu.Game/Screens/Select/SongSelect.cs
This commit is contained in:
Endrik Tombak
2020-11-21 13:59:59 +02:00
1470 changed files with 49711 additions and 13757 deletions

View File

@ -1,28 +1,29 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osuTK;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Configuration;
using osuTK.Input;
using osu.Framework.Utils;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Caching;
using osu.Framework.Threading;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Pooling;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Threading;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Extensions;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Cursor;
using osu.Game.Input.Bindings;
using osu.Game.Screens.Select.Carousel;
using osuTK;
using osuTK.Input;
namespace osu.Game.Screens.Select
{
@ -73,6 +74,18 @@ namespace osu.Game.Screens.Select
public override bool PropagatePositionalInputSubTree => AllowSelection;
public override bool PropagateNonPositionalInputSubTree => AllowSelection;
private (int first, int last) displayedRange;
/// <summary>
/// Extend the range to retain already loaded pooled drawables.
/// </summary>
private const float distance_offscreen_before_unload = 1024;
/// <summary>
/// Extend the range to update positions / retrieve pooled drawables outside of visible range.
/// </summary>
private const float distance_offscreen_to_preload = 512; // todo: adjust this appropriately once we can make set panel contents load while off-screen.
/// <summary>
/// Whether carousel items have completed asynchronously loaded.
/// </summary>
@ -93,20 +106,19 @@ namespace osu.Game.Screens.Select
{
CarouselRoot newRoot = new CarouselRoot(this);
beatmapSets.Select(createCarouselSet).Where(g => g != null).ForEach(newRoot.AddChild);
newRoot.Filter(activeCriteria);
// preload drawables as the ctor overhead is quite high currently.
_ = newRoot.Drawables;
newRoot.AddChildren(beatmapSets.Select(createCarouselSet).Where(g => g != null));
root = newRoot;
if (selectedBeatmapSet != null && !beatmapSets.Contains(selectedBeatmapSet.BeatmapSet))
selectedBeatmapSet = null;
scrollableContent.Clear(false);
ScrollableContent.Clear(false);
itemsCache.Invalidate();
scrollPositionCache.Invalidate();
// apply any pending filter operation that may have been delayed (see applyActiveCriteria's scheduling behaviour when BeatmapSetsLoaded is false).
FlushPendingFilterOperations();
// Run on late scheduler want to ensure this runs after all pending UpdateBeatmapSet / RemoveBeatmapSet operations are run.
SchedulerAfterChildren.Add(() =>
{
@ -115,11 +127,12 @@ namespace osu.Game.Screens.Select
});
}
private readonly List<float> yPositions = new List<float>();
private readonly List<CarouselItem> visibleItems = new List<CarouselItem>();
private readonly Cached itemsCache = new Cached();
private readonly Cached scrollPositionCache = new Cached();
private readonly Container<DrawableCarouselItem> scrollableContent;
protected readonly Container<DrawableCarouselItem> ScrollableContent;
public Bindable<bool> RightClickScrollingEnabled = new Bindable<bool>();
@ -127,8 +140,6 @@ namespace osu.Game.Screens.Select
private readonly List<CarouselBeatmapSet> previouslyVisitedRandomSets = new List<CarouselBeatmapSet>();
private readonly Stack<CarouselBeatmap> randomSelectedBeatmaps = new Stack<CarouselBeatmap>();
protected List<DrawableCarouselItem> Items = new List<DrawableCarouselItem>();
private CarouselRoot root;
private IBindable<WeakReference<BeatmapSetInfo>> itemUpdated;
@ -136,6 +147,8 @@ namespace osu.Game.Screens.Select
private IBindable<WeakReference<BeatmapInfo>> itemHidden;
private IBindable<WeakReference<BeatmapInfo>> itemRestored;
private readonly DrawablePool<DrawableCarouselBeatmapSet> setPool = new DrawablePool<DrawableCarouselBeatmapSet>(100);
public BeatmapCarousel()
{
root = new CarouselRoot(this);
@ -146,9 +159,13 @@ namespace osu.Game.Screens.Select
{
Masking = false,
RelativeSizeAxes = Axes.Both,
Child = scrollableContent = new Container<DrawableCarouselItem>
Children = new Drawable[]
{
RelativeSizeAxes = Axes.X,
setPool,
ScrollableContent = new Container<DrawableCarouselItem>
{
RelativeSizeAxes = Axes.X,
}
}
}
};
@ -175,7 +192,8 @@ namespace osu.Game.Screens.Select
itemRestored = beatmaps.BeatmapRestored.GetBoundCopy();
itemRestored.BindValueChanged(beatmapRestored);
loadBeatmapSets(GetLoadableBeatmaps());
if (!beatmapSets.Any())
loadBeatmapSets(GetLoadableBeatmaps());
}
protected virtual IEnumerable<BeatmapSetInfo> GetLoadableBeatmaps() => beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.AllButFiles);
@ -301,6 +319,9 @@ namespace osu.Game.Screens.Select
private void selectNextDifficulty(int direction)
{
if (selectedBeatmap == null)
return;
var unfilteredDifficulties = selectedBeatmapSet.Children.Where(s => !s.Filtered.Value).ToList();
int index = unfilteredDifficulties.IndexOf(selectedBeatmap);
@ -317,6 +338,9 @@ namespace osu.Game.Screens.Select
/// <returns>True if a selection could be made, else False.</returns>
public bool SelectNextRandom()
{
if (!AllowSelection)
return false;
var visibleSets = beatmapSets.Where(s => !s.Filtered.Value).ToList();
if (!visibleSets.Any())
return false;
@ -423,7 +447,19 @@ namespace osu.Game.Screens.Select
private void applyActiveCriteria(bool debounce, bool alwaysResetScrollPosition = true)
{
if (root.Children.Any() != true) return;
PendingFilter?.Cancel();
PendingFilter = null;
if (debounce)
PendingFilter = Scheduler.AddDelayed(perform, 250);
else
{
// if initial load is not yet finished, this will be run inline in loadBeatmapSets to ensure correct order of operation.
if (!BeatmapSetsLoaded)
PendingFilter = Schedule(perform);
else
perform();
}
void perform()
{
@ -435,14 +471,6 @@ namespace osu.Game.Screens.Select
if (alwaysResetScrollPosition || !scroll.UserScrolling)
ScrollToSelected();
}
PendingFilter?.Cancel();
PendingFilter = null;
if (debounce)
PendingFilter = Scheduler.AddDelayed(perform, 250);
else
perform();
}
private float? scrollTarget;
@ -452,32 +480,49 @@ namespace osu.Game.Screens.Select
/// </summary>
public void ScrollToSelected() => scrollPositionCache.Invalidate();
#region Key / button selection logic
protected override bool OnKeyDown(KeyDownEvent e)
{
switch (e.Key)
{
case Key.Left:
SelectNext(-1, true);
if (!e.Repeat)
beginRepeatSelection(() => SelectNext(-1, true), e.Key);
return true;
case Key.Right:
SelectNext(1, true);
if (!e.Repeat)
beginRepeatSelection(() => SelectNext(1, true), e.Key);
return true;
}
return false;
}
protected override void OnKeyUp(KeyUpEvent e)
{
switch (e.Key)
{
case Key.Left:
case Key.Right:
endRepeatSelection(e.Key);
break;
}
base.OnKeyUp(e);
}
public bool OnPressed(GlobalAction action)
{
switch (action)
{
case GlobalAction.SelectNext:
SelectNext(1, false);
beginRepeatSelection(() => SelectNext(1, false), action);
return true;
case GlobalAction.SelectPrevious:
SelectNext(-1, false);
beginRepeatSelection(() => SelectNext(-1, false), action);
return true;
}
@ -486,77 +531,143 @@ namespace osu.Game.Screens.Select
public void OnReleased(GlobalAction action)
{
switch (action)
{
case GlobalAction.SelectNext:
case GlobalAction.SelectPrevious:
endRepeatSelection(action);
break;
}
}
private ScheduledDelegate repeatDelegate;
private object lastRepeatSource;
/// <summary>
/// Begin repeating the specified selection action.
/// </summary>
/// <param name="action">The action to perform.</param>
/// <param name="source">The source of the action. Used in conjunction with <see cref="endRepeatSelection"/> to only cancel the correct action (most recently pressed key).</param>
private void beginRepeatSelection(Action action, object source)
{
endRepeatSelection();
lastRepeatSource = source;
repeatDelegate = this.BeginKeyRepeat(Scheduler, action);
}
private void endRepeatSelection(object source = null)
{
// only the most recent source should be able to cancel the current action.
if (source != null && !EqualityComparer<object>.Default.Equals(lastRepeatSource, source))
return;
repeatDelegate?.Cancel();
repeatDelegate = null;
lastRepeatSource = null;
}
#endregion
protected override void Update()
{
base.Update();
if (!itemsCache.IsValid)
updateItems();
bool revalidateItems = !itemsCache.IsValid;
// Remove all items that should no longer be on-screen
scrollableContent.RemoveAll(p => p.Y < visibleUpperBound - p.DrawHeight || p.Y > visibleBottomBound || !p.IsPresent);
// First we iterate over all non-filtered carousel items and populate their
// vertical position data.
if (revalidateItems)
updateYPositions();
// Find index range of all items that should be on-screen
Trace.Assert(Items.Count == yPositions.Count);
// This data is consumed to find the currently displayable range.
// This is the range we want to keep drawables for, and should exceed the visible range slightly to avoid drawable churn.
var newDisplayRange = getDisplayRange();
int firstIndex = yPositions.BinarySearch(visibleUpperBound - DrawableCarouselItem.MAX_HEIGHT);
if (firstIndex < 0) firstIndex = ~firstIndex;
int lastIndex = yPositions.BinarySearch(visibleBottomBound);
if (lastIndex < 0) lastIndex = ~lastIndex;
int notVisibleCount = 0;
// Add those items within the previously found index range that should be displayed.
for (int i = firstIndex; i < lastIndex; ++i)
// If the filtered items or visible range has changed, pooling requirements need to be checked.
// This involves fetching new items from the pool, returning no-longer required items.
if (revalidateItems || newDisplayRange != displayedRange)
{
DrawableCarouselItem item = Items[i];
displayedRange = newDisplayRange;
if (!item.Item.Visible)
if (visibleItems.Count > 0)
{
if (!item.IsPresent)
notVisibleCount++;
continue;
}
var toDisplay = visibleItems.GetRange(displayedRange.first, displayedRange.last - displayedRange.first + 1);
float depth = i + (item is DrawableCarouselBeatmapSet ? -Items.Count : 0);
// Only add if we're not already part of the content.
if (!scrollableContent.Contains(item))
{
// Makes sure headers are always _below_ items,
// and depth flows downward.
item.Depth = depth;
switch (item.LoadState)
foreach (var panel in ScrollableContent.Children)
{
case LoadState.NotLoaded:
LoadComponentAsync(item);
break;
if (toDisplay.Remove(panel.Item))
{
// panel already displayed.
continue;
}
case LoadState.Loading:
break;
default:
scrollableContent.Add(item);
break;
// panel loaded as drawable but not required by visible range.
// remove but only if too far off-screen
if (panel.Y + panel.DrawHeight < visibleUpperBound - distance_offscreen_before_unload || panel.Y > visibleBottomBound + distance_offscreen_before_unload)
{
// may want a fade effect here (could be seen if a huge change happens, like a set with 20 difficulties becomes selected).
panel.ClearTransforms();
panel.Expire();
}
}
// Add those items within the previously found index range that should be displayed.
foreach (var item in toDisplay)
{
var panel = setPool.Get(p => p.Item = item);
panel.Depth = item.CarouselYPosition;
panel.Y = item.CarouselYPosition;
ScrollableContent.Add(panel);
}
}
else
{
scrollableContent.ChangeChildDepth(item, depth);
}
}
// this is not actually useful right now, but once we have groups may well be.
if (notVisibleCount > 50)
itemsCache.Invalidate();
// Finally, if the filtered items have changed, animate drawables to their new locations.
// This is common if a selected/collapsed state has changed.
if (revalidateItems)
{
foreach (DrawableCarouselItem panel in ScrollableContent.Children)
{
panel.MoveToY(panel.Item.CarouselYPosition, 800, Easing.OutQuint);
}
}
// Update externally controlled state of currently visible items
// (e.g. x-offset and opacity).
foreach (DrawableCarouselItem p in scrollableContent.Children)
updateItem(p);
// Update externally controlled state of currently visible items (e.g. x-offset and opacity).
// This is a per-frame update on all drawable panels.
foreach (DrawableCarouselItem item in ScrollableContent.Children)
{
updateItem(item);
if (item is DrawableCarouselBeatmapSet set)
{
foreach (var diff in set.DrawableBeatmaps)
updateItem(diff, item);
}
}
}
private readonly CarouselBoundsItem carouselBoundsItem = new CarouselBoundsItem();
private (int firstIndex, int lastIndex) getDisplayRange()
{
// Find index range of all items that should be on-screen
carouselBoundsItem.CarouselYPosition = visibleUpperBound - distance_offscreen_to_preload;
int firstIndex = visibleItems.BinarySearch(carouselBoundsItem);
if (firstIndex < 0) firstIndex = ~firstIndex;
carouselBoundsItem.CarouselYPosition = visibleBottomBound + distance_offscreen_to_preload;
int lastIndex = visibleItems.BinarySearch(carouselBoundsItem);
if (lastIndex < 0) lastIndex = ~lastIndex;
// as we can't be 100% sure on the size of individual carousel drawables,
// always play it safe and extend bounds by one.
firstIndex = Math.Max(0, firstIndex - 1);
lastIndex = Math.Clamp(lastIndex + 1, firstIndex, Math.Max(0, visibleItems.Count - 1));
return (firstIndex, lastIndex);
}
protected override void UpdateAfterChildren()
@ -567,15 +678,6 @@ namespace osu.Game.Screens.Select
updateScrollPosition();
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
// aggressively dispose "off-screen" items to reduce GC pressure.
foreach (var i in Items)
i.Dispose();
}
private void beatmapRemoved(ValueChangedEvent<WeakReference<BeatmapSetInfo>> weakItem)
{
if (weakItem.NewValue.TryGetTarget(out var item))
@ -607,10 +709,7 @@ namespace osu.Game.Screens.Select
// todo: remove the need for this.
foreach (var b in beatmapSet.Beatmaps)
{
if (b.Metadata == null)
b.Metadata = beatmapSet.Metadata;
}
b.Metadata ??= beatmapSet.Metadata;
var set = new CarouselBeatmapSet(beatmapSet)
{
@ -635,79 +734,62 @@ namespace osu.Game.Screens.Select
return set;
}
private const float panel_padding = 5;
/// <summary>
/// Computes the target Y positions for every item in the carousel.
/// </summary>
/// <returns>The Y position of the currently selected item.</returns>
private void updateItems()
private void updateYPositions()
{
Items = root.Drawables.ToList();
yPositions.Clear();
visibleItems.Clear();
float currentY = visibleHalfHeight;
DrawableCarouselBeatmapSet lastSet = null;
scrollTarget = null;
foreach (DrawableCarouselItem d in Items)
foreach (CarouselItem item in root.Children)
{
if (d.IsPresent)
if (item.Filtered.Value)
continue;
switch (item)
{
switch (d)
case CarouselBeatmapSet set:
{
case DrawableCarouselBeatmapSet set:
{
lastSet = set;
visibleItems.Add(set);
set.CarouselYPosition = currentY;
set.MoveToX(set.Item.State.Value == CarouselItemState.Selected ? -100 : 0, 500, Easing.OutExpo);
set.MoveToY(currentY, 750, Easing.OutExpo);
break;
if (item.State.Value == CarouselItemState.Selected)
{
// scroll position at currentY makes the set panel appear at the very top of the carousel's screen space
// move down by half of visible height (height of the carousel's visible extent, including semi-transparent areas)
// then reapply the top semi-transparent area (because carousel's screen space starts below it)
scrollTarget = currentY + DrawableCarouselBeatmapSet.HEIGHT - visibleHalfHeight + BleedTop;
foreach (var b in set.Beatmaps)
{
if (!b.Visible)
continue;
if (b.State.Value == CarouselItemState.Selected)
{
scrollTarget += b.TotalHeight / 2;
break;
}
scrollTarget += b.TotalHeight;
}
}
case DrawableCarouselBeatmap beatmap:
{
if (beatmap.Item.State.Value == CarouselItemState.Selected)
// scroll position at currentY makes the set panel appear at the very top of the carousel's screen space
// move down by half of visible height (height of the carousel's visible extent, including semi-transparent areas)
// then reapply the top semi-transparent area (because carousel's screen space starts below it)
// and finally add half of the panel's own height to achieve vertical centering of the panel itself
scrollTarget = currentY - visibleHalfHeight + BleedTop + beatmap.DrawHeight / 2;
void performMove(float y, float? startY = null)
{
if (startY != null) beatmap.MoveTo(new Vector2(0, startY.Value));
beatmap.MoveToX(beatmap.Item.State.Value == CarouselItemState.Selected ? -50 : 0, 500, Easing.OutExpo);
beatmap.MoveToY(y, 750, Easing.OutExpo);
}
Debug.Assert(lastSet != null);
float? setY = null;
if (!d.IsLoaded || beatmap.Alpha == 0) // can't use IsPresent due to DrawableCarouselItem override.
setY = lastSet.Y + lastSet.DrawHeight + 5;
if (d.IsLoaded)
performMove(currentY, setY);
else
{
float y = currentY;
d.OnLoadComplete += _ => performMove(y, setY);
}
break;
}
currentY += set.TotalHeight + panel_padding;
break;
}
}
yPositions.Add(currentY);
if (d.Item.Visible)
currentY += d.DrawHeight + 5;
}
currentY += visibleHalfHeight;
scrollableContent.Height = currentY;
ScrollableContent.Height = currentY;
if (BeatmapSetsLoaded && (selectedBeatmapSet == null || selectedBeatmap == null || selectedBeatmapSet.State.Value != CarouselItemState.Selected))
{
@ -758,21 +840,31 @@ namespace osu.Game.Screens.Select
/// Update a item's x position and multiplicative alpha based on its y position and
/// the current scroll position.
/// </summary>
/// <param name="p">The item to be updated.</param>
private void updateItem(DrawableCarouselItem p)
/// <param name="item">The item to be updated.</param>
/// <param name="parent">For nested items, the parent of the item to be updated.</param>
private void updateItem(DrawableCarouselItem item, DrawableCarouselItem parent = null)
{
float itemDrawY = p.Position.Y - visibleUpperBound + p.DrawHeight / 2;
Vector2 posInScroll = ScrollableContent.ToLocalSpace(item.Header.ScreenSpaceDrawQuad.Centre);
float itemDrawY = posInScroll.Y - visibleUpperBound;
float dist = Math.Abs(1f - itemDrawY / visibleHalfHeight);
// Setting the origin position serves as an additive position on top of potential
// 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, visibleHalfHeight), 0);
// adjusting the item's overall X position can cause it to become masked away when
// child items (difficulties) are still visible.
item.Header.X = offsetX(dist, visibleHalfHeight) - (parent?.X ?? 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(Math.Clamp(1.75f - 1.5f * dist, 0, 1));
// layer alpha transformations on top.
item.SetMultiplicativeAlpha(Math.Clamp(1.75f - 1.5f * dist, 0, 1));
}
/// <summary>
/// A carousel item strictly used for binary search purposes.
/// </summary>
private class CarouselBoundsItem : CarouselItem
{
public override DrawableCarouselItem CreateDrawableRepresentation() =>
throw new NotImplementedException();
}
private class CarouselRoot : CarouselGroupEagerSelect
@ -806,6 +898,7 @@ namespace osu.Game.Screens.Select
/// </summary>
public bool UserScrolling { get; private set; }
// ReSharper disable once OptionalParameterHierarchyMismatch 2020.3 EAP4 bug. (https://youtrack.jetbrains.com/issue/RSRP-481535?p=RIDER-51910)
protected override void OnUserScroll(float value, bool animated = true, double? distanceDecay = default)
{
UserScrolling = true;

View File

@ -30,6 +30,8 @@ namespace osu.Game.Screens.Select
protected Bindable<BeatmapDetailAreaTabItem> CurrentTab => tabControl.Current;
protected Bindable<bool> CurrentModsFilter => tabControl.CurrentModsFilter;
private readonly Container content;
protected override Container<Drawable> Content => content;

View File

@ -26,6 +26,12 @@ namespace osu.Game.Screens.Select
set => tabs.Current = value;
}
public Bindable<bool> CurrentModsFilter
{
get => modsCheckbox.Current;
set => modsCheckbox.Current = value;
}
public Action<BeatmapDetailAreaTabItem, bool> OnFilter; // passed the selected tab and if mods is checked
public IReadOnlyList<BeatmapDetailAreaTabItem> TabItems

View File

@ -236,7 +236,7 @@ namespace osu.Game.Screens.Select
private void updateMetrics()
{
var hasRatings = beatmap?.BeatmapSet?.Metrics?.Ratings?.Any() ?? false;
var hasRetriesFails = (beatmap?.Metrics?.Retries?.Any() ?? false) && (beatmap?.Metrics.Fails?.Any() ?? false);
var hasRetriesFails = (beatmap?.Metrics?.Retries?.Any() ?? false) || (beatmap?.Metrics?.Fails?.Any() ?? false);
if (hasRatings)
{
@ -300,6 +300,7 @@ namespace osu.Game.Screens.Select
public MetadataSection(string title)
{
Alpha = 0;
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;

View File

@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using JetBrains.Annotations;
using osuTK;
using osuTK.Graphics;
@ -27,6 +28,7 @@ using osu.Framework.Logging;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Ranking.Expanded;
namespace osu.Game.Screens.Select
{
@ -38,6 +40,11 @@ namespace osu.Game.Screens.Select
private readonly IBindable<RulesetInfo> ruleset = new Bindable<RulesetInfo>();
[Resolved]
private BeatmapDifficultyCache difficultyCache { get; set; }
private IBindable<StarDifficulty> beatmapDifficulty;
protected BufferedWedgeInfo Info;
public BeatmapInfoWedge()
@ -79,6 +86,8 @@ namespace osu.Game.Screens.Select
private WorkingBeatmap beatmap;
private CancellationTokenSource cancellationSource;
public WorkingBeatmap Beatmap
{
get => beatmap;
@ -87,6 +96,13 @@ namespace osu.Game.Screens.Select
if (beatmap == value) return;
beatmap = value;
cancellationSource?.Cancel();
cancellationSource = new CancellationTokenSource();
beatmapDifficulty?.UnbindAll();
beatmapDifficulty = difficultyCache.GetBindableDifficulty(beatmap.BeatmapInfo, cancellationSource.Token);
beatmapDifficulty.BindValueChanged(_ => updateDisplay());
updateDisplay();
}
}
@ -97,33 +113,44 @@ namespace osu.Game.Screens.Select
private void updateDisplay()
{
void removeOldInfo()
{
State.Value = beatmap == null ? Visibility.Hidden : Visibility.Visible;
Scheduler.AddOnce(perform);
Info?.FadeOut(250);
Info?.Expire();
Info = null;
void perform()
{
void removeOldInfo()
{
State.Value = beatmap == null ? Visibility.Hidden : Visibility.Visible;
Info?.FadeOut(250);
Info?.Expire();
Info = null;
}
if (beatmap == null)
{
removeOldInfo();
return;
}
LoadComponentAsync(loadingInfo = new BufferedWedgeInfo(beatmap, ruleset.Value, beatmapDifficulty.Value)
{
Shear = -Shear,
Depth = Info?.Depth + 1 ?? 0
}, loaded =>
{
// ensure we are the most recent loaded wedge.
if (loaded != loadingInfo) return;
removeOldInfo();
Add(Info = loaded);
});
}
}
if (beatmap == null)
{
removeOldInfo();
return;
}
LoadComponentAsync(loadingInfo = new BufferedWedgeInfo(beatmap, ruleset.Value)
{
Shear = -Shear,
Depth = Info?.Depth + 1 ?? 0
}, loaded =>
{
// ensure we are the most recent loaded wedge.
if (loaded != loadingInfo) return;
removeOldInfo();
Add(Info = loaded);
});
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
cancellationSource?.Cancel();
}
public class BufferedWedgeInfo : BufferedContainer
@ -140,12 +167,14 @@ namespace osu.Game.Screens.Select
private readonly WorkingBeatmap beatmap;
private readonly RulesetInfo ruleset;
private readonly StarDifficulty starDifficulty;
public BufferedWedgeInfo(WorkingBeatmap beatmap, RulesetInfo userRuleset)
public BufferedWedgeInfo(WorkingBeatmap beatmap, RulesetInfo userRuleset, StarDifficulty difficulty)
: base(pixelSnapping: true)
{
this.beatmap = beatmap;
ruleset = userRuleset ?? beatmap.BeatmapInfo.Ruleset;
starDifficulty = difficulty;
}
[BackgroundDependencyLoader]
@ -155,7 +184,6 @@ namespace osu.Game.Screens.Select
var metadata = beatmapInfo.Metadata ?? beatmap.BeatmapSetInfo?.Metadata ?? new BeatmapMetadata();
CacheDrawnFrameBuffer = true;
RedrawOnScale = false;
RelativeSizeAxes = Axes.Both;
@ -190,7 +218,7 @@ namespace osu.Game.Screens.Select
},
},
},
new DifficultyColourBar(beatmapInfo)
new DifficultyColourBar(starDifficulty)
{
RelativeSizeAxes = Axes.Y,
Width = 20,
@ -223,10 +251,20 @@ namespace osu.Game.Screens.Select
Direction = FillDirection.Vertical,
Padding = new MarginPadding { Top = 14, Right = shear_width / 2 },
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
Shear = wedged_container_shear,
Children = new[]
{
createStarRatingDisplay(starDifficulty).With(display =>
{
display.Anchor = Anchor.TopRight;
display.Origin = Anchor.TopRight;
display.Shear = -wedged_container_shear;
}),
StatusPill = new BeatmapSetOnlineStatusPill
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Shear = -wedged_container_shear,
TextSize = 11,
TextPadding = new MarginPadding { Horizontal = 8, Vertical = 2 },
Status = beatmapInfo.Status,
@ -283,6 +321,13 @@ namespace osu.Game.Screens.Select
StatusPill.Hide();
}
private static Drawable createStarRatingDisplay(StarDifficulty difficulty) => difficulty.Stars > 0
? new StarRatingDisplay(difficulty)
{
Margin = new MarginPadding { Bottom = 5 }
}
: Empty();
private void setMetadata(string source)
{
ArtistLabel.Text = artistBinding.Value;
@ -301,14 +346,14 @@ namespace osu.Game.Screens.Select
labels.Add(new InfoLabel(new BeatmapStatistic
{
Name = "Length",
Icon = FontAwesome.Regular.Clock,
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Length),
Content = TimeSpan.FromMilliseconds(b.BeatmapInfo.Length).ToString(@"m\:ss"),
}));
labels.Add(new InfoLabel(new BeatmapStatistic
{
Name = "BPM",
Icon = FontAwesome.Regular.Circle,
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Bpm),
Content = getBPMRange(b),
}));
@ -401,10 +446,18 @@ namespace osu.Game.Screens.Select
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Scale = new Vector2(0.8f),
Colour = Color4Extensions.FromHex(@"f7dd55"),
Icon = statistic.Icon,
Icon = FontAwesome.Regular.Circle,
Size = new Vector2(0.8f)
},
statistic.CreateIcon().With(i =>
{
i.Anchor = Anchor.Centre;
i.Origin = Anchor.Centre;
i.RelativeSizeAxes = Axes.Both;
i.Colour = Color4Extensions.FromHex(@"f7dd55");
i.Size = new Vector2(0.64f);
}),
}
},
new OsuSpriteText
@ -422,11 +475,11 @@ namespace osu.Game.Screens.Select
private class DifficultyColourBar : Container
{
private readonly BeatmapInfo beatmap;
private readonly StarDifficulty difficulty;
public DifficultyColourBar(BeatmapInfo beatmap)
public DifficultyColourBar(StarDifficulty difficulty)
{
this.beatmap = beatmap;
this.difficulty = difficulty;
}
[BackgroundDependencyLoader]
@ -434,7 +487,7 @@ namespace osu.Game.Screens.Select
{
const float full_opacity_ratio = 0.7f;
var difficultyColour = colours.ForDifficultyRating(beatmap.DifficultyRating);
var difficultyColour = colours.ForDifficultyRating(difficulty.DifficultyRating);
Children = new Drawable[]
{

View File

@ -10,6 +10,8 @@ namespace osu.Game.Screens.Select.Carousel
{
public class CarouselBeatmap : CarouselItem
{
public override float TotalHeight => DrawableCarouselBeatmap.HEIGHT;
public readonly BeatmapInfo Beatmap;
public CarouselBeatmap(BeatmapInfo beatmap)
@ -18,7 +20,7 @@ namespace osu.Game.Screens.Select.Carousel
State.Value = CarouselItemState.Collapsed;
}
protected override DrawableCarouselItem CreateDrawableRepresentation() => new DrawableCarouselBeatmap(this);
public override DrawableCarouselItem CreateDrawableRepresentation() => new DrawableCarouselBeatmap(this);
public override void Filter(FilterCriteria criteria)
{
@ -57,9 +59,20 @@ namespace osu.Game.Screens.Select.Carousel
var terms = Beatmap.SearchableTerms;
foreach (var criteriaTerm in criteria.SearchTerms)
match &= terms.Any(term => term.IndexOf(criteriaTerm, StringComparison.InvariantCultureIgnoreCase) >= 0);
match &= terms.Any(term => term.Contains(criteriaTerm, StringComparison.InvariantCultureIgnoreCase));
// if a match wasn't found via text matching of terms, do a second catch-all check matching against online IDs.
// this should be done after text matching so we can prioritise matching numbers in metadata.
if (!match && criteria.SearchNumber.HasValue)
{
match = (Beatmap.OnlineBeatmapID == criteria.SearchNumber.Value) ||
(Beatmap.BeatmapSet?.OnlineBeatmapSetID == criteria.SearchNumber.Value);
}
}
if (match)
match &= criteria.Collection?.Beatmaps.Contains(Beatmap) ?? true;
Filtered.Value = !match;
}

View File

@ -12,6 +12,21 @@ namespace osu.Game.Screens.Select.Carousel
{
public class CarouselBeatmapSet : CarouselGroupEagerSelect
{
public override float TotalHeight
{
get
{
switch (State.Value)
{
case CarouselItemState.Selected:
return DrawableCarouselBeatmapSet.HEIGHT + Children.Count(c => c.Visible) * DrawableCarouselBeatmap.HEIGHT;
default:
return DrawableCarouselBeatmapSet.HEIGHT;
}
}
}
public IEnumerable<CarouselBeatmap> Beatmaps => InternalChildren.OfType<CarouselBeatmap>();
public BeatmapSetInfo BeatmapSet;
@ -28,8 +43,6 @@ namespace osu.Game.Screens.Select.Carousel
.ForEach(AddChild);
}
protected override DrawableCarouselItem CreateDrawableRepresentation() => new DrawableCarouselBeatmapSet(this);
protected override CarouselItem GetNextToSelect()
{
if (LastSelected == null)

View File

@ -11,7 +11,7 @@ namespace osu.Game.Screens.Select.Carousel
/// </summary>
public class CarouselGroup : CarouselItem
{
protected override DrawableCarouselItem CreateDrawableRepresentation() => null;
public override DrawableCarouselItem CreateDrawableRepresentation() => null;
public IReadOnlyList<CarouselItem> Children => InternalChildren;
@ -23,22 +23,6 @@ namespace osu.Game.Screens.Select.Carousel
/// </summary>
private ulong currentChildID;
public override List<DrawableCarouselItem> Drawables
{
get
{
var drawables = base.Drawables;
// if we are explicitly not present, don't ever present children.
// without this check, children drawables can potentially be presented without their group header.
if (DrawableRepresentation.Value?.IsPresent == false) return drawables;
foreach (var c in InternalChildren)
drawables.AddRange(c.Drawables);
return drawables;
}
}
public virtual void RemoveChild(CarouselItem i)
{
InternalChildren.Remove(i);

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
namespace osu.Game.Screens.Select.Carousel
@ -54,6 +55,14 @@ namespace osu.Game.Screens.Select.Carousel
updateSelectedIndex();
}
public void AddChildren(IEnumerable<CarouselItem> items)
{
foreach (var i in items)
base.AddChild(i);
attemptSelection();
}
public override void AddChild(CarouselItem i)
{
base.AddChild(i);

View File

@ -0,0 +1,114 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Graphics;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Select.Carousel
{
public class CarouselHeader : Container
{
private SampleChannel sampleHover;
private readonly Box hoverLayer;
public Container BorderContainer;
public readonly Bindable<CarouselItemState> State = new Bindable<CarouselItemState>(CarouselItemState.NotSelected);
protected override Container<Drawable> Content { get; } = new Container { RelativeSizeAxes = Axes.Both };
public CarouselHeader()
{
RelativeSizeAxes = Axes.X;
Height = DrawableCarouselItem.MAX_HEIGHT;
InternalChild = BorderContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Masking = true,
CornerRadius = 10,
BorderColour = new Color4(221, 255, 255, 255),
Children = new Drawable[]
{
Content,
hoverLayer = new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
Blending = BlendingParameters.Additive,
},
}
};
}
[BackgroundDependencyLoader]
private void load(AudioManager audio, OsuColour colours)
{
sampleHover = audio.Samples.Get($@"SongSelect/song-ping-variation-{RNG.Next(1, 5)}");
hoverLayer.Colour = colours.Blue.Opacity(0.1f);
}
protected override void LoadComplete()
{
base.LoadComplete();
State.BindValueChanged(updateState, true);
}
private void updateState(ValueChangedEvent<CarouselItemState> state)
{
switch (state.NewValue)
{
case CarouselItemState.Collapsed:
case CarouselItemState.NotSelected:
BorderContainer.BorderThickness = 0;
BorderContainer.EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Shadow,
Offset = new Vector2(1),
Radius = 10,
Colour = Color4.Black.Opacity(100),
};
break;
case CarouselItemState.Selected:
BorderContainer.BorderThickness = 2.5f;
BorderContainer.EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
Colour = new Color4(130, 204, 255, 150),
Radius = 20,
Roundness = 10,
};
break;
}
}
protected override bool OnHover(HoverEvent e)
{
sampleHover?.Play();
hoverLayer.FadeIn(100, Easing.OutQuint);
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
hoverLayer.FadeOut(1000, Easing.OutQuint);
base.OnHoverLost(e);
}
}
}

View File

@ -2,13 +2,19 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using osu.Framework.Bindables;
namespace osu.Game.Screens.Select.Carousel
{
public abstract class CarouselItem
public abstract class CarouselItem : IComparable<CarouselItem>
{
public virtual float TotalHeight => 0;
/// <summary>
/// An externally defined value used to determine this item's vertical display offset relative to the carousel.
/// </summary>
public float CarouselYPosition;
public readonly BindableBool Filtered = new BindableBool();
public readonly Bindable<CarouselItemState> State = new Bindable<CarouselItemState>(CarouselItemState.NotSelected);
@ -18,23 +24,8 @@ namespace osu.Game.Screens.Select.Carousel
/// </summary>
public bool Visible => State.Value != CarouselItemState.Collapsed && !Filtered.Value;
public virtual List<DrawableCarouselItem> Drawables
{
get
{
var items = new List<DrawableCarouselItem>();
var self = DrawableRepresentation.Value;
if (self?.IsPresent == true) items.Add(self);
return items;
}
}
protected CarouselItem()
{
DrawableRepresentation = new Lazy<DrawableCarouselItem>(CreateDrawableRepresentation);
Filtered.ValueChanged += filtered =>
{
if (filtered.NewValue && State.Value == CarouselItemState.Selected)
@ -42,23 +33,23 @@ namespace osu.Game.Screens.Select.Carousel
};
}
protected readonly Lazy<DrawableCarouselItem> DrawableRepresentation;
/// <summary>
/// Used as a default sort method for <see cref="CarouselItem"/>s of differing types.
/// </summary>
internal ulong ChildID;
/// <summary>
/// Create a fresh drawable version of this item. If you wish to consume the current representation, use <see cref="DrawableRepresentation"/> instead.
/// Create a fresh drawable version of this item.
/// </summary>
protected abstract DrawableCarouselItem CreateDrawableRepresentation();
public abstract DrawableCarouselItem CreateDrawableRepresentation();
public virtual void Filter(FilterCriteria criteria)
{
}
public virtual int CompareTo(FilterCriteria criteria, CarouselItem other) => ChildID.CompareTo(other.ChildID);
public int CompareTo(CarouselItem other) => CarouselYPosition.CompareTo(other.CarouselYPosition);
}
public enum CarouselItemState

View File

@ -3,7 +3,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
@ -15,6 +18,7 @@ using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Collections;
using osu.Game.Graphics;
using osu.Game.Graphics.Backgrounds;
using osu.Game.Graphics.Sprites;
@ -27,6 +31,15 @@ namespace osu.Game.Screens.Select.Carousel
{
public class DrawableCarouselBeatmap : DrawableCarouselItem, IHasContextMenu
{
public const float CAROUSEL_BEATMAP_SPACING = 5;
/// <summary>
/// The height of a carousel beatmap, including vertical spacing.
/// </summary>
public const float HEIGHT = height + CAROUSEL_BEATMAP_SPACING;
private const float height = MAX_HEIGHT * 0.6f;
private readonly BeatmapInfo beatmap;
private Sprite background;
@ -41,16 +54,29 @@ namespace osu.Game.Screens.Select.Carousel
[Resolved(CanBeNull = true)]
private BeatmapSetOverlay beatmapOverlay { get; set; }
[Resolved]
private BeatmapDifficultyCache difficultyCache { get; set; }
[Resolved(CanBeNull = true)]
private CollectionManager collectionManager { get; set; }
[Resolved(CanBeNull = true)]
private ManageCollectionsDialog manageCollectionsDialog { get; set; }
private IBindable<StarDifficulty> starDifficultyBindable;
private CancellationTokenSource starDifficultyCancellationSource;
public DrawableCarouselBeatmap(CarouselBeatmap panel)
: base(panel)
{
beatmap = panel.Beatmap;
Height *= 0.60f;
Item = panel;
}
[BackgroundDependencyLoader(true)]
private void load(BeatmapManager manager, SongSelect songSelect)
{
Header.Height = height;
if (songSelect != null)
{
startRequested = b => songSelect.FinaliseSelection(b);
@ -61,7 +87,7 @@ namespace osu.Game.Screens.Select.Carousel
if (manager != null)
hideRequested = manager.Hide;
Children = new Drawable[]
Header.Children = new Drawable[]
{
background = new Box
{
@ -137,7 +163,6 @@ namespace osu.Game.Screens.Select.Carousel
},
starCounter = new StarCounter
{
Current = (float)beatmap.StarDifficulty,
Scale = new Vector2(0.8f),
}
}
@ -153,6 +178,8 @@ namespace osu.Game.Screens.Select.Carousel
{
base.Selected();
MovementContainer.MoveToX(-50, 500, Easing.OutExpo);
background.Colour = ColourInfo.GradientVertical(
new Color4(20, 43, 51, 255),
new Color4(40, 86, 102, 255));
@ -164,6 +191,8 @@ namespace osu.Game.Screens.Select.Carousel
{
base.Deselected();
MovementContainer.MoveToX(0, 500, Easing.OutExpo);
background.Colour = new Color4(20, 43, 51, 255);
triangles.Colour = OsuColour.Gray(0.5f);
}
@ -181,6 +210,16 @@ namespace osu.Game.Screens.Select.Carousel
if (Item.State.Value != CarouselItemState.Collapsed && Alpha == 0)
starCounter.ReplayAnimation();
starDifficultyCancellationSource?.Cancel();
// Only compute difficulty when the item is visible.
if (Item.State.Value != CarouselItemState.Collapsed)
{
// We've potentially cancelled the computation above so a new bindable is required.
starDifficultyBindable = difficultyCache.GetBindableDifficulty(beatmap, (starDifficultyCancellationSource = new CancellationTokenSource()).Token);
starDifficultyBindable.BindValueChanged(d => starCounter.Current = (float)d.NewValue.Stars, true);
}
base.ApplyState();
}
@ -196,14 +235,43 @@ namespace osu.Game.Screens.Select.Carousel
if (editRequested != null)
items.Add(new OsuMenuItem("Edit", MenuItemType.Standard, () => editRequested(beatmap)));
if (beatmap.OnlineBeatmapID.HasValue && beatmapOverlay != null)
items.Add(new OsuMenuItem("Details...", MenuItemType.Standard, () => beatmapOverlay.FetchAndShowBeatmap(beatmap.OnlineBeatmapID.Value)));
if (collectionManager != null)
{
var collectionItems = collectionManager.Collections.Select(createCollectionMenuItem).ToList();
if (manageCollectionsDialog != null)
collectionItems.Add(new OsuMenuItem("Manage...", MenuItemType.Standard, manageCollectionsDialog.Show));
items.Add(new OsuMenuItem("Collections") { Items = collectionItems });
}
if (hideRequested != null)
items.Add(new OsuMenuItem("Hide", MenuItemType.Destructive, () => hideRequested(beatmap)));
if (beatmap.OnlineBeatmapID.HasValue && beatmapOverlay != null)
items.Add(new OsuMenuItem("Details", MenuItemType.Standard, () => beatmapOverlay.FetchAndShowBeatmap(beatmap.OnlineBeatmapID.Value)));
return items.ToArray();
}
}
private MenuItem createCollectionMenuItem(BeatmapCollection collection)
{
return new ToggleMenuItem(collection.Name.Value, MenuItemType.Standard, s =>
{
if (s)
collection.Beatmaps.Add(beatmap);
else
collection.Beatmaps.Remove(beatmap);
})
{
State = { Value = collection.Beatmaps.Contains(beatmap) }
};
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
starDifficultyCancellationSource?.Cancel();
}
}
}

View File

@ -3,130 +3,169 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
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.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Collections;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osu.Game.Rulesets;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Select.Carousel
{
public class DrawableCarouselBeatmapSet : DrawableCarouselItem, IHasContextMenu
{
public const float HEIGHT = MAX_HEIGHT;
private Action<BeatmapSetInfo> restoreHiddenRequested;
private Action<int> viewDetails;
[Resolved(CanBeNull = true)]
private DialogOverlay dialogOverlay { get; set; }
private readonly BeatmapSetInfo beatmapSet;
[Resolved(CanBeNull = true)]
private CollectionManager collectionManager { get; set; }
public DrawableCarouselBeatmapSet(CarouselBeatmapSet set)
: base(set)
[Resolved(CanBeNull = true)]
private ManageCollectionsDialog manageCollectionsDialog { get; set; }
public IEnumerable<DrawableCarouselItem> DrawableBeatmaps => beatmapContainer?.Children ?? Enumerable.Empty<DrawableCarouselItem>();
private Container<DrawableCarouselItem> beatmapContainer;
private BeatmapSetInfo beatmapSet;
[Resolved]
private BeatmapManager manager { get; set; }
protected override void FreeAfterUse()
{
beatmapSet = set.BeatmapSet;
base.FreeAfterUse();
Item = null;
ClearTransforms();
}
[BackgroundDependencyLoader(true)]
private void load(BeatmapManager manager, BeatmapSetOverlay beatmapOverlay)
private void load(BeatmapSetOverlay beatmapOverlay)
{
restoreHiddenRequested = s => s.Beatmaps.ForEach(manager.Restore);
if (beatmapOverlay != null)
viewDetails = beatmapOverlay.FetchAndShowBeatmapSet;
Children = new Drawable[]
{
new DelayedLoadUnloadWrapper(() =>
{
var background = new PanelBackground(manager.GetWorkingBeatmap(beatmapSet.Beatmaps.FirstOrDefault()))
{
RelativeSizeAxes = Axes.Both,
};
background.OnLoadComplete += d => d.FadeInFromZero(1000, Easing.OutQuint);
return background;
}, 300, 5000
),
new FillFlowContainer
{
Direction = FillDirection.Vertical,
Padding = new MarginPadding { Top = 5, Left = 18, Right = 10, Bottom = 10 },
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
{
new OsuSpriteText
{
Text = new LocalisedString((beatmapSet.Metadata.TitleUnicode, beatmapSet.Metadata.Title)),
Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 22, italics: true),
Shadow = true,
},
new OsuSpriteText
{
Text = new LocalisedString((beatmapSet.Metadata.ArtistUnicode, beatmapSet.Metadata.Artist)),
Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 17, italics: true),
Shadow = true,
},
new FillFlowContainer
{
Direction = FillDirection.Horizontal,
AutoSizeAxes = Axes.Both,
Margin = new MarginPadding { Top = 5 },
Children = new Drawable[]
{
new BeatmapSetOnlineStatusPill
{
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
Margin = new MarginPadding { Right = 5 },
TextSize = 11,
TextPadding = new MarginPadding { Horizontal = 8, Vertical = 2 },
Status = beatmapSet.Status
},
new FillFlowContainer<DifficultyIcon>
{
AutoSizeAxes = Axes.Both,
Spacing = new Vector2(3),
ChildrenEnumerable = getDifficultyIcons(),
},
}
}
}
}
};
}
private const int maximum_difficulty_icons = 18;
private IEnumerable<DifficultyIcon> getDifficultyIcons()
protected override void UpdateItem()
{
var beatmaps = ((CarouselBeatmapSet)Item).Beatmaps.ToList();
base.UpdateItem();
return beatmaps.Count > maximum_difficulty_icons
? (IEnumerable<DifficultyIcon>)beatmaps.GroupBy(b => b.Beatmap.Ruleset).Select(group => new FilterableGroupedDifficultyIcon(group.ToList(), group.Key))
: beatmaps.Select(b => new FilterableDifficultyIcon(b));
Content.Clear();
beatmapContainer = null;
if (Item == null)
return;
beatmapSet = ((CarouselBeatmapSet)Item).BeatmapSet;
DelayedLoadWrapper background;
DelayedLoadWrapper mainFlow;
Header.Children = new Drawable[]
{
background = new DelayedLoadWrapper(() => new SetPanelBackground(manager.GetWorkingBeatmap(beatmapSet.Beatmaps.FirstOrDefault()))
{
RelativeSizeAxes = Axes.Both,
}, 300),
mainFlow = new DelayedLoadWrapper(() => new SetPanelContent((CarouselBeatmapSet)Item), 100),
};
background.DelayedLoadComplete += fadeContentIn;
mainFlow.DelayedLoadComplete += fadeContentIn;
}
private void fadeContentIn(Drawable d) => d.FadeInFromZero(750, Easing.OutQuint);
protected override void Deselected()
{
base.Deselected();
MovementContainer.MoveToX(0, 500, Easing.OutExpo);
if (beatmapContainer != null)
{
foreach (var beatmap in beatmapContainer)
beatmap.MoveToY(0, 800, Easing.OutQuint);
}
}
protected override void Selected()
{
base.Selected();
MovementContainer.MoveToX(-100, 500, Easing.OutExpo);
updateBeatmapDifficulties();
}
private void updateBeatmapDifficulties()
{
var carouselBeatmapSet = (CarouselBeatmapSet)Item;
var visibleBeatmaps = carouselBeatmapSet.Children.Where(c => c.Visible).ToArray();
// if we are already displaying all the correct beatmaps, only run animation updates.
// note that the displayed beatmaps may change due to the applied filter.
// a future optimisation could add/remove only changed difficulties rather than reinitialise.
if (beatmapContainer != null && visibleBeatmaps.Length == beatmapContainer.Count && visibleBeatmaps.All(b => beatmapContainer.Any(c => c.Item == b)))
{
updateBeatmapYPositions();
}
else
{
// on selection we show our child beatmaps.
// for now this is a simple drawable construction each selection.
// can be improved in the future.
beatmapContainer = new Container<DrawableCarouselItem>
{
X = 100,
RelativeSizeAxes = Axes.Both,
ChildrenEnumerable = visibleBeatmaps.Select(c => c.CreateDrawableRepresentation())
};
LoadComponentAsync(beatmapContainer, loaded =>
{
// make sure the pooled target hasn't changed.
if (beatmapContainer != loaded)
return;
Content.Child = loaded;
updateBeatmapYPositions();
});
}
void updateBeatmapYPositions()
{
float yPos = DrawableCarouselBeatmap.CAROUSEL_BEATMAP_SPACING;
foreach (var panel in beatmapContainer.Children)
{
panel.MoveToY(yPos, 800, Easing.OutQuint);
yPos += panel.Item.TotalHeight;
}
}
}
public MenuItem[] ContextMenuItems
{
get
{
Debug.Assert(beatmapSet != null);
List<MenuItem> items = new List<MenuItem>();
if (Item.State.Value == CarouselItemState.NotSelected)
@ -135,125 +174,61 @@ namespace osu.Game.Screens.Select.Carousel
if (beatmapSet.OnlineBeatmapSetID != null && viewDetails != null)
items.Add(new OsuMenuItem("Details...", MenuItemType.Standard, () => viewDetails(beatmapSet.OnlineBeatmapSetID.Value)));
if (collectionManager != null)
{
var collectionItems = collectionManager.Collections.Select(createCollectionMenuItem).ToList();
if (manageCollectionsDialog != null)
collectionItems.Add(new OsuMenuItem("Manage...", MenuItemType.Standard, manageCollectionsDialog.Show));
items.Add(new OsuMenuItem("Collections") { Items = collectionItems });
}
if (beatmapSet.Beatmaps.Any(b => b.Hidden))
items.Add(new OsuMenuItem("Restore all hidden", MenuItemType.Standard, () => restoreHiddenRequested(beatmapSet)));
if (dialogOverlay != null)
items.Add(new OsuMenuItem("Delete", MenuItemType.Destructive, () => dialogOverlay.Push(new BeatmapDeleteDialog(beatmapSet))));
items.Add(new OsuMenuItem("Delete...", MenuItemType.Destructive, () => dialogOverlay.Push(new BeatmapDeleteDialog(beatmapSet))));
return items.ToArray();
}
}
private class PanelBackground : BufferedContainer
private MenuItem createCollectionMenuItem(BeatmapCollection collection)
{
public PanelBackground(WorkingBeatmap working)
{
CacheDrawnFrameBuffer = true;
RedrawOnScale = false;
Debug.Assert(beatmapSet != null);
Children = new Drawable[]
TernaryState state;
var countExisting = beatmapSet.Beatmaps.Count(b => collection.Beatmaps.Contains(b));
if (countExisting == beatmapSet.Beatmaps.Count)
state = TernaryState.True;
else if (countExisting > 0)
state = TernaryState.Indeterminate;
else
state = TernaryState.False;
return new TernaryStateMenuItem(collection.Name.Value, MenuItemType.Standard, s =>
{
foreach (var b in beatmapSet.Beatmaps)
{
new BeatmapBackgroundSprite(working)
switch (s)
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
FillMode = FillMode.Fill,
},
new FillFlowContainer
{
Depth = -1,
RelativeSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
// This makes the gradient not be perfectly horizontal, but diagonal at a ~40° angle
Shear = new Vector2(0.8f, 0),
Alpha = 0.5f,
Children = new[]
{
// The left half with no gradient applied
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black,
Width = 0.4f,
},
// Piecewise-linear gradient with 3 segments to make it appear smoother
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = ColourInfo.GradientHorizontal(Color4.Black, new Color4(0f, 0f, 0f, 0.9f)),
Width = 0.05f,
},
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = ColourInfo.GradientHorizontal(new Color4(0f, 0f, 0f, 0.9f), new Color4(0f, 0f, 0f, 0.1f)),
Width = 0.2f,
},
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = ColourInfo.GradientHorizontal(new Color4(0f, 0f, 0f, 0.1f), new Color4(0, 0, 0, 0)),
Width = 0.05f,
},
}
},
};
}
}
case TernaryState.True:
if (collection.Beatmaps.Contains(b))
continue;
public class FilterableDifficultyIcon : DifficultyIcon
{
private readonly BindableBool filtered = new BindableBool();
collection.Beatmaps.Add(b);
break;
public bool IsFiltered => filtered.Value;
public readonly CarouselBeatmap Item;
public FilterableDifficultyIcon(CarouselBeatmap item)
: base(item.Beatmap)
case TernaryState.False:
collection.Beatmaps.Remove(b);
break;
}
}
})
{
filtered.BindTo(item.Filtered);
filtered.ValueChanged += isFiltered => Schedule(() => this.FadeTo(isFiltered.NewValue ? 0.1f : 1, 100));
filtered.TriggerChange();
Item = item;
}
protected override bool OnClick(ClickEvent e)
{
Item.State.Value = CarouselItemState.Selected;
return true;
}
}
public class FilterableGroupedDifficultyIcon : GroupedDifficultyIcon
{
public readonly List<CarouselBeatmap> Items;
public FilterableGroupedDifficultyIcon(List<CarouselBeatmap> items, RulesetInfo ruleset)
: base(items.Select(i => i.Beatmap).ToList(), ruleset, Color4.White)
{
Items = items;
foreach (var item in items)
item.Filtered.BindValueChanged(_ => Scheduler.AddOnce(updateFilteredDisplay));
updateFilteredDisplay();
}
protected override bool OnClick(ClickEvent e)
{
Items.First().State.Value = CarouselItemState.Selected;
return true;
}
private void updateFilteredDisplay()
{
// for now, fade the whole group based on the ratio of hidden items.
this.FadeTo(1 - 0.9f * ((float)Items.Count(i => i.Filtered.Value) / Items.Count), 100);
}
State = { Value = state }
};
}
}
}

View File

@ -1,106 +1,133 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Extensions.Color4Extensions;
using System.Diagnostics;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Pooling;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Graphics;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Select.Carousel
{
public abstract class DrawableCarouselItem : Container
public abstract class DrawableCarouselItem : PoolableDrawable
{
public const float MAX_HEIGHT = 80;
public override bool RemoveWhenNotAlive => false;
public override bool IsPresent => base.IsPresent || Item?.Visible == true;
public override bool IsPresent => base.IsPresent || Item.Visible;
public readonly CarouselHeader Header;
public readonly CarouselItem Item;
/// <summary>
/// Optional content which sits below the header.
/// </summary>
protected readonly Container<Drawable> Content;
private Container nestedContainer;
private Container borderContainer;
protected readonly Container MovementContainer;
private Box hoverLayer;
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
Header.ReceivePositionalInputAt(screenSpacePos);
protected override Container<Drawable> Content => nestedContainer;
private CarouselItem item;
protected DrawableCarouselItem(CarouselItem item)
public CarouselItem Item
{
Item = item;
Height = MAX_HEIGHT;
RelativeSizeAxes = Axes.X;
Alpha = 0;
}
private SampleChannel sampleHover;
[BackgroundDependencyLoader]
private void load(AudioManager audio, OsuColour colours)
{
InternalChild = borderContainer = new Container
get => item;
set
{
RelativeSizeAxes = Axes.Both,
Masking = true,
CornerRadius = 10,
BorderColour = new Color4(221, 255, 255, 255),
Children = new Drawable[]
if (item == value)
return;
if (item != null)
{
nestedContainer = new Container
item.Filtered.ValueChanged -= onStateChange;
item.State.ValueChanged -= onStateChange;
Header.State.UnbindFrom(item.State);
if (item is CarouselGroup group)
{
RelativeSizeAxes = Axes.Both,
},
hoverLayer = new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
Blending = BlendingParameters.Additive,
},
foreach (var c in group.Children)
c.Filtered.ValueChanged -= onStateChange;
}
}
item = value;
if (IsLoaded)
UpdateItem();
}
}
protected DrawableCarouselItem()
{
RelativeSizeAxes = Axes.X;
Alpha = 0;
InternalChildren = new Drawable[]
{
MovementContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
Header = new CarouselHeader(),
Content = new Container
{
RelativeSizeAxes = Axes.Both,
}
}
},
};
sampleHover = audio.Samples.Get($@"SongSelect/song-ping-variation-{RNG.Next(1, 5)}");
hoverLayer.Colour = colours.Blue.Opacity(0.1f);
}
protected override bool OnHover(HoverEvent e)
{
sampleHover?.Play();
hoverLayer.FadeIn(100, Easing.OutQuint);
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
hoverLayer.FadeOut(1000, Easing.OutQuint);
base.OnHoverLost(e);
}
public void SetMultiplicativeAlpha(float alpha) => borderContainer.Alpha = alpha;
public void SetMultiplicativeAlpha(float alpha) => Header.BorderContainer.Alpha = alpha;
protected override void LoadComplete()
{
base.LoadComplete();
ApplyState();
Item.Filtered.ValueChanged += _ => Schedule(ApplyState);
Item.State.ValueChanged += _ => Schedule(ApplyState);
UpdateItem();
}
protected override void Update()
{
base.Update();
Content.Y = Header.Height;
}
protected virtual void UpdateItem()
{
if (item == null)
return;
Scheduler.AddOnce(ApplyState);
Item.Filtered.ValueChanged += onStateChange;
Item.State.ValueChanged += onStateChange;
Header.State.BindTo(Item.State);
if (Item is CarouselGroup group)
{
foreach (var c in group.Children)
c.Filtered.ValueChanged += onStateChange;
}
}
private void onStateChange(ValueChangedEvent<CarouselItemState> obj) => Scheduler.AddOnce(ApplyState);
private void onStateChange(ValueChangedEvent<bool> _) => Scheduler.AddOnce(ApplyState);
protected virtual void ApplyState()
{
if (!IsLoaded) return;
// Use the fact that we know the precise height of the item from the model to avoid the need for AutoSize overhead.
// Additionally, AutoSize doesn't work well due to content starting off-screen and being masked away.
Height = Item.TotalHeight;
Debug.Assert(Item != null);
switch (Item.State.Value)
{
@ -121,30 +148,11 @@ namespace osu.Game.Screens.Select.Carousel
protected virtual void Selected()
{
Item.State.Value = CarouselItemState.Selected;
borderContainer.BorderThickness = 2.5f;
borderContainer.EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
Colour = new Color4(130, 204, 255, 150),
Radius = 20,
Roundness = 10,
};
Debug.Assert(Item != null);
}
protected virtual void Deselected()
{
Item.State.Value = CarouselItemState.NotSelected;
borderContainer.BorderThickness = 0;
borderContainer.EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Shadow,
Offset = new Vector2(1),
Radius = 10,
Colour = Color4.Black.Opacity(100),
};
}
protected override bool OnClick(ClickEvent e)

View File

@ -0,0 +1,35 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps.Drawables;
namespace osu.Game.Screens.Select.Carousel
{
public class FilterableDifficultyIcon : DifficultyIcon
{
private readonly BindableBool filtered = new BindableBool();
public bool IsFiltered => filtered.Value;
public readonly CarouselBeatmap Item;
public FilterableDifficultyIcon(CarouselBeatmap item)
: base(item.Beatmap, performBackgroundDifficultyLookup: false)
{
filtered.BindTo(item.Filtered);
filtered.ValueChanged += isFiltered => Schedule(() => this.FadeTo(isFiltered.NewValue ? 0.1f : 1, 100));
filtered.TriggerChange();
Item = item;
}
protected override bool OnClick(ClickEvent e)
{
Item.State.Value = CarouselItemState.Selected;
return true;
}
}
}

View File

@ -0,0 +1,41 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Rulesets;
using osuTK.Graphics;
namespace osu.Game.Screens.Select.Carousel
{
public class FilterableGroupedDifficultyIcon : GroupedDifficultyIcon
{
public readonly List<CarouselBeatmap> Items;
public FilterableGroupedDifficultyIcon(List<CarouselBeatmap> items, RulesetInfo ruleset)
: base(items.Select(i => i.Beatmap).ToList(), ruleset, Color4.White)
{
Items = items;
foreach (var item in items)
item.Filtered.BindValueChanged(_ => Scheduler.AddOnce(updateFilteredDisplay));
updateFilteredDisplay();
}
protected override bool OnClick(ClickEvent e)
{
Items.First().State.Value = CarouselItemState.Selected;
return true;
}
private void updateFilteredDisplay()
{
// for now, fade the whole group based on the ratio of hidden items.
this.FadeTo(1 - 0.9f * ((float)Items.Count(i => i.Filtered.Value) / Items.Count), 100);
}
}
}

View File

@ -0,0 +1,72 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Select.Carousel
{
public class SetPanelBackground : BufferedContainer
{
public SetPanelBackground(WorkingBeatmap working)
{
CacheDrawnFrameBuffer = true;
RedrawOnScale = false;
Children = new Drawable[]
{
new BeatmapBackgroundSprite(working)
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
FillMode = FillMode.Fill,
},
new FillFlowContainer
{
Depth = -1,
RelativeSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
// This makes the gradient not be perfectly horizontal, but diagonal at a ~40° angle
Shear = new Vector2(0.8f, 0),
Alpha = 0.5f,
Children = new[]
{
// The left half with no gradient applied
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black,
Width = 0.4f,
},
// Piecewise-linear gradient with 3 segments to make it appear smoother
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = ColourInfo.GradientHorizontal(Color4.Black, new Color4(0f, 0f, 0f, 0.9f)),
Width = 0.05f,
},
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = ColourInfo.GradientHorizontal(new Color4(0f, 0f, 0f, 0.9f), new Color4(0f, 0f, 0f, 0.1f)),
Width = 0.2f,
},
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = ColourInfo.GradientHorizontal(new Color4(0f, 0f, 0f, 0.1f), new Color4(0, 0, 0, 0)),
Width = 0.05f,
},
}
},
};
}
}
}

View File

@ -0,0 +1,93 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Localisation;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osuTK;
namespace osu.Game.Screens.Select.Carousel
{
public class SetPanelContent : CompositeDrawable
{
private readonly CarouselBeatmapSet carouselSet;
public SetPanelContent(CarouselBeatmapSet carouselSet)
{
this.carouselSet = carouselSet;
// required to ensure we load as soon as any part of the panel comes on screen
RelativeSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load()
{
var beatmapSet = carouselSet.BeatmapSet;
InternalChild = new FillFlowContainer
{
// required to ensure we load as soon as any part of the panel comes on screen
RelativeSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Padding = new MarginPadding { Top = 5, Left = 18, Right = 10, Bottom = 10 },
Children = new Drawable[]
{
new OsuSpriteText
{
Text = new LocalisedString((beatmapSet.Metadata.TitleUnicode, beatmapSet.Metadata.Title)),
Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 22, italics: true),
Shadow = true,
},
new OsuSpriteText
{
Text = new LocalisedString((beatmapSet.Metadata.ArtistUnicode, beatmapSet.Metadata.Artist)),
Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 17, italics: true),
Shadow = true,
},
new FillFlowContainer
{
Direction = FillDirection.Horizontal,
AutoSizeAxes = Axes.Both,
Margin = new MarginPadding { Top = 5 },
Children = new Drawable[]
{
new BeatmapSetOnlineStatusPill
{
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
Margin = new MarginPadding { Right = 5 },
TextSize = 11,
TextPadding = new MarginPadding { Horizontal = 8, Vertical = 2 },
Status = beatmapSet.Status
},
new FillFlowContainer<DifficultyIcon>
{
AutoSizeAxes = Axes.Both,
Spacing = new Vector2(3),
ChildrenEnumerable = getDifficultyIcons(),
},
}
}
}
};
}
private const int maximum_difficulty_icons = 18;
private IEnumerable<DifficultyIcon> getDifficultyIcons()
{
var beatmaps = carouselSet.Beatmaps.ToList();
return beatmaps.Count > maximum_difficulty_icons
? (IEnumerable<DifficultyIcon>)beatmaps.GroupBy(b => b.Beatmap.Ruleset).Select(group => new FilterableGroupedDifficultyIcon(group.ToList(), group.Key))
: beatmaps.Select(b => new FilterableDifficultyIcon(b));
}
}
}

View File

@ -14,10 +14,12 @@ using osu.Framework.Bindables;
using System.Collections.Generic;
using osu.Game.Rulesets.Mods;
using System.Linq;
using System.Threading;
using osu.Framework.Threading;
using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets;
namespace osu.Game.Screens.Select.Details
{
@ -26,6 +28,12 @@ namespace osu.Game.Screens.Select.Details
[Resolved]
private IBindable<IReadOnlyList<Mod>> mods { get; set; }
[Resolved]
private IBindable<RulesetInfo> ruleset { get; set; }
[Resolved]
private BeatmapDifficultyCache difficultyCache { get; set; }
protected readonly StatisticRow FirstValue, HpDrain, Accuracy, ApproachRate;
private readonly StatisticRow starDifficulty;
@ -71,6 +79,7 @@ namespace osu.Game.Screens.Select.Details
{
base.LoadComplete();
ruleset.BindValueChanged(_ => updateStatistics());
mods.BindValueChanged(modsChanged, true);
}
@ -132,11 +141,39 @@ namespace osu.Game.Screens.Select.Details
break;
}
starDifficulty.Value = ((float)(Beatmap?.StarDifficulty ?? 0), null);
HpDrain.Value = (baseDifficulty?.DrainRate ?? 0, adjustedDifficulty?.DrainRate);
Accuracy.Value = (baseDifficulty?.OverallDifficulty ?? 0, adjustedDifficulty?.OverallDifficulty);
ApproachRate.Value = (baseDifficulty?.ApproachRate ?? 0, adjustedDifficulty?.ApproachRate);
updateStarDifficulty();
}
private IBindable<StarDifficulty> normalStarDifficulty;
private IBindable<StarDifficulty> moddedStarDifficulty;
private CancellationTokenSource starDifficultyCancellationSource;
private void updateStarDifficulty()
{
starDifficultyCancellationSource?.Cancel();
if (Beatmap == null)
return;
starDifficultyCancellationSource = new CancellationTokenSource();
normalStarDifficulty = difficultyCache.GetBindableDifficulty(Beatmap, ruleset.Value, null, starDifficultyCancellationSource.Token);
moddedStarDifficulty = difficultyCache.GetBindableDifficulty(Beatmap, ruleset.Value, mods.Value, starDifficultyCancellationSource.Token);
normalStarDifficulty.BindValueChanged(_ => updateDisplay());
moddedStarDifficulty.BindValueChanged(_ => updateDisplay(), true);
void updateDisplay() => starDifficulty.Value = ((float)normalStarDifficulty.Value.Stars, (float)moddedStarDifficulty.Value.Stars);
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
starDifficultyCancellationSource?.Cancel();
}
public class StatisticRow : Container, IHasAccentColour

View File

@ -29,16 +29,30 @@ namespace osu.Game.Screens.Select.Details
var retries = Metrics?.Retries ?? Array.Empty<int>();
var fails = Metrics?.Fails ?? Array.Empty<int>();
var retriesAndFails = sumRetriesAndFails(retries, fails);
float maxValue = fails.Any() ? fails.Zip(retries, (fail, retry) => fail + retry).Max() : 0;
float maxValue = retriesAndFails.Any() ? retriesAndFails.Max() : 0;
failGraph.MaxValue = maxValue;
retryGraph.MaxValue = maxValue;
failGraph.Values = fails.Select(f => (float)f);
retryGraph.Values = retries.Zip(fails, (retry, fail) => retry + Math.Clamp(fail, 0, maxValue));
failGraph.Values = fails.Select(v => (float)v);
retryGraph.Values = retriesAndFails.Select(v => (float)v);
}
}
private int[] sumRetriesAndFails(int[] retries, int[] fails)
{
var result = new int[Math.Max(retries.Length, fails.Length)];
for (int i = 0; i < retries.Length; ++i)
result[i] = retries[i];
for (int i = 0; i < fails.Length; ++i)
result[i] += fails[i];
return result;
}
public FailRetryGraph()
{
Children = new[]

View File

@ -15,7 +15,7 @@ using osu.Game.Rulesets;
namespace osu.Game.Screens.Select
{
public class DifficultyRecommender : Component, IOnlineComponent
public class DifficultyRecommender : Component
{
[Resolved]
private IAPIProvider api { get; set; }
@ -28,10 +28,13 @@ namespace osu.Game.Screens.Select
private readonly Dictionary<RulesetInfo, double> recommendedStarDifficulty = new Dictionary<RulesetInfo, double>();
private readonly IBindable<APIState> apiState = new Bindable<APIState>();
[BackgroundDependencyLoader]
private void load()
{
api.Register(this);
apiState.BindTo(api.State);
apiState.BindValueChanged(onlineStateChanged, true);
}
/// <summary>
@ -114,21 +117,14 @@ namespace osu.Game.Screens.Select
return orderedRulesets;
}
public void APIStateChanged(IAPIProvider api, APIState state)
private void onlineStateChanged(ValueChangedEvent<APIState> state) => Schedule(() =>
{
switch (state)
switch (state.NewValue)
{
case APIState.Online:
calculateRecommendedDifficulties();
break;
}
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
api?.Unregister(this);
}
});
}
}

View File

@ -2,33 +2,33 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using osuTK;
using osuTK.Graphics;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Screens.Select.Filter;
using Container = osu.Framework.Graphics.Containers.Container;
using osu.Framework.Graphics.Shapes;
using osu.Game.Configuration;
using osu.Game.Rulesets;
using osu.Framework.Input.Events;
using osu.Game.Collections;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets;
using osu.Game.Screens.Select.Filter;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Select
{
public class FilterControl : Container
{
public const float HEIGHT = 100;
public const float HEIGHT = 2 * side_margin + 85;
private const float side_margin = 20;
public Action<FilterCriteria> FilterChanged;
private readonly OsuTabControl<SortMode> sortTabs;
private readonly TabControl<GroupMode> groupTabs;
private OsuTabControl<SortMode> sortTabs;
private Bindable<SortMode> sortMode;
@ -44,6 +44,7 @@ namespace osu.Game.Screens.Select
Sort = sortMode.Value,
AllowConvertedBeatmaps = showConverted.Value,
Ruleset = ruleset.Value,
Collection = collectionDropdown?.Current.Value.Collection
};
if (!minimumStars.IsDefault)
@ -56,89 +57,148 @@ namespace osu.Game.Screens.Select
return criteria;
}
private readonly SeekLimitedSearchTextBox searchTextBox;
private SeekLimitedSearchTextBox searchTextBox;
private CollectionFilterDropdown collectionDropdown;
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
base.ReceivePositionalInputAt(screenSpacePos) || groupTabs.ReceivePositionalInputAt(screenSpacePos) || sortTabs.ReceivePositionalInputAt(screenSpacePos);
base.ReceivePositionalInputAt(screenSpacePos) || sortTabs.ReceivePositionalInputAt(screenSpacePos);
public FilterControl()
[BackgroundDependencyLoader(permitNulls: true)]
private void load(OsuColour colours, IBindable<RulesetInfo> parentRuleset, OsuConfigManager config)
{
sortMode = config.GetBindable<SortMode>(OsuSetting.SongSelectSortingMode);
groupMode = config.GetBindable<GroupMode>(OsuSetting.SongSelectGroupingMode);
Children = new Drawable[]
{
Background = new Box
new Box
{
Colour = Color4.Black,
Alpha = 0.8f,
Width = 2,
RelativeSizeAxes = Axes.Both,
},
new Container
{
Padding = new MarginPadding(20),
Padding = new MarginPadding(side_margin),
RelativeSizeAxes = Axes.Both,
Width = 0.5f,
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Children = new Drawable[]
// Reverse ChildID so that dropdowns in the top section appear on top of the bottom section.
Child = new ReverseChildIDFillFlowContainer<Drawable>
{
searchTextBox = new SeekLimitedSearchTextBox { RelativeSizeAxes = Axes.X },
new Box
RelativeSizeAxes = Axes.Both,
Spacing = new Vector2(0, 5),
Children = new[]
{
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,
Children = new Drawable[]
new Container
{
groupTabs = new OsuTabControl<GroupMode>
RelativeSizeAxes = Axes.X,
Height = 60,
Children = new Drawable[]
{
RelativeSizeAxes = Axes.X,
Height = 24,
Width = 0.5f,
AutoSort = true,
},
//spriteText = new OsuSpriteText
//{
// Font = @"Exo2.0-Bold",
// Text = "Sort results by",
// Size = 14,
// Margin = new MarginPadding
// {
// Top = 5,
// Bottom = 5
// },
//},
sortTabs = new OsuTabControl<SortMode>
{
RelativeSizeAxes = Axes.X,
Width = 0.5f,
Height = 24,
AutoSort = true,
searchTextBox = new SeekLimitedSearchTextBox { RelativeSizeAxes = Axes.X },
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,
Spacing = new Vector2(OsuTabControl<SortMode>.HORIZONTAL_SPACING, 0),
Children = new Drawable[]
{
new OsuTabControlCheckbox
{
Text = "Show converted",
Current = config.GetBindable<bool>(OsuSetting.ShowConvertedBeatmaps),
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
},
sortTabs = new OsuTabControl<SortMode>
{
RelativeSizeAxes = Axes.X,
Width = 0.5f,
Height = 24,
AutoSort = true,
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
AccentColour = colours.GreenLight,
Current = { BindTarget = sortMode }
},
new OsuSpriteText
{
Text = "Sort by",
Font = OsuFont.GetFont(size: 14),
Margin = new MarginPadding(5),
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
},
}
},
}
}
},
},
new Container
{
RelativeSizeAxes = Axes.X,
Height = 20,
Children = new Drawable[]
{
collectionDropdown = new CollectionFilterDropdown
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
RelativeSizeAxes = Axes.X,
Width = 0.4f,
}
}
},
}
}
}
};
searchTextBox.Current.ValueChanged += _ => FilterChanged?.Invoke(CreateCriteria());
config.BindWith(OsuSetting.ShowConvertedBeatmaps, showConverted);
showConverted.ValueChanged += _ => updateCriteria();
groupTabs.PinItem(GroupMode.All);
groupTabs.PinItem(GroupMode.RecentlyPlayed);
config.BindWith(OsuSetting.DisplayStarsMinimum, minimumStars);
minimumStars.ValueChanged += _ => updateCriteria();
config.BindWith(OsuSetting.DisplayStarsMaximum, maximumStars);
maximumStars.ValueChanged += _ => updateCriteria();
ruleset.BindTo(parentRuleset);
ruleset.BindValueChanged(_ => updateCriteria());
groupMode.BindValueChanged(_ => updateCriteria());
sortMode.BindValueChanged(_ => updateCriteria());
collectionDropdown.Current.ValueChanged += val =>
{
if (val.NewValue == null)
// may be null briefly while menu is repopulated.
return;
updateCriteria();
};
searchTextBox.Current.ValueChanged += _ => updateCriteria();
updateCriteria();
}
public void Deactivate()
{
searchTextBox.ReadOnly = true;
searchTextBox.HoldFocus = false;
if (searchTextBox.HasFocus)
GetContainingInputManager().ChangeFocus(searchTextBox);
@ -156,37 +216,6 @@ namespace osu.Game.Screens.Select
private readonly Bindable<double> minimumStars = new BindableDouble();
private readonly Bindable<double> maximumStars = new BindableDouble();
public readonly Box Background;
[BackgroundDependencyLoader(permitNulls: true)]
private void load(OsuColour colours, IBindable<RulesetInfo> parentRuleset, OsuConfigManager config)
{
sortTabs.AccentColour = colours.GreenLight;
config.BindWith(OsuSetting.ShowConvertedBeatmaps, showConverted);
showConverted.ValueChanged += _ => updateCriteria();
config.BindWith(OsuSetting.DisplayStarsMinimum, minimumStars);
minimumStars.ValueChanged += _ => updateCriteria();
config.BindWith(OsuSetting.DisplayStarsMaximum, maximumStars);
maximumStars.ValueChanged += _ => updateCriteria();
ruleset.BindTo(parentRuleset);
ruleset.BindValueChanged(_ => updateCriteria());
sortMode = config.GetBindable<SortMode>(OsuSetting.SongSelectSortingMode);
groupMode = config.GetBindable<GroupMode>(OsuSetting.SongSelectGroupingMode);
sortTabs.Current.BindTo(sortMode);
groupTabs.Current.BindTo(groupMode);
groupMode.BindValueChanged(_ => updateCriteria());
sortMode.BindValueChanged(_ => updateCriteria());
updateCriteria();
}
private void updateCriteria() => FilterChanged?.Invoke(CreateCriteria());
protected override bool OnClick(ClickEvent e) => true;

View File

@ -4,7 +4,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using osu.Game.Beatmaps;
using osu.Game.Collections;
using osu.Game.Rulesets;
using osu.Game.Screens.Select.Filter;
@ -41,6 +43,11 @@ namespace osu.Game.Screens.Select
private string searchText;
/// <summary>
/// <see cref="SearchText"/> as a number (if it can be parsed as one).
/// </summary>
public int? SearchNumber { get; private set; }
public string SearchText
{
get => searchText;
@ -48,9 +55,20 @@ namespace osu.Game.Screens.Select
{
searchText = value;
SearchTerms = searchText.Split(new[] { ',', ' ', '!' }, StringSplitOptions.RemoveEmptyEntries).ToArray();
SearchNumber = null;
if (SearchTerms.Length == 1 && int.TryParse(SearchTerms[0], out int parsed))
SearchNumber = parsed;
}
}
/// <summary>
/// The collection to filter beatmaps from.
/// </summary>
[CanBeNull]
public BeatmapCollection Collection;
public struct OptionalRange<T> : IEquatable<OptionalRange<T>>
where T : struct
{
@ -108,7 +126,7 @@ namespace osu.Game.Screens.Select
if (string.IsNullOrEmpty(value))
return false;
return value.IndexOf(SearchTerm, StringComparison.InvariantCultureIgnoreCase) >= 0;
return value.Contains(SearchTerm, StringComparison.InvariantCultureIgnoreCase);
}
public string SearchTerm;

View File

@ -11,7 +11,7 @@ namespace osu.Game.Screens.Select
internal static class FilterQueryParser
{
private static readonly Regex query_syntax_regex = new Regex(
@"\b(?<key>stars|ar|dr|cs|divisor|length|objects|bpm|status|creator|artist)(?<op>[=:><]+)(?<value>("".*"")|(\S*))",
@"\b(?<key>stars|ar|dr|hp|cs|divisor|length|objects|bpm|status|creator|artist)(?<op>[=:><]+)(?<value>("".*"")|(\S*))",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
internal static void ApplyQueries(FilterCriteria criteria, string query)
@ -43,6 +43,7 @@ namespace osu.Game.Screens.Select
break;
case "dr" when parseFloatWithPoint(value, out var dr):
case "hp" when parseFloatWithPoint(value, out dr):
updateCriteriaRange(ref criteria.DrainRate, op, dr, 0.1f / 2);
break;
@ -78,10 +79,10 @@ namespace osu.Game.Screens.Select
}
private static int getLengthScale(string value) =>
value.EndsWith("ms") ? 1 :
value.EndsWith("s") ? 1000 :
value.EndsWith("m") ? 60000 :
value.EndsWith("h") ? 3600000 : 1000;
value.EndsWith("ms", StringComparison.Ordinal) ? 1 :
value.EndsWith('s') ? 1000 :
value.EndsWith('m') ? 60000 :
value.EndsWith('h') ? 3600000 : 1000;
private static bool parseFloatWithPoint(string value, out float result) =>
float.TryParse(value, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out result);

View File

@ -12,7 +12,7 @@ namespace osu.Game.Screens.Select
public ImportFromStablePopup(Action importFromStable)
{
HeaderText = @"You have no beatmaps!";
BodyText = "An existing copy of osu! was found, though.\nWould you like to import your beatmaps, skins and scores?";
BodyText = "An existing copy of osu! was found, though.\nWould you like to import your beatmaps, skins, collections and scores?\nThis will create a second copy of all files on disk.";
Icon = FontAwesome.Solid.Plane;

View File

@ -9,7 +9,6 @@ using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Leaderboards;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
@ -41,25 +40,8 @@ namespace osu.Game.Screens.Select.Leaderboards
}
}
public APILegacyUserTopScoreInfo TopScore
{
get => topScoreContainer.Score.Value;
set
{
if (value == null)
topScoreContainer.Hide();
else
{
topScoreContainer.Show();
topScoreContainer.Score.Value = value;
}
}
}
private bool filterMods;
private UserTopScoreContainer topScoreContainer;
private IBindable<WeakReference<ScoreInfo>> itemRemoved;
/// <summary>
@ -101,11 +83,6 @@ namespace osu.Game.Screens.Select.Leaderboards
UpdateScores();
};
Content.Add(topScoreContainer = new UserTopScoreContainer
{
ScoreSelected = s => ScoreSelected?.Invoke(s)
});
itemRemoved = scoreManager.ItemRemoved.GetBoundCopy();
itemRemoved.BindValueChanged(onScoreRemoved);
}
@ -183,7 +160,7 @@ namespace osu.Game.Screens.Select.Leaderboards
req.Success += r =>
{
scoresCallback?.Invoke(r.Scores.Select(s => s.CreateScoreInfo(rulesets)));
TopScore = r.UserScore;
TopScore = r.UserScore?.CreateScoreInfo(rulesets);
};
return req;
@ -193,5 +170,10 @@ namespace osu.Game.Screens.Select.Leaderboards
{
Action = () => ScoreSelected?.Invoke(model)
};
protected override LeaderboardScore CreateDrawableTopScore(ScoreInfo model) => new LeaderboardScore(model, model.Position, false)
{
Action = () => ScoreSelected?.Invoke(model)
};
}
}

View File

@ -1,101 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Leaderboards;
using osu.Game.Rulesets;
using osu.Game.Scoring;
using osuTK;
namespace osu.Game.Screens.Select.Leaderboards
{
public class UserTopScoreContainer : VisibilityContainer
{
private const int duration = 500;
private readonly Container scoreContainer;
public Bindable<APILegacyUserTopScoreInfo> Score = new Bindable<APILegacyUserTopScoreInfo>();
public Action<ScoreInfo> ScoreSelected;
protected override bool StartHidden => true;
[Resolved]
private RulesetStore rulesets { get; set; }
public UserTopScoreContainer()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Margin = new MarginPadding { Vertical = 5 };
Children = new Drawable[]
{
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(5),
Children = new Drawable[]
{
new OsuSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Text = @"your personal best".ToUpper(),
Font = OsuFont.GetFont(size: 15, weight: FontWeight.Bold),
},
scoreContainer = new Container
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
}
}
}
};
Score.BindValueChanged(onScoreChanged);
}
private CancellationTokenSource loadScoreCancellation;
private void onScoreChanged(ValueChangedEvent<APILegacyUserTopScoreInfo> score)
{
var newScore = score.NewValue;
scoreContainer.Clear();
loadScoreCancellation?.Cancel();
if (newScore == null)
return;
var scoreInfo = newScore.Score.CreateScoreInfo(rulesets);
LoadComponentAsync(new LeaderboardScore(scoreInfo, newScore.Position, false)
{
Action = () => ScoreSelected?.Invoke(scoreInfo)
}, drawableScore =>
{
scoreContainer.Child = drawableScore;
drawableScore.FadeInFromZero(duration, Easing.OutQuint);
}, (loadScoreCancellation = new CancellationTokenSource()).Token);
}
protected override void PopIn() => this.FadeIn(duration, Easing.OutQuint);
protected override void PopOut() => this.FadeOut(duration, Easing.OutQuint);
}
}

View File

@ -76,7 +76,7 @@ namespace osu.Game.Screens.Select
item.Ruleset.Value = Ruleset.Value;
item.RequiredMods.Clear();
item.RequiredMods.AddRange(Mods.Value);
item.RequiredMods.AddRange(Mods.Value.Select(m => m.CreateCopy()));
}
}
}

View File

@ -12,7 +12,6 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osuTK;
using osuTK.Graphics;
using osuTK.Input;
using osu.Game.Graphics.Containers;
namespace osu.Game.Screens.Select.Options
@ -52,8 +51,6 @@ namespace osu.Game.Screens.Select.Options
set => secondLine.Text = value;
}
public Key? HotKey;
protected override bool OnMouseDown(MouseDownEvent e)
{
flash.FadeTo(0.1f, 1000, Easing.OutQuint);
@ -75,17 +72,6 @@ namespace osu.Game.Screens.Select.Options
return base.OnClick(e);
}
protected override bool OnKeyDown(KeyDownEvent e)
{
if (!e.Repeat && e.Key == HotKey)
{
Click();
return true;
}
return false;
}
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => box.ReceivePositionalInputAt(screenSpacePos);
public BeatmapOptionsButton()

View File

@ -11,6 +11,8 @@ using osuTK;
using osuTK.Graphics;
using osuTK.Input;
using osu.Game.Graphics.Containers;
using osu.Framework.Input.Events;
using System.Linq;
namespace osu.Game.Screens.Select.Options
{
@ -27,33 +29,6 @@ namespace osu.Game.Screens.Select.Options
public override bool BlockScreenWideMouse => false;
protected override void PopIn()
{
base.PopIn();
this.FadeIn(transition_duration, Easing.OutQuint);
if (buttonsContainer.Position.X == 1 || Alpha == 0)
buttonsContainer.MoveToX(x_position - x_movement);
holder.ScaleTo(new Vector2(1, 1), transition_duration / 2, Easing.OutQuint);
buttonsContainer.MoveToX(x_position, transition_duration, Easing.OutQuint);
buttonsContainer.TransformSpacingTo(Vector2.Zero, transition_duration, Easing.OutQuint);
}
protected override void PopOut()
{
base.PopOut();
holder.ScaleTo(new Vector2(1, 0), transition_duration / 2, Easing.InSine);
buttonsContainer.MoveToX(x_position + x_movement, transition_duration, Easing.InSine);
buttonsContainer.TransformSpacingTo(new Vector2(200f, 0f), transition_duration, Easing.InSine);
this.FadeOut(transition_duration, Easing.InQuint);
}
public BeatmapOptionsOverlay()
{
AutoSizeAxes = Axes.Y;
@ -87,9 +62,8 @@ namespace osu.Game.Screens.Select.Options
/// <param name="secondLine">Text in the second line.</param>
/// <param name="colour">Colour of the button.</param>
/// <param name="icon">Icon of the button.</param>
/// <param name="hotkey">Hotkey of the button.</param>
/// <param name="action">Binding the button does.</param>
public void AddButton(string firstLine, string secondLine, IconUsage icon, Color4 colour, Action action, Key? hotkey = null)
public void AddButton(string firstLine, string secondLine, IconUsage icon, Color4 colour, Action action)
{
var button = new BeatmapOptionsButton
{
@ -102,10 +76,58 @@ namespace osu.Game.Screens.Select.Options
Hide();
action?.Invoke();
},
HotKey = hotkey
};
buttonsContainer.Add(button);
}
protected override void PopIn()
{
base.PopIn();
this.FadeIn(transition_duration, Easing.OutQuint);
if (buttonsContainer.Position.X == 1 || Alpha == 0)
buttonsContainer.MoveToX(x_position - x_movement);
holder.ScaleTo(new Vector2(1, 1), transition_duration / 2, Easing.OutQuint);
buttonsContainer.MoveToX(x_position, transition_duration, Easing.OutQuint);
buttonsContainer.TransformSpacingTo(Vector2.Zero, transition_duration, Easing.OutQuint);
}
protected override void PopOut()
{
base.PopOut();
holder.ScaleTo(new Vector2(1, 0), transition_duration / 2, Easing.InSine);
buttonsContainer.MoveToX(x_position + x_movement, transition_duration, Easing.InSine);
buttonsContainer.TransformSpacingTo(new Vector2(200f, 0f), transition_duration, Easing.InSine);
this.FadeOut(transition_duration, Easing.InQuint);
}
protected override bool OnKeyDown(KeyDownEvent e)
{
// don't absorb control as ToolbarRulesetSelector uses control + number to navigate
if (e.ControlPressed) return false;
if (!e.Repeat && e.Key >= Key.Number1 && e.Key <= Key.Number9)
{
int requested = e.Key - Key.Number1;
// go reverse as buttonsContainer is a ReverseChildIDFillFlowContainer
BeatmapOptionsButton found = buttonsContainer.Children.ElementAtOrDefault((buttonsContainer.Children.Count - 1) - requested);
if (found != null)
{
found.Click();
return true;
}
}
return base.OnKeyDown(e);
}
}
}

View File

@ -29,6 +29,8 @@ namespace osu.Game.Screens.Select
private Bindable<TabType> selectedTab;
private Bindable<bool> selectedModsFilter;
public PlayBeatmapDetailArea()
{
Add(Leaderboard = new BeatmapLeaderboard { RelativeSizeAxes = Axes.Both });
@ -38,8 +40,13 @@ namespace osu.Game.Screens.Select
private void load(OsuConfigManager config)
{
selectedTab = config.GetBindable<TabType>(OsuSetting.BeatmapDetailTab);
selectedModsFilter = config.GetBindable<bool>(OsuSetting.BeatmapDetailModsFilter);
selectedTab.BindValueChanged(tab => CurrentTab.Value = getTabItemFromTabType(tab.NewValue), true);
CurrentTab.BindValueChanged(tab => selectedTab.Value = getTabTypeFromTabItem(tab.NewValue));
selectedModsFilter.BindValueChanged(checkbox => CurrentModsFilter.Value = checkbox.NewValue, true);
CurrentModsFilter.BindValueChanged(checkbox => selectedModsFilter.Value = checkbox.NewValue);
}
public override void Refresh()

View File

@ -7,6 +7,8 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Framework.Screens;
using osu.Game.Graphics;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking;
@ -20,6 +22,9 @@ namespace osu.Game.Screens.Select
private bool removeAutoModOnResume;
private OsuScreen player;
[Resolved(CanBeNull = true)]
private NotificationOverlay notifications { get; set; }
public override bool AllowExternalScreenChange => true;
protected override UserActivity InitialActivity => new UserActivity.ChoosingBeatmap();
@ -27,11 +32,7 @@ namespace osu.Game.Screens.Select
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
BeatmapOptions.AddButton(@"Edit", @"beatmap", FontAwesome.Solid.PencilAlt, colours.Yellow, () =>
{
ValidForResume = false;
Edit();
}, Key.Number4);
BeatmapOptions.AddButton(@"Edit", @"beatmap", FontAwesome.Solid.PencilAlt, colours.Yellow, () => Edit());
((PlayBeatmapDetailArea)BeatmapDetails).Leaderboard.ScoreSelected += PresentScore;
}
@ -49,8 +50,11 @@ namespace osu.Game.Screens.Select
if (removeAutoModOnResume)
{
var autoType = Ruleset.Value.CreateInstance().GetAutoplayMod().GetType();
ModSelect.DeselectTypes(new[] { autoType }, true);
var autoType = Ruleset.Value.CreateInstance().GetAutoplayMod()?.GetType();
if (autoType != null)
ModSelect.DeselectTypes(new[] { autoType }, true);
removeAutoModOnResume = false;
}
}
@ -78,10 +82,19 @@ namespace osu.Game.Screens.Select
if (GetContainingInputManager().CurrentState?.Keyboard.ControlPressed == true)
{
var auto = Ruleset.Value.CreateInstance().GetAutoplayMod();
var autoType = auto.GetType();
var autoType = auto?.GetType();
var mods = Mods.Value;
if (autoType == null)
{
notifications?.Post(new SimpleNotification
{
Text = "The current ruleset doesn't have an autoplay mod avalaible!"
});
return false;
}
if (mods.All(m => m.GetType() != autoType))
{
Mods.Value = mods.Append(auto).ToArray();

View File

@ -4,7 +4,6 @@
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@ -32,10 +31,13 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using osu.Framework.Audio.Track;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings;
using osu.Game.Collections;
using osu.Game.Graphics.UserInterface;
using osu.Game.Scoring;
using System.Diagnostics;
namespace osu.Game.Screens.Select
{
@ -97,11 +99,11 @@ namespace osu.Game.Screens.Select
private readonly Bindable<RulesetInfo> decoupledRuleset = new Bindable<RulesetInfo>();
[Resolved(canBeNull: true)]
[Resolved]
private MusicController music { get; set; }
[BackgroundDependencyLoader(true)]
private void load(AudioManager audio, DialogOverlay dialog, OsuColour colours, SkinManager skins, ScoreManager scores, DifficultyRecommender recommender)
private void load(AudioManager audio, DialogOverlay dialog, OsuColour colours, SkinManager skins, ScoreManager scores, CollectionManager collections, ManageCollectionsDialog manageCollectionsDialog, DifficultyRecommender recommender)
{
// initial value transfer is required for FilterControl (it uses our re-cached bindables in its async load for the initial filter).
transferRulesetValue();
@ -170,7 +172,6 @@ namespace osu.Game.Screens.Select
RelativeSizeAxes = Axes.X,
Height = FilterControl.HEIGHT,
FilterChanged = ApplyFilterToCarousel,
Background = { Width = 2 },
},
new GridContainer // used for max width implementation
{
@ -272,9 +273,10 @@ namespace osu.Game.Screens.Select
Footer.AddButton(new FooterButtonRandom { Action = triggerRandom });
Footer.AddButton(new FooterButtonOptions(), BeatmapOptions);
BeatmapOptions.AddButton(@"Remove", @"from unplayed", FontAwesome.Regular.TimesCircle, colours.Purple, null, Key.Number1);
BeatmapOptions.AddButton(@"Clear", @"local scores", FontAwesome.Solid.Eraser, colours.Purple, () => clearScores(Beatmap.Value.BeatmapInfo), Key.Number2);
BeatmapOptions.AddButton(@"Delete", @"all difficulties", FontAwesome.Solid.Trash, colours.Pink, () => delete(Beatmap.Value.BeatmapSetInfo), Key.Number3);
BeatmapOptions.AddButton(@"Manage", @"collections", FontAwesome.Solid.Book, colours.Green, () => manageCollectionsDialog?.Show());
BeatmapOptions.AddButton(@"Delete", @"all difficulties", FontAwesome.Solid.Trash, colours.Pink, () => delete(Beatmap.Value.BeatmapSetInfo));
BeatmapOptions.AddButton(@"Remove", @"from unplayed", FontAwesome.Regular.TimesCircle, colours.Purple, null);
BeatmapOptions.AddButton(@"Clear", @"local scores", FontAwesome.Solid.Eraser, colours.Purple, () => clearScores(Beatmap.Value.BeatmapInfo));
}
dialogOverlay = dialog;
@ -292,7 +294,12 @@ namespace osu.Game.Screens.Select
{
dialogOverlay.Push(new ImportFromStablePopup(() =>
{
Task.Run(beatmaps.ImportFromStableAsync).ContinueWith(_ => scores.ImportFromStableAsync(), TaskContinuationOptions.OnlyOnRanToCompletion);
Task.Run(beatmaps.ImportFromStableAsync)
.ContinueWith(_ =>
{
Task.Run(scores.ImportFromStableAsync);
Task.Run(collections.ImportFromStableAsync);
}, TaskContinuationOptions.OnlyOnRanToCompletion);
Task.Run(skins.ImportFromStableAsync);
}));
}
@ -509,6 +516,8 @@ namespace osu.Game.Screens.Select
FilterControl.Activate();
ModSelect.SelectedMods.BindTo(selectedMods);
beginLooping();
}
private const double logo_transition = 250;
@ -559,15 +568,16 @@ namespace osu.Game.Screens.Select
BeatmapDetails.Refresh();
Beatmap.Value.Track.Looping = true;
music?.ResetTrackAdjustments();
beginLooping();
music.ResetTrackAdjustments();
if (Beatmap != null && !Beatmap.Value.BeatmapSetInfo.DeletePending)
{
updateComponentFromBeatmap(Beatmap.Value);
// restart playback on returning to song select, regardless.
music?.Play();
// not sure this should be a permanent thing (we may want to leave a user pause paused even on returning)
music.Play(requestedByUser: true);
}
this.FadeIn(250);
@ -584,8 +594,7 @@ namespace osu.Game.Screens.Select
BeatmapOptions.Hide();
if (Beatmap.Value.Track != null)
Beatmap.Value.Track.Looping = false;
endLooping();
this.ScaleTo(1.1f, 250, Easing.InSine);
@ -597,12 +606,6 @@ namespace osu.Game.Screens.Select
public override bool OnExiting(IScreen next)
{
if (ModSelect.State.Value == Visibility.Visible)
{
ModSelect.Hide();
return true;
}
if (base.OnExiting(next))
return true;
@ -612,8 +615,43 @@ namespace osu.Game.Screens.Select
FilterControl.Deactivate();
if (Beatmap.Value.Track != null)
Beatmap.Value.Track.Looping = false;
endLooping();
return false;
}
private bool isHandlingLooping;
private void beginLooping()
{
Debug.Assert(!isHandlingLooping);
music.CurrentTrack.Looping = isHandlingLooping = true;
music.TrackChanged += ensureTrackLooping;
}
private void endLooping()
{
// may be called multiple times during screen exit process.
if (!isHandlingLooping)
return;
music.CurrentTrack.Looping = isHandlingLooping = false;
music.TrackChanged -= ensureTrackLooping;
}
private void ensureTrackLooping(WorkingBeatmap beatmap, TrackChangeDirection changeDirection)
=> music.CurrentTrack.Looping = true;
public override bool OnBackButton()
{
if (ModSelect.State.Value == Visibility.Visible)
{
ModSelect.Hide();
return true;
}
return false;
}
@ -623,6 +661,9 @@ namespace osu.Game.Screens.Select
base.Dispose(isDisposing);
decoupledRuleset.UnbindAll();
if (music != null)
music.TrackChanged -= ensureTrackLooping;
}
/// <summary>
@ -642,12 +683,9 @@ namespace osu.Game.Screens.Select
beatmapInfoWedge.Beatmap = beatmap;
BeatmapDetails.Beatmap = beatmap;
if (beatmap.Track != null)
beatmap.Track.Looping = true;
}
private readonly WeakReference<Track> lastTrack = new WeakReference<Track>(null);
private readonly WeakReference<ITrack> lastTrack = new WeakReference<ITrack>(null);
/// <summary>
/// Ensures some music is playing for the current track.
@ -655,14 +693,14 @@ namespace osu.Game.Screens.Select
/// </summary>
private void ensurePlayingSelected()
{
Track track = Beatmap.Value.Track;
ITrack track = music.CurrentTrack;
bool isNewTrack = !lastTrack.TryGetTarget(out var last) || last != track;
track.RestartPoint = Beatmap.Value.Metadata.PreviewTime;
if (!track.IsRunning && (music?.IsUserPaused != true || isNewTrack))
music?.Play(true);
if (!track.IsRunning && (music.UserPauseRequested != true || isNewTrack))
music.Play(true);
lastTrack.SetTarget(track);
}