mirror of
https://github.com/osukey/osukey.git
synced 2025-08-05 07:33:55 +09:00
Merge branch 'master' into rebind-song-select
This commit is contained in:
@ -1,28 +1,30 @@
|
||||
// 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.Layout;
|
||||
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,12 +75,24 @@ 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>
|
||||
public bool BeatmapSetsLoaded { get; private set; }
|
||||
|
||||
private readonly CarouselScrollContainer scroll;
|
||||
protected readonly CarouselScrollContainer Scroll;
|
||||
|
||||
private IEnumerable<CarouselBeatmapSet> beatmapSets => root.Children.OfType<CarouselBeatmapSet>();
|
||||
|
||||
@ -93,33 +107,33 @@ 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);
|
||||
Scroll.Clear(false);
|
||||
itemsCache.Invalidate();
|
||||
scrollPositionCache.Invalidate();
|
||||
ScrollToSelected();
|
||||
|
||||
// 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(() =>
|
||||
{
|
||||
BeatmapSetsChanged?.Invoke();
|
||||
BeatmapSetsLoaded = true;
|
||||
|
||||
itemsCache.Invalidate();
|
||||
});
|
||||
}
|
||||
|
||||
private readonly List<float> yPositions = new List<float>();
|
||||
private readonly Cached itemsCache = new Cached();
|
||||
private readonly Cached scrollPositionCache = new Cached();
|
||||
private readonly List<CarouselItem> visibleItems = new List<CarouselItem>();
|
||||
|
||||
private readonly Container<DrawableCarouselItem> scrollableContent;
|
||||
private readonly Cached itemsCache = new Cached();
|
||||
private PendingScrollOperation pendingScrollOperation = PendingScrollOperation.None;
|
||||
|
||||
public Bindable<bool> RightClickScrollingEnabled = new Bindable<bool>();
|
||||
|
||||
@ -127,8 +141,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,19 +148,20 @@ 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);
|
||||
InternalChild = new OsuContextMenuContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = scroll = new CarouselScrollContainer
|
||||
Children = new Drawable[]
|
||||
{
|
||||
Masking = false,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = scrollableContent = new Container<DrawableCarouselItem>
|
||||
setPool,
|
||||
Scroll = new CarouselScrollContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -163,7 +176,7 @@ namespace osu.Game.Screens.Select
|
||||
config.BindWith(OsuSetting.RandomSelectAlgorithm, RandomAlgorithm);
|
||||
config.BindWith(OsuSetting.SongSelectRightMouseScroll, RightClickScrollingEnabled);
|
||||
|
||||
RightClickScrollingEnabled.ValueChanged += enabled => scroll.RightMouseScrollbar = enabled.NewValue;
|
||||
RightClickScrollingEnabled.ValueChanged += enabled => Scroll.RightMouseScrollbar = enabled.NewValue;
|
||||
RightClickScrollingEnabled.TriggerChange();
|
||||
|
||||
itemUpdated = beatmaps.ItemUpdated.GetBoundCopy();
|
||||
@ -175,7 +188,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 +315,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 +334,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;
|
||||
@ -397,12 +417,12 @@ namespace osu.Game.Screens.Select
|
||||
/// <summary>
|
||||
/// The position of the lower visible bound with respect to the current scroll position.
|
||||
/// </summary>
|
||||
private float visibleBottomBound => scroll.Current + DrawHeight + BleedBottom;
|
||||
private float visibleBottomBound => Scroll.Current + DrawHeight + BleedBottom;
|
||||
|
||||
/// <summary>
|
||||
/// The position of the upper visible bound with respect to the current scroll position.
|
||||
/// </summary>
|
||||
private float visibleUpperBound => scroll.Current - BleedTop;
|
||||
private float visibleUpperBound => Scroll.Current - BleedTop;
|
||||
|
||||
public void FlushPendingFilterOperations()
|
||||
{
|
||||
@ -423,7 +443,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()
|
||||
{
|
||||
@ -432,17 +464,9 @@ namespace osu.Game.Screens.Select
|
||||
root.Filter(activeCriteria);
|
||||
itemsCache.Invalidate();
|
||||
|
||||
if (alwaysResetScrollPosition || !scroll.UserScrolling)
|
||||
ScrollToSelected();
|
||||
if (alwaysResetScrollPosition || !Scroll.UserScrolling)
|
||||
ScrollToSelected(true);
|
||||
}
|
||||
|
||||
PendingFilter?.Cancel();
|
||||
PendingFilter = null;
|
||||
|
||||
if (debounce)
|
||||
PendingFilter = Scheduler.AddDelayed(perform, 250);
|
||||
else
|
||||
perform();
|
||||
}
|
||||
|
||||
private float? scrollTarget;
|
||||
@ -450,34 +474,56 @@ namespace osu.Game.Screens.Select
|
||||
/// <summary>
|
||||
/// Scroll to the current <see cref="SelectedBeatmap"/>.
|
||||
/// </summary>
|
||||
public void ScrollToSelected() => scrollPositionCache.Invalidate();
|
||||
/// <param name="immediate">
|
||||
/// Whether the scroll position should immediately be shifted to the target, delegating animation to visible panels.
|
||||
/// This should be true for operations like filtering - where panels are changing visibility state - to avoid large jumps in animation.
|
||||
/// </param>
|
||||
public void ScrollToSelected(bool immediate = false) =>
|
||||
pendingScrollOperation = immediate ? PendingScrollOperation.Immediate : PendingScrollOperation.Standard;
|
||||
|
||||
#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), e.Key);
|
||||
return true;
|
||||
|
||||
case Key.Right:
|
||||
SelectNext(1, true);
|
||||
if (!e.Repeat)
|
||||
beginRepeatSelection(() => SelectNext(), 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,94 +532,147 @@ 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 bool OnInvalidate(Invalidation invalidation, InvalidationSource source)
|
||||
{
|
||||
// handles the vertical size of the carousel changing (ie. on window resize when aspect ratio has changed).
|
||||
if ((invalidation & Invalidation.Layout) > 0)
|
||||
itemsCache.Invalidate();
|
||||
|
||||
return base.OnInvalidate(invalidation, source);
|
||||
}
|
||||
|
||||
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);
|
||||
// if there is a pending scroll action we apply it without animation and transfer the difference in position to the panels.
|
||||
// this is intentionally applied before updating the visible range below, to avoid animating new items (sourced from pool) from locations off-screen, as it looks bad.
|
||||
if (pendingScrollOperation != PendingScrollOperation.None)
|
||||
updateScrollPosition();
|
||||
|
||||
int firstIndex = yPositions.BinarySearch(visibleUpperBound - DrawableCarouselItem.MAX_HEIGHT);
|
||||
if (firstIndex < 0) firstIndex = ~firstIndex;
|
||||
int lastIndex = yPositions.BinarySearch(visibleBottomBound);
|
||||
if (lastIndex < 0) lastIndex = ~lastIndex;
|
||||
// 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 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 Scroll.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;
|
||||
|
||||
Scroll.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();
|
||||
// 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 Scroll.Children)
|
||||
{
|
||||
updateItem(item);
|
||||
|
||||
// Update externally controlled state of currently visible items
|
||||
// (e.g. x-offset and opacity).
|
||||
foreach (DrawableCarouselItem p in scrollableContent.Children)
|
||||
updateItem(p);
|
||||
if (item is DrawableCarouselBeatmapSet set)
|
||||
{
|
||||
foreach (var diff in set.DrawableBeatmaps)
|
||||
updateItem(diff, item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void UpdateAfterChildren()
|
||||
private readonly CarouselBoundsItem carouselBoundsItem = new CarouselBoundsItem();
|
||||
|
||||
private (int firstIndex, int lastIndex) getDisplayRange()
|
||||
{
|
||||
base.UpdateAfterChildren();
|
||||
// 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;
|
||||
|
||||
if (!scrollPositionCache.IsValid)
|
||||
updateScrollPosition();
|
||||
}
|
||||
carouselBoundsItem.CarouselYPosition = visibleBottomBound + distance_offscreen_to_preload;
|
||||
int lastIndex = visibleItems.BinarySearch(carouselBoundsItem);
|
||||
if (lastIndex < 0) lastIndex = ~lastIndex;
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
// 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));
|
||||
|
||||
// aggressively dispose "off-screen" items to reduce GC pressure.
|
||||
foreach (var i in Items)
|
||||
i.Dispose();
|
||||
return (firstIndex, lastIndex);
|
||||
}
|
||||
|
||||
private void beatmapRemoved(ValueChangedEvent<WeakReference<BeatmapSetInfo>> weakItem)
|
||||
@ -632,87 +731,77 @@ 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;
|
||||
|
||||
if (BeatmapSetsLoaded && (selectedBeatmapSet == null || selectedBeatmap == null || selectedBeatmapSet.State.Value != CarouselItemState.Selected))
|
||||
{
|
||||
selectedBeatmapSet = null;
|
||||
SelectionChanged?.Invoke(null);
|
||||
}
|
||||
Scroll.ScrollContent.Height = currentY;
|
||||
|
||||
itemsCache.Validate();
|
||||
|
||||
// update and let external consumers know about selection loss.
|
||||
if (BeatmapSetsLoaded)
|
||||
{
|
||||
bool selectionLost = selectedBeatmapSet != null && selectedBeatmapSet.State.Value != CarouselItemState.Selected;
|
||||
|
||||
if (selectionLost)
|
||||
{
|
||||
selectedBeatmapSet = null;
|
||||
SelectionChanged?.Invoke(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool firstScroll = true;
|
||||
@ -724,12 +813,30 @@ namespace osu.Game.Screens.Select
|
||||
if (firstScroll)
|
||||
{
|
||||
// reduce movement when first displaying the carousel.
|
||||
scroll.ScrollTo(scrollTarget.Value - 200, false);
|
||||
Scroll.ScrollTo(scrollTarget.Value - 200, false);
|
||||
firstScroll = false;
|
||||
}
|
||||
|
||||
scroll.ScrollTo(scrollTarget.Value);
|
||||
scrollPositionCache.Validate();
|
||||
switch (pendingScrollOperation)
|
||||
{
|
||||
case PendingScrollOperation.Standard:
|
||||
Scroll.ScrollTo(scrollTarget.Value);
|
||||
break;
|
||||
|
||||
case PendingScrollOperation.Immediate:
|
||||
|
||||
// in order to simplify animation logic, rather than using the animated version of ScrollTo,
|
||||
// we take the difference in scroll height and apply to all visible panels.
|
||||
// this avoids edge cases like when the visible panels is reduced suddenly, causing ScrollContainer
|
||||
// to enter clamp-special-case mode where it animates completely differently to normal.
|
||||
float scrollChange = scrollTarget.Value - Scroll.Current;
|
||||
Scroll.ScrollTo(scrollTarget.Value, false);
|
||||
foreach (var i in Scroll.Children)
|
||||
i.Y += scrollChange;
|
||||
break;
|
||||
}
|
||||
|
||||
pendingScrollOperation = PendingScrollOperation.None;
|
||||
}
|
||||
}
|
||||
|
||||
@ -755,21 +862,38 @@ 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 = Scroll.ScrollContent.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));
|
||||
}
|
||||
|
||||
private enum PendingScrollOperation
|
||||
{
|
||||
None,
|
||||
Standard,
|
||||
Immediate,
|
||||
}
|
||||
|
||||
/// <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
|
||||
@ -794,25 +918,17 @@ namespace osu.Game.Screens.Select
|
||||
}
|
||||
}
|
||||
|
||||
private class CarouselScrollContainer : OsuScrollContainer
|
||||
protected class CarouselScrollContainer : UserTrackingScrollContainer<DrawableCarouselItem>
|
||||
{
|
||||
private bool rightMouseScrollBlocked;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the last scroll event was user triggered, directly on the scroll container.
|
||||
/// </summary>
|
||||
public bool UserScrolling { get; private set; }
|
||||
|
||||
protected override void OnUserScroll(float value, bool animated = true, double? distanceDecay = default)
|
||||
public CarouselScrollContainer()
|
||||
{
|
||||
UserScrolling = true;
|
||||
base.OnUserScroll(value, animated, distanceDecay);
|
||||
}
|
||||
// size is determined by the carousel itself, due to not all content necessarily being loaded.
|
||||
ScrollContent.AutoSizeAxes = Axes.None;
|
||||
|
||||
public new void ScrollTo(float value, bool animated = true, double? distanceDecay = null)
|
||||
{
|
||||
UserScrolling = false;
|
||||
base.ScrollTo(value, animated, distanceDecay);
|
||||
// the scroll container may get pushed off-screen by global screen changes, but we still want panels to display outside of the bounds.
|
||||
Masking = false;
|
||||
}
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e)
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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
|
||||
|
@ -9,7 +9,6 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using System.Linq;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Game.Screens.Select.Details;
|
||||
@ -19,6 +18,7 @@ using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Online;
|
||||
|
||||
namespace osu.Game.Screens.Select
|
||||
{
|
||||
@ -27,11 +27,8 @@ namespace osu.Game.Screens.Select
|
||||
private const float spacing = 10;
|
||||
private const float transition_duration = 250;
|
||||
|
||||
private readonly FillFlowContainer top, statsFlow;
|
||||
private readonly AdvancedStats advanced;
|
||||
private readonly DetailBox ratingsContainer;
|
||||
private readonly UserRatings ratings;
|
||||
private readonly OsuScrollContainer metadataScroll;
|
||||
private readonly MetadataSection description, source, tags;
|
||||
private readonly Container failRetryContainer;
|
||||
private readonly FailRetryGraph failRetryGraph;
|
||||
@ -40,8 +37,6 @@ namespace osu.Game.Screens.Select
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; }
|
||||
|
||||
private ScheduledDelegate pendingBeatmapSwitch;
|
||||
|
||||
[Resolved]
|
||||
private RulesetStore rulesets { get; set; }
|
||||
|
||||
@ -56,15 +51,12 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
beatmap = value;
|
||||
|
||||
pendingBeatmapSwitch?.Cancel();
|
||||
pendingBeatmapSwitch = Schedule(updateStatistics);
|
||||
Scheduler.AddOnce(updateStatistics);
|
||||
}
|
||||
}
|
||||
|
||||
public BeatmapDetails()
|
||||
{
|
||||
Container content;
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
@ -72,105 +64,111 @@ namespace osu.Game.Screens.Select
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4.Black.Opacity(0.5f),
|
||||
},
|
||||
content = new Container
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding { Horizontal = spacing },
|
||||
Children = new Drawable[]
|
||||
{
|
||||
top = new FillFlowContainer
|
||||
new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Children = new Drawable[]
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
RowDimensions = new[]
|
||||
{
|
||||
statsFlow = new FillFlowContainer
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
new Dimension()
|
||||
},
|
||||
Content = new[]
|
||||
{
|
||||
new Drawable[]
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Width = 0.5f,
|
||||
Spacing = new Vector2(spacing),
|
||||
Padding = new MarginPadding { Right = spacing / 2 },
|
||||
Children = new[]
|
||||
{
|
||||
new DetailBox
|
||||
{
|
||||
Child = advanced = new AdvancedStats
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Padding = new MarginPadding { Horizontal = spacing, Top = spacing * 2, Bottom = spacing },
|
||||
},
|
||||
},
|
||||
ratingsContainer = new DetailBox
|
||||
{
|
||||
Child = ratings = new UserRatings
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 134,
|
||||
Padding = new MarginPadding { Horizontal = spacing, Top = spacing },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
metadataScroll = new OsuScrollContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Width = 0.5f,
|
||||
ScrollbarVisible = false,
|
||||
Padding = new MarginPadding { Left = spacing / 2 },
|
||||
Child = new FillFlowContainer
|
||||
new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
LayoutDuration = transition_duration,
|
||||
LayoutEasing = Easing.OutQuad,
|
||||
Spacing = new Vector2(spacing * 2),
|
||||
Margin = new MarginPadding { Top = spacing * 2 },
|
||||
Children = new[]
|
||||
Direction = FillDirection.Horizontal,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
description = new MetadataSection("Description"),
|
||||
source = new MetadataSection("Source"),
|
||||
tags = new MetadataSection("Tags"),
|
||||
new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Width = 0.5f,
|
||||
Spacing = new Vector2(spacing),
|
||||
Padding = new MarginPadding { Right = spacing / 2 },
|
||||
Children = new[]
|
||||
{
|
||||
new DetailBox().WithChild(advanced = new AdvancedStats
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Padding = new MarginPadding { Horizontal = spacing, Top = spacing * 2, Bottom = spacing },
|
||||
}),
|
||||
new DetailBox().WithChild(new OnlineViewContainer(string.Empty)
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 134,
|
||||
Padding = new MarginPadding { Horizontal = spacing, Top = spacing },
|
||||
Child = ratings = new UserRatings
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
new OsuScrollContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Width = 0.5f,
|
||||
ScrollbarVisible = false,
|
||||
Padding = new MarginPadding { Left = spacing / 2 },
|
||||
Child = new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
LayoutDuration = transition_duration,
|
||||
LayoutEasing = Easing.OutQuad,
|
||||
Spacing = new Vector2(spacing * 2),
|
||||
Margin = new MarginPadding { Top = spacing * 2 },
|
||||
Children = new[]
|
||||
{
|
||||
description = new MetadataSection("Description"),
|
||||
source = new MetadataSection("Source"),
|
||||
tags = new MetadataSection("Tags"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
failRetryContainer = new Container
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new OsuSpriteText
|
||||
new Drawable[]
|
||||
{
|
||||
Text = "Points of Failure",
|
||||
Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 14),
|
||||
},
|
||||
failRetryGraph = new FailRetryGraph
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding { Top = 14 + spacing / 2 },
|
||||
},
|
||||
},
|
||||
failRetryContainer = new OnlineViewContainer("Sign in to view more details")
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new OsuSpriteText
|
||||
{
|
||||
Text = "Points of Failure",
|
||||
Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 14),
|
||||
},
|
||||
failRetryGraph = new FailRetryGraph
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding { Top = 14 + spacing / 2 },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
loading = new LoadingLayer(content),
|
||||
loading = new LoadingLayer(true)
|
||||
};
|
||||
}
|
||||
|
||||
protected override void UpdateAfterChildren()
|
||||
{
|
||||
base.UpdateAfterChildren();
|
||||
|
||||
metadataScroll.Height = statsFlow.DrawHeight;
|
||||
failRetryContainer.Height = DrawHeight - Padding.TotalVertical - (top.DrawHeight + spacing / 2);
|
||||
}
|
||||
|
||||
private void updateStatistics()
|
||||
{
|
||||
advanced.Beatmap = Beatmap;
|
||||
@ -186,7 +184,7 @@ namespace osu.Game.Screens.Select
|
||||
}
|
||||
|
||||
// for now, let's early abort if an OnlineBeatmapID is not present (should have been populated at import time).
|
||||
if (Beatmap?.OnlineBeatmapID == null)
|
||||
if (Beatmap?.OnlineBeatmapID == null || api.State.Value == APIState.Offline)
|
||||
{
|
||||
updateMetrics();
|
||||
return;
|
||||
@ -236,17 +234,18 @@ 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)
|
||||
{
|
||||
ratings.Metrics = beatmap.BeatmapSet.Metrics;
|
||||
ratingsContainer.FadeIn(transition_duration);
|
||||
ratings.FadeIn(transition_duration);
|
||||
}
|
||||
else
|
||||
{
|
||||
// loading or just has no data server-side.
|
||||
ratings.Metrics = new BeatmapSetMetrics { Ratings = new int[10] };
|
||||
ratingsContainer.FadeTo(0.25f, transition_duration);
|
||||
ratings.FadeTo(0.25f, transition_duration);
|
||||
}
|
||||
|
||||
if (hasRetriesFails)
|
||||
@ -261,7 +260,6 @@ namespace osu.Game.Screens.Select
|
||||
Fails = new int[100],
|
||||
Retries = new int[100],
|
||||
};
|
||||
failRetryContainer.FadeOut(transition_duration);
|
||||
}
|
||||
|
||||
loading.Hide();
|
||||
@ -300,6 +298,7 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
public MetadataSection(string title)
|
||||
{
|
||||
Alpha = 0;
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
|
||||
|
@ -4,7 +4,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using System.Threading;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using osu.Framework.Allocation;
|
||||
@ -24,9 +24,11 @@ using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Game.Configuration;
|
||||
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
|
||||
{
|
||||
@ -36,7 +38,16 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
private static readonly Vector2 wedged_container_shear = new Vector2(shear_width / SongSelect.WEDGE_HEIGHT, 0);
|
||||
|
||||
private readonly IBindable<RulesetInfo> ruleset = new Bindable<RulesetInfo>();
|
||||
[Resolved]
|
||||
private IBindable<RulesetInfo> ruleset { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private IBindable<IReadOnlyList<Mod>> mods { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private BeatmapDifficultyCache difficultyCache { get; set; }
|
||||
|
||||
private IBindable<StarDifficulty?> beatmapDifficulty;
|
||||
|
||||
protected BufferedWedgeInfo Info;
|
||||
|
||||
@ -56,11 +67,10 @@ namespace osu.Game.Screens.Select
|
||||
};
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader(true)]
|
||||
private void load([CanBeNull] Bindable<RulesetInfo> parentRuleset)
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
ruleset.BindTo(parentRuleset);
|
||||
ruleset.ValueChanged += _ => updateDisplay();
|
||||
ruleset.BindValueChanged(_ => updateDisplay());
|
||||
}
|
||||
|
||||
protected override void PopIn()
|
||||
@ -79,6 +89,8 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
private WorkingBeatmap beatmap;
|
||||
|
||||
private CancellationTokenSource cancellationSource;
|
||||
|
||||
public WorkingBeatmap Beatmap
|
||||
{
|
||||
get => beatmap;
|
||||
@ -87,6 +99,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 +116,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, mods.Value, beatmapDifficulty.Value ?? new StarDifficulty())
|
||||
{
|
||||
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
|
||||
@ -133,19 +163,26 @@ namespace osu.Game.Screens.Select
|
||||
public OsuSpriteText ArtistLabel { get; private set; }
|
||||
public BeatmapSetOnlineStatusPill StatusPill { get; private set; }
|
||||
public FillFlowContainer MapperContainer { get; private set; }
|
||||
public FillFlowContainer InfoLabelContainer { get; private set; }
|
||||
|
||||
private ILocalisedBindableString titleBinding;
|
||||
private ILocalisedBindableString artistBinding;
|
||||
private FillFlowContainer infoLabelContainer;
|
||||
private Container bpmLabelContainer;
|
||||
|
||||
private readonly WorkingBeatmap beatmap;
|
||||
private readonly RulesetInfo ruleset;
|
||||
private readonly IReadOnlyList<Mod> mods;
|
||||
private readonly StarDifficulty starDifficulty;
|
||||
|
||||
public BufferedWedgeInfo(WorkingBeatmap beatmap, RulesetInfo userRuleset)
|
||||
private ModSettingChangeTracker settingChangeTracker;
|
||||
|
||||
public BufferedWedgeInfo(WorkingBeatmap beatmap, RulesetInfo userRuleset, IReadOnlyList<Mod> mods, StarDifficulty difficulty)
|
||||
: base(pixelSnapping: true)
|
||||
{
|
||||
this.beatmap = beatmap;
|
||||
ruleset = userRuleset ?? beatmap.BeatmapInfo.Ruleset;
|
||||
this.mods = mods;
|
||||
starDifficulty = difficulty;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@ -155,12 +192,10 @@ namespace osu.Game.Screens.Select
|
||||
var metadata = beatmapInfo.Metadata ?? beatmap.BeatmapSetInfo?.Metadata ?? new BeatmapMetadata();
|
||||
|
||||
CacheDrawnFrameBuffer = true;
|
||||
RedrawOnScale = false;
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
titleBinding = localisation.GetLocalisedString(new LocalisedString((metadata.TitleUnicode, metadata.Title)));
|
||||
artistBinding = localisation.GetLocalisedString(new LocalisedString((metadata.ArtistUnicode, metadata.Artist)));
|
||||
titleBinding = localisation.GetLocalisedString(new RomanisableString(metadata.TitleUnicode, metadata.Title));
|
||||
artistBinding = localisation.GetLocalisedString(new RomanisableString(metadata.ArtistUnicode, metadata.Artist));
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
@ -190,7 +225,7 @@ namespace osu.Game.Screens.Select
|
||||
},
|
||||
},
|
||||
},
|
||||
new DifficultyColourBar(beatmapInfo)
|
||||
new DifficultyColourBar(starDifficulty)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Width = 20,
|
||||
@ -223,10 +258,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,
|
||||
@ -264,12 +309,11 @@ namespace osu.Game.Screens.Select
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Children = getMapper(metadata)
|
||||
},
|
||||
InfoLabelContainer = new FillFlowContainer
|
||||
infoLabelContainer = new FillFlowContainer
|
||||
{
|
||||
Margin = new MarginPadding { Top = 20 },
|
||||
Spacing = new Vector2(20, 0),
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Children = getInfoLabels()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -281,8 +325,17 @@ namespace osu.Game.Screens.Select
|
||||
// no difficulty means it can't have a status to show
|
||||
if (beatmapInfo.Version == null)
|
||||
StatusPill.Hide();
|
||||
|
||||
addInfoLabels();
|
||||
}
|
||||
|
||||
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;
|
||||
@ -290,63 +343,91 @@ namespace osu.Game.Screens.Select
|
||||
ForceRedraw();
|
||||
}
|
||||
|
||||
private InfoLabel[] getInfoLabels()
|
||||
private void addInfoLabels()
|
||||
{
|
||||
var b = beatmap.Beatmap;
|
||||
if (beatmap.Beatmap?.HitObjects?.Any() != true)
|
||||
return;
|
||||
|
||||
List<InfoLabel> labels = new List<InfoLabel>();
|
||||
|
||||
if (b?.HitObjects?.Any() == true)
|
||||
infoLabelContainer.Children = new Drawable[]
|
||||
{
|
||||
labels.Add(new InfoLabel(new BeatmapStatistic
|
||||
new InfoLabel(new BeatmapStatistic
|
||||
{
|
||||
Name = "Length",
|
||||
Icon = FontAwesome.Regular.Clock,
|
||||
Content = TimeSpan.FromMilliseconds(b.BeatmapInfo.Length).ToString(@"m\:ss"),
|
||||
}));
|
||||
|
||||
labels.Add(new InfoLabel(new BeatmapStatistic
|
||||
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Length),
|
||||
Content = TimeSpan.FromMilliseconds(beatmap.BeatmapInfo.Length).ToString(@"m\:ss"),
|
||||
}),
|
||||
bpmLabelContainer = new Container
|
||||
{
|
||||
Name = "BPM",
|
||||
Icon = FontAwesome.Regular.Circle,
|
||||
Content = getBPMRange(b),
|
||||
}));
|
||||
AutoSizeAxes = Axes.Both,
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Spacing = new Vector2(20, 0),
|
||||
Children = getRulesetInfoLabels()
|
||||
}
|
||||
};
|
||||
|
||||
settingChangeTracker = new ModSettingChangeTracker(mods);
|
||||
settingChangeTracker.SettingChanged += _ => refreshBPMLabel();
|
||||
|
||||
refreshBPMLabel();
|
||||
}
|
||||
|
||||
private InfoLabel[] getRulesetInfoLabels()
|
||||
{
|
||||
try
|
||||
{
|
||||
IBeatmap playableBeatmap;
|
||||
|
||||
try
|
||||
{
|
||||
IBeatmap playableBeatmap;
|
||||
|
||||
try
|
||||
{
|
||||
// Try to get the beatmap with the user's ruleset
|
||||
playableBeatmap = beatmap.GetPlayableBeatmap(ruleset, Array.Empty<Mod>());
|
||||
}
|
||||
catch (BeatmapInvalidForRulesetException)
|
||||
{
|
||||
// Can't be converted to the user's ruleset, so use the beatmap's own ruleset
|
||||
playableBeatmap = beatmap.GetPlayableBeatmap(beatmap.BeatmapInfo.Ruleset, Array.Empty<Mod>());
|
||||
}
|
||||
|
||||
labels.AddRange(playableBeatmap.GetStatistics().Select(s => new InfoLabel(s)));
|
||||
// Try to get the beatmap with the user's ruleset
|
||||
playableBeatmap = beatmap.GetPlayableBeatmap(ruleset, Array.Empty<Mod>());
|
||||
}
|
||||
catch (Exception e)
|
||||
catch (BeatmapInvalidForRulesetException)
|
||||
{
|
||||
Logger.Error(e, "Could not load beatmap successfully!");
|
||||
// Can't be converted to the user's ruleset, so use the beatmap's own ruleset
|
||||
playableBeatmap = beatmap.GetPlayableBeatmap(beatmap.BeatmapInfo.Ruleset, Array.Empty<Mod>());
|
||||
}
|
||||
|
||||
return playableBeatmap.GetStatistics().Select(s => new InfoLabel(s)).ToArray();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error(e, "Could not load beatmap successfully!");
|
||||
}
|
||||
|
||||
return labels.ToArray();
|
||||
return Array.Empty<InfoLabel>();
|
||||
}
|
||||
|
||||
private string getBPMRange(IBeatmap beatmap)
|
||||
private void refreshBPMLabel()
|
||||
{
|
||||
double bpmMax = beatmap.ControlPointInfo.BPMMaximum;
|
||||
double bpmMin = beatmap.ControlPointInfo.BPMMinimum;
|
||||
var b = beatmap.Beatmap;
|
||||
if (b == null)
|
||||
return;
|
||||
|
||||
if (Precision.AlmostEquals(bpmMin, bpmMax))
|
||||
return $"{bpmMin:0}";
|
||||
// this doesn't consider mods which apply variable rates, yet.
|
||||
double rate = 1;
|
||||
foreach (var mod in mods.OfType<IApplicableToRate>())
|
||||
rate = mod.ApplyToRate(0, rate);
|
||||
|
||||
return $"{bpmMin:0}-{bpmMax:0} (mostly {beatmap.ControlPointInfo.BPMMode:0})";
|
||||
double bpmMax = b.ControlPointInfo.BPMMaximum * rate;
|
||||
double bpmMin = b.ControlPointInfo.BPMMinimum * rate;
|
||||
double mostCommonBPM = 60000 / b.GetMostCommonBeatLength() * rate;
|
||||
|
||||
string labelText = Precision.AlmostEquals(bpmMin, bpmMax)
|
||||
? $"{bpmMin:0}"
|
||||
: $"{bpmMin:0}-{bpmMax:0} (mostly {mostCommonBPM:0})";
|
||||
|
||||
bpmLabelContainer.Child = new InfoLabel(new BeatmapStatistic
|
||||
{
|
||||
Name = "BPM",
|
||||
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Bpm),
|
||||
Content = labelText
|
||||
});
|
||||
|
||||
ForceRedraw();
|
||||
}
|
||||
|
||||
private OsuSpriteText[] getMapper(BeatmapMetadata metadata)
|
||||
@ -369,6 +450,12 @@ namespace osu.Game.Screens.Select
|
||||
};
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
settingChangeTracker?.Dispose();
|
||||
}
|
||||
|
||||
public class InfoLabel : Container, IHasTooltip
|
||||
{
|
||||
public string TooltipText { get; }
|
||||
@ -401,10 +488,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 +517,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 +529,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[]
|
||||
{
|
||||
|
@ -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,23 @@ 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;
|
||||
|
||||
if (match && criteria.RulesetCriteria != null)
|
||||
match &= criteria.RulesetCriteria.Matches(Beatmap);
|
||||
|
||||
Filtered.Value = !match;
|
||||
}
|
||||
|
||||
|
@ -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,11 +43,9 @@ namespace osu.Game.Screens.Select.Carousel
|
||||
.ForEach(AddChild);
|
||||
}
|
||||
|
||||
protected override DrawableCarouselItem CreateDrawableRepresentation() => new DrawableCarouselBeatmapSet(this);
|
||||
|
||||
protected override CarouselItem GetNextToSelect()
|
||||
{
|
||||
if (LastSelected == null)
|
||||
if (LastSelected == null || LastSelected.Filtered.Value)
|
||||
{
|
||||
if (GetRecommendedBeatmap?.Invoke(Children.OfType<CarouselBeatmap>().Where(b => !b.Filtered.Value).Select(b => b.Beatmap)) is BeatmapInfo recommended)
|
||||
return Children.OfType<CarouselBeatmap>().First(b => b.Beatmap == recommended);
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
160
osu.Game/Screens/Select/Carousel/CarouselHeader.cs
Normal file
160
osu.Game/Screens/Select/Carousel/CarouselHeader.cs
Normal file
@ -0,0 +1,160 @@
|
||||
// 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 osu.Game.Graphics.UserInterface;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.Select.Carousel
|
||||
{
|
||||
public class CarouselHeader : Container
|
||||
{
|
||||
public Container BorderContainer;
|
||||
|
||||
public readonly Bindable<CarouselItemState> State = new Bindable<CarouselItemState>(CarouselItemState.NotSelected);
|
||||
|
||||
private readonly HoverLayer hoverLayer;
|
||||
|
||||
protected override Container<Drawable> Content { get; } = new Container { RelativeSizeAxes = Axes.Both };
|
||||
|
||||
private const float corner_radius = 10;
|
||||
private const float border_thickness = 2.5f;
|
||||
|
||||
public CarouselHeader()
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
Height = DrawableCarouselItem.MAX_HEIGHT;
|
||||
|
||||
InternalChild = BorderContainer = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
CornerRadius = corner_radius,
|
||||
BorderColour = new Color4(221, 255, 255, 255),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
Content,
|
||||
hoverLayer = new HoverLayer()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
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:
|
||||
hoverLayer.InsetForBorder = false;
|
||||
|
||||
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:
|
||||
hoverLayer.InsetForBorder = true;
|
||||
|
||||
BorderContainer.BorderThickness = border_thickness;
|
||||
BorderContainer.EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Glow,
|
||||
Colour = new Color4(130, 204, 255, 150),
|
||||
Radius = 20,
|
||||
Roundness = 10,
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public class HoverLayer : HoverSampleDebounceComponent
|
||||
{
|
||||
private Sample sampleHover;
|
||||
|
||||
private Box box;
|
||||
|
||||
public HoverLayer()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(AudioManager audio, OsuColour colours)
|
||||
{
|
||||
InternalChild = box = new Box
|
||||
{
|
||||
Colour = colours.Blue.Opacity(0.1f),
|
||||
Alpha = 0,
|
||||
Blending = BlendingParameters.Additive,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
};
|
||||
|
||||
sampleHover = audio.Samples.Get("SongSelect/song-ping");
|
||||
}
|
||||
|
||||
public bool InsetForBorder
|
||||
{
|
||||
set
|
||||
{
|
||||
if (value)
|
||||
{
|
||||
// apply same border as above to avoid applying additive overlay to it (and blowing out the colour).
|
||||
Masking = true;
|
||||
CornerRadius = corner_radius;
|
||||
BorderThickness = border_thickness;
|
||||
}
|
||||
else
|
||||
{
|
||||
BorderThickness = 0;
|
||||
CornerRadius = 0;
|
||||
Masking = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
box.FadeIn(100, Easing.OutQuint);
|
||||
return base.OnHover(e);
|
||||
}
|
||||
|
||||
protected override void OnHoverLost(HoverLostEvent e)
|
||||
{
|
||||
box.FadeOut(1000, Easing.OutQuint);
|
||||
base.OnHoverLost(e);
|
||||
}
|
||||
|
||||
public override void PlayHoverSample()
|
||||
{
|
||||
if (sampleHover == null) return;
|
||||
|
||||
sampleHover.Frequency.Value = 0.90 + RNG.NextDouble(0.2);
|
||||
sampleHover.Play();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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,19 @@ 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 ?? 0);
|
||||
}, true);
|
||||
}
|
||||
|
||||
base.ApplyState();
|
||||
}
|
||||
|
||||
@ -196,14 +238,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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,130 +3,212 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using JetBrains.Annotations;
|
||||
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.Framework.Utils;
|
||||
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>();
|
||||
|
||||
[CanBeNull]
|
||||
private Container<DrawableCarouselItem> beatmapContainer;
|
||||
|
||||
private BeatmapSetInfo beatmapSet;
|
||||
|
||||
[CanBeNull]
|
||||
private Task beatmapsLoadTask;
|
||||
|
||||
[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 Update()
|
||||
{
|
||||
var beatmaps = ((CarouselBeatmapSet)Item).Beatmaps.ToList();
|
||||
base.Update();
|
||||
|
||||
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));
|
||||
// position updates should not occur if the item is filtered away.
|
||||
// this avoids panels flying across the screen only to be eventually off-screen or faded out.
|
||||
if (!Item.Visible)
|
||||
return;
|
||||
|
||||
float targetY = Item.CarouselYPosition;
|
||||
|
||||
if (Precision.AlmostEquals(targetY, Y))
|
||||
Y = targetY;
|
||||
else
|
||||
// algorithm for this is taken from ScrollContainer.
|
||||
// while it doesn't necessarily need to match 1:1, as we are emulating scroll in some cases this feels most correct.
|
||||
Y = (float)Interpolation.Lerp(targetY, Y, Math.Exp(-0.01 * Time.Elapsed));
|
||||
}
|
||||
|
||||
protected override void UpdateItem()
|
||||
{
|
||||
base.UpdateItem();
|
||||
|
||||
Content.Clear();
|
||||
|
||||
beatmapContainer = null;
|
||||
beatmapsLoadTask = 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)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
},
|
||||
mainFlow = new DelayedLoadWrapper(() => new SetPanelContent((CarouselBeatmapSet)Item), 100)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
},
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
updateBeatmapYPositions();
|
||||
}
|
||||
|
||||
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())
|
||||
};
|
||||
|
||||
beatmapsLoadTask = LoadComponentAsync(beatmapContainer, loaded =>
|
||||
{
|
||||
// make sure the pooled target hasn't changed.
|
||||
if (beatmapContainer != loaded)
|
||||
return;
|
||||
|
||||
Content.Child = loaded;
|
||||
updateBeatmapYPositions();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void updateBeatmapYPositions()
|
||||
{
|
||||
if (beatmapContainer == null)
|
||||
return;
|
||||
|
||||
if (beatmapsLoadTask == null || !beatmapsLoadTask.IsCompleted)
|
||||
return;
|
||||
|
||||
float yPos = DrawableCarouselBeatmap.CAROUSEL_BEATMAP_SPACING;
|
||||
|
||||
bool isSelected = Item.State.Value == CarouselItemState.Selected;
|
||||
|
||||
foreach (var panel in beatmapContainer.Children)
|
||||
{
|
||||
if (isSelected)
|
||||
{
|
||||
panel.MoveToY(yPos, 800, Easing.OutQuint);
|
||||
yPos += panel.Item.TotalHeight;
|
||||
}
|
||||
else
|
||||
panel.MoveToY(0, 800, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
|
||||
public MenuItem[] ContextMenuItems
|
||||
{
|
||||
get
|
||||
{
|
||||
Debug.Assert(beatmapSet != null);
|
||||
|
||||
List<MenuItem> items = new List<MenuItem>();
|
||||
|
||||
if (Item.State.Value == CarouselItemState.NotSelected)
|
||||
@ -135,125 +217,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 }
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
35
osu.Game/Screens/Select/Carousel/FilterableDifficultyIcon.cs
Normal file
35
osu.Game/Screens/Select/Carousel/FilterableDifficultyIcon.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
72
osu.Game/Screens/Select/Carousel/SetPanelBackground.cs
Normal file
72
osu.Game/Screens/Select/Carousel/SetPanelBackground.cs
Normal 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,
|
||||
},
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
93
osu.Game/Screens/Select/Carousel/SetPanelContent.cs
Normal file
93
osu.Game/Screens/Select/Carousel/SetPanelContent.cs
Normal 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 RomanisableString(beatmapSet.Metadata.TitleUnicode, beatmapSet.Metadata.Title),
|
||||
Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 22, italics: true),
|
||||
Shadow = true,
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Text = new RomanisableString(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));
|
||||
}
|
||||
}
|
||||
}
|
@ -14,10 +14,13 @@ using osu.Framework.Bindables;
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Localisation;
|
||||
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 +29,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,35 +80,26 @@ namespace osu.Game.Screens.Select.Details
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
ruleset.BindValueChanged(_ => updateStatistics());
|
||||
mods.BindValueChanged(modsChanged, true);
|
||||
}
|
||||
|
||||
private readonly List<ISettingsItem> references = new List<ISettingsItem>();
|
||||
private ModSettingChangeTracker modSettingChangeTracker;
|
||||
private ScheduledDelegate debouncedStatisticsUpdate;
|
||||
|
||||
private void modsChanged(ValueChangedEvent<IReadOnlyList<Mod>> mods)
|
||||
{
|
||||
// TODO: find a more permanent solution for this if/when it is needed in other components.
|
||||
// this is generating drawables for the only purpose of storing bindable references.
|
||||
foreach (var r in references)
|
||||
r.Dispose();
|
||||
modSettingChangeTracker?.Dispose();
|
||||
|
||||
references.Clear();
|
||||
|
||||
ScheduledDelegate debounce = null;
|
||||
|
||||
foreach (var mod in mods.NewValue.OfType<IApplicableToDifficulty>())
|
||||
modSettingChangeTracker = new ModSettingChangeTracker(mods.NewValue);
|
||||
modSettingChangeTracker.SettingChanged += m =>
|
||||
{
|
||||
foreach (var setting in mod.CreateSettingsControls().OfType<ISettingsItem>())
|
||||
{
|
||||
setting.SettingChanged += () =>
|
||||
{
|
||||
debounce?.Cancel();
|
||||
debounce = Scheduler.AddDelayed(updateStatistics, 100);
|
||||
};
|
||||
if (!(m is IApplicableToDifficulty))
|
||||
return;
|
||||
|
||||
references.Add(setting);
|
||||
}
|
||||
}
|
||||
debouncedStatisticsUpdate?.Cancel();
|
||||
debouncedStatisticsUpdate = Scheduler.AddDelayed(updateStatistics, 100);
|
||||
};
|
||||
|
||||
updateStatistics();
|
||||
}
|
||||
@ -132,11 +132,38 @@ 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 CancellationTokenSource starDifficultyCancellationSource;
|
||||
|
||||
private void updateStarDifficulty()
|
||||
{
|
||||
starDifficultyCancellationSource?.Cancel();
|
||||
|
||||
if (Beatmap == null)
|
||||
return;
|
||||
|
||||
starDifficultyCancellationSource = new CancellationTokenSource();
|
||||
|
||||
var normalStarDifficulty = difficultyCache.GetDifficultyAsync(Beatmap, ruleset.Value, null, starDifficultyCancellationSource.Token);
|
||||
var moddedStarDifficulty = difficultyCache.GetDifficultyAsync(Beatmap, ruleset.Value, mods.Value, starDifficultyCancellationSource.Token);
|
||||
|
||||
Task.WhenAll(normalStarDifficulty, moddedStarDifficulty).ContinueWith(_ => Schedule(() =>
|
||||
{
|
||||
starDifficulty.Value = ((float)normalStarDifficulty.Result.Stars, (float)moddedStarDifficulty.Result.Stars);
|
||||
}), starDifficultyCancellationSource.Token, TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.Current);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
modSettingChangeTracker?.Dispose();
|
||||
starDifficultyCancellationSource?.Cancel();
|
||||
}
|
||||
|
||||
public class StatisticRow : Container, IHasAccentColour
|
||||
@ -153,7 +180,7 @@ namespace osu.Game.Screens.Select.Details
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; }
|
||||
|
||||
public string Title
|
||||
public LocalisableString Title
|
||||
{
|
||||
get => name.Text;
|
||||
set => name.Text = value;
|
||||
|
@ -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[]
|
||||
|
@ -1,92 +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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Rulesets;
|
||||
|
||||
namespace osu.Game.Screens.Select
|
||||
{
|
||||
public class DifficultyRecommender : Component, IOnlineComponent
|
||||
{
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private RulesetStore rulesets { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private Bindable<RulesetInfo> ruleset { get; set; }
|
||||
|
||||
private readonly Dictionary<RulesetInfo, double> recommendedStarDifficulty = new Dictionary<RulesetInfo, double>();
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
api.Register(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find the recommended difficulty from a selection of available difficulties for the current local user.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This requires the user to be online for now.
|
||||
/// </remarks>
|
||||
/// <param name="beatmaps">A collection of beatmaps to select a difficulty from.</param>
|
||||
/// <returns>The recommended difficulty, or null if a recommendation could not be provided.</returns>
|
||||
public BeatmapInfo GetRecommendedBeatmap(IEnumerable<BeatmapInfo> beatmaps)
|
||||
{
|
||||
if (recommendedStarDifficulty.TryGetValue(ruleset.Value, out var stars))
|
||||
{
|
||||
return beatmaps.OrderBy(b =>
|
||||
{
|
||||
var difference = b.StarDifficulty - stars;
|
||||
return difference >= 0 ? difference * 2 : difference * -1; // prefer easier over harder
|
||||
}).FirstOrDefault();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void calculateRecommendedDifficulties()
|
||||
{
|
||||
rulesets.AvailableRulesets.ForEach(rulesetInfo =>
|
||||
{
|
||||
var req = new GetUserRequest(api.LocalUser.Value.Id, rulesetInfo);
|
||||
|
||||
req.Success += result =>
|
||||
{
|
||||
// algorithm taken from https://github.com/ppy/osu-web/blob/e6e2825516449e3d0f3f5e1852c6bdd3428c3437/app/Models/User.php#L1505
|
||||
recommendedStarDifficulty[rulesetInfo] = Math.Pow((double)(result.Statistics.PP ?? 0), 0.4) * 0.195;
|
||||
};
|
||||
|
||||
api.Queue(req);
|
||||
});
|
||||
}
|
||||
|
||||
public void APIStateChanged(IAPIProvider api, APIState state)
|
||||
{
|
||||
switch (state)
|
||||
{
|
||||
case APIState.Online:
|
||||
calculateRecommendedDifficulties();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
api?.Unregister(this);
|
||||
}
|
||||
}
|
||||
}
|
17
osu.Game/Screens/Select/Filter/Operator.cs
Normal file
17
osu.Game/Screens/Select/Filter/Operator.cs
Normal file
@ -0,0 +1,17 @@
|
||||
// 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.
|
||||
|
||||
namespace osu.Game.Screens.Select.Filter
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines logical operators that can be used in the song select search box keyword filters.
|
||||
/// </summary>
|
||||
public enum Operator
|
||||
{
|
||||
Less,
|
||||
LessOrEqual,
|
||||
Equal,
|
||||
GreaterOrEqual,
|
||||
Greater
|
||||
}
|
||||
}
|
@ -2,33 +2,34 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using System.Diagnostics;
|
||||
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;
|
||||
|
||||
@ -36,6 +37,8 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
public FilterCriteria CreateCriteria()
|
||||
{
|
||||
Debug.Assert(ruleset.Value.ID != null);
|
||||
|
||||
var query = searchTextBox.Text;
|
||||
|
||||
var criteria = new FilterCriteria
|
||||
@ -44,6 +47,7 @@ namespace osu.Game.Screens.Select
|
||||
Sort = sortMode.Value,
|
||||
AllowConvertedBeatmaps = showConverted.Value,
|
||||
Ruleset = ruleset.Value,
|
||||
Collection = collectionDropdown?.Current.Value?.Collection
|
||||
};
|
||||
|
||||
if (!minimumStars.IsDefault)
|
||||
@ -52,93 +56,154 @@ namespace osu.Game.Screens.Select
|
||||
if (!maximumStars.IsDefault)
|
||||
criteria.UserStarDifficulty.Max = maximumStars.Value;
|
||||
|
||||
criteria.RulesetCriteria = ruleset.Value.CreateInstance().CreateRulesetFilterCriteria();
|
||||
|
||||
FilterQueryParser.ApplyQueries(criteria, query);
|
||||
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 +221,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;
|
||||
|
@ -4,8 +4,11 @@
|
||||
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.Rulesets.Filter;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
|
||||
namespace osu.Game.Screens.Select
|
||||
@ -41,6 +44,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 +56,23 @@ 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;
|
||||
|
||||
[CanBeNull]
|
||||
public IRulesetFilterCriteria RulesetCriteria { get; set; }
|
||||
|
||||
public struct OptionalRange<T> : IEquatable<OptionalRange<T>>
|
||||
where T : struct
|
||||
{
|
||||
@ -108,7 +130,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;
|
||||
|
@ -5,13 +5,17 @@ using System;
|
||||
using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
|
||||
namespace osu.Game.Screens.Select
|
||||
{
|
||||
internal static class FilterQueryParser
|
||||
/// <summary>
|
||||
/// Utility class used for parsing song select filter queries entered via the search box.
|
||||
/// </summary>
|
||||
public 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>\w+)(?<op>(:|=|(>|<)(:|=)?))(?<value>("".*"")|(\S*))",
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
internal static void ApplyQueries(FilterCriteria criteria, string query)
|
||||
@ -19,193 +23,287 @@ namespace osu.Game.Screens.Select
|
||||
foreach (Match match in query_syntax_regex.Matches(query))
|
||||
{
|
||||
var key = match.Groups["key"].Value.ToLower();
|
||||
var op = match.Groups["op"].Value;
|
||||
var op = parseOperator(match.Groups["op"].Value);
|
||||
var value = match.Groups["value"].Value;
|
||||
|
||||
parseKeywordCriteria(criteria, key, value, op);
|
||||
|
||||
query = query.Replace(match.ToString(), "");
|
||||
if (tryParseKeywordCriteria(criteria, key, value, op))
|
||||
query = query.Replace(match.ToString(), "");
|
||||
}
|
||||
|
||||
criteria.SearchText = query;
|
||||
}
|
||||
|
||||
private static void parseKeywordCriteria(FilterCriteria criteria, string key, string value, string op)
|
||||
private static bool tryParseKeywordCriteria(FilterCriteria criteria, string key, string value, Operator op)
|
||||
{
|
||||
switch (key)
|
||||
{
|
||||
case "stars" when parseFloatWithPoint(value, out var stars):
|
||||
updateCriteriaRange(ref criteria.StarDifficulty, op, stars, 0.01f / 2);
|
||||
break;
|
||||
case "stars":
|
||||
return TryUpdateCriteriaRange(ref criteria.StarDifficulty, op, value, 0.01d / 2);
|
||||
|
||||
case "ar" when parseFloatWithPoint(value, out var ar):
|
||||
updateCriteriaRange(ref criteria.ApproachRate, op, ar, 0.1f / 2);
|
||||
break;
|
||||
case "ar":
|
||||
return TryUpdateCriteriaRange(ref criteria.ApproachRate, op, value);
|
||||
|
||||
case "dr" when parseFloatWithPoint(value, out var dr):
|
||||
updateCriteriaRange(ref criteria.DrainRate, op, dr, 0.1f / 2);
|
||||
break;
|
||||
case "dr":
|
||||
case "hp":
|
||||
return TryUpdateCriteriaRange(ref criteria.DrainRate, op, value);
|
||||
|
||||
case "cs" when parseFloatWithPoint(value, out var cs):
|
||||
updateCriteriaRange(ref criteria.CircleSize, op, cs, 0.1f / 2);
|
||||
break;
|
||||
case "cs":
|
||||
return TryUpdateCriteriaRange(ref criteria.CircleSize, op, value);
|
||||
|
||||
case "bpm" when parseDoubleWithPoint(value, out var bpm):
|
||||
updateCriteriaRange(ref criteria.BPM, op, bpm, 0.01d / 2);
|
||||
break;
|
||||
case "bpm":
|
||||
return TryUpdateCriteriaRange(ref criteria.BPM, op, value, 0.01d / 2);
|
||||
|
||||
case "length" when parseDoubleWithPoint(value.TrimEnd('m', 's', 'h'), out var length):
|
||||
var scale = getLengthScale(value);
|
||||
updateCriteriaRange(ref criteria.Length, op, length * scale, scale / 2.0);
|
||||
break;
|
||||
case "length":
|
||||
return tryUpdateLengthRange(criteria, op, value);
|
||||
|
||||
case "divisor" when parseInt(value, out var divisor):
|
||||
updateCriteriaRange(ref criteria.BeatDivisor, op, divisor);
|
||||
break;
|
||||
case "divisor":
|
||||
return TryUpdateCriteriaRange(ref criteria.BeatDivisor, op, value, tryParseInt);
|
||||
|
||||
case "status" when Enum.TryParse<BeatmapSetOnlineStatus>(value, true, out var statusValue):
|
||||
updateCriteriaRange(ref criteria.OnlineStatus, op, statusValue);
|
||||
break;
|
||||
case "status":
|
||||
return TryUpdateCriteriaRange(ref criteria.OnlineStatus, op, value,
|
||||
(string s, out BeatmapSetOnlineStatus val) => Enum.TryParse(value, true, out val));
|
||||
|
||||
case "creator":
|
||||
updateCriteriaText(ref criteria.Creator, op, value);
|
||||
break;
|
||||
return TryUpdateCriteriaText(ref criteria.Creator, op, value);
|
||||
|
||||
case "artist":
|
||||
updateCriteriaText(ref criteria.Artist, op, value);
|
||||
break;
|
||||
return TryUpdateCriteriaText(ref criteria.Artist, op, value);
|
||||
|
||||
default:
|
||||
return criteria.RulesetCriteria?.TryParseCustomKeywordCriteria(key, op, value) ?? false;
|
||||
}
|
||||
}
|
||||
|
||||
private static Operator parseOperator(string value)
|
||||
{
|
||||
switch (value)
|
||||
{
|
||||
case "=":
|
||||
case ":":
|
||||
return Operator.Equal;
|
||||
|
||||
case "<":
|
||||
return Operator.Less;
|
||||
|
||||
case "<=":
|
||||
case "<:":
|
||||
return Operator.LessOrEqual;
|
||||
|
||||
case ">":
|
||||
return Operator.Greater;
|
||||
|
||||
case ">=":
|
||||
case ">:":
|
||||
return Operator.GreaterOrEqual;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(value), $"Unsupported operator {value}");
|
||||
}
|
||||
}
|
||||
|
||||
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) =>
|
||||
private static bool tryParseFloatWithPoint(string value, out float result) =>
|
||||
float.TryParse(value, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out result);
|
||||
|
||||
private static bool parseDoubleWithPoint(string value, out double result) =>
|
||||
private static bool tryParseDoubleWithPoint(string value, out double result) =>
|
||||
double.TryParse(value, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out result);
|
||||
|
||||
private static bool parseInt(string value, out int result) =>
|
||||
private static bool tryParseInt(string value, out int result) =>
|
||||
int.TryParse(value, NumberStyles.None, CultureInfo.InvariantCulture, out result);
|
||||
|
||||
private static void updateCriteriaText(ref FilterCriteria.OptionalTextFilter textFilter, string op, string value)
|
||||
/// <summary>
|
||||
/// Attempts to parse a keyword filter with the specified <paramref name="op"/> and textual <paramref name="value"/>.
|
||||
/// If the value indicates a valid textual filter, the function returns <c>true</c> and the resulting data is stored into
|
||||
/// <paramref name="textFilter"/>.
|
||||
/// </summary>
|
||||
/// <param name="textFilter">The <see cref="FilterCriteria.OptionalTextFilter"/> to store the parsed data into, if successful.</param>
|
||||
/// <param name="op">
|
||||
/// The operator for the keyword filter.
|
||||
/// Only <see cref="Operator.Equal"/> is valid for textual filters.
|
||||
/// </param>
|
||||
/// <param name="value">The value of the keyword filter.</param>
|
||||
public static bool TryUpdateCriteriaText(ref FilterCriteria.OptionalTextFilter textFilter, Operator op, string value)
|
||||
{
|
||||
switch (op)
|
||||
{
|
||||
case "=":
|
||||
case ":":
|
||||
case Operator.Equal:
|
||||
textFilter.SearchTerm = value.Trim('"');
|
||||
break;
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void updateCriteriaRange(ref FilterCriteria.OptionalRange<float> range, string op, float value, float tolerance = 0.05f)
|
||||
/// <summary>
|
||||
/// Attempts to parse a keyword filter of type <see cref="float"/>
|
||||
/// from the specified <paramref name="op"/> and <paramref name="val"/>.
|
||||
/// If <paramref name="val"/> can be parsed as a <see cref="float"/>, the function returns <c>true</c>
|
||||
/// and the resulting range constraint is stored into <paramref name="range"/>.
|
||||
/// </summary>
|
||||
/// <param name="range">
|
||||
/// The <see cref="float"/>-typed <see cref="FilterCriteria.OptionalRange{T}"/>
|
||||
/// to store the parsed data into, if successful.
|
||||
/// </param>
|
||||
/// <param name="op">The operator for the keyword filter.</param>
|
||||
/// <param name="val">The value of the keyword filter.</param>
|
||||
/// <param name="tolerance">Allowed tolerance of the parsed range boundary value.</param>
|
||||
public static bool TryUpdateCriteriaRange(ref FilterCriteria.OptionalRange<float> range, Operator op, string val, float tolerance = 0.05f)
|
||||
=> tryParseFloatWithPoint(val, out float value) && tryUpdateCriteriaRange(ref range, op, value, tolerance);
|
||||
|
||||
private static bool tryUpdateCriteriaRange(ref FilterCriteria.OptionalRange<float> range, Operator op, float value, float tolerance = 0.05f)
|
||||
{
|
||||
switch (op)
|
||||
{
|
||||
default:
|
||||
return;
|
||||
return false;
|
||||
|
||||
case "=":
|
||||
case ":":
|
||||
case Operator.Equal:
|
||||
range.Min = value - tolerance;
|
||||
range.Max = value + tolerance;
|
||||
break;
|
||||
|
||||
case ">":
|
||||
case Operator.Greater:
|
||||
range.Min = value + tolerance;
|
||||
break;
|
||||
|
||||
case ">=":
|
||||
case ">:":
|
||||
case Operator.GreaterOrEqual:
|
||||
range.Min = value - tolerance;
|
||||
break;
|
||||
|
||||
case "<":
|
||||
case Operator.Less:
|
||||
range.Max = value - tolerance;
|
||||
break;
|
||||
|
||||
case "<=":
|
||||
case "<:":
|
||||
case Operator.LessOrEqual:
|
||||
range.Max = value + tolerance;
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void updateCriteriaRange(ref FilterCriteria.OptionalRange<double> range, string op, double value, double tolerance = 0.05)
|
||||
/// <summary>
|
||||
/// Attempts to parse a keyword filter of type <see cref="double"/>
|
||||
/// from the specified <paramref name="op"/> and <paramref name="val"/>.
|
||||
/// If <paramref name="val"/> can be parsed as a <see cref="double"/>, the function returns <c>true</c>
|
||||
/// and the resulting range constraint is stored into <paramref name="range"/>.
|
||||
/// </summary>
|
||||
/// <param name="range">
|
||||
/// The <see cref="double"/>-typed <see cref="FilterCriteria.OptionalRange{T}"/>
|
||||
/// to store the parsed data into, if successful.
|
||||
/// </param>
|
||||
/// <param name="op">The operator for the keyword filter.</param>
|
||||
/// <param name="val">The value of the keyword filter.</param>
|
||||
/// <param name="tolerance">Allowed tolerance of the parsed range boundary value.</param>
|
||||
public static bool TryUpdateCriteriaRange(ref FilterCriteria.OptionalRange<double> range, Operator op, string val, double tolerance = 0.05)
|
||||
=> tryParseDoubleWithPoint(val, out double value) && tryUpdateCriteriaRange(ref range, op, value, tolerance);
|
||||
|
||||
private static bool tryUpdateCriteriaRange(ref FilterCriteria.OptionalRange<double> range, Operator op, double value, double tolerance = 0.05)
|
||||
{
|
||||
switch (op)
|
||||
{
|
||||
default:
|
||||
return;
|
||||
return false;
|
||||
|
||||
case "=":
|
||||
case ":":
|
||||
case Operator.Equal:
|
||||
range.Min = value - tolerance;
|
||||
range.Max = value + tolerance;
|
||||
break;
|
||||
|
||||
case ">":
|
||||
case Operator.Greater:
|
||||
range.Min = value + tolerance;
|
||||
break;
|
||||
|
||||
case ">=":
|
||||
case ">:":
|
||||
case Operator.GreaterOrEqual:
|
||||
range.Min = value - tolerance;
|
||||
break;
|
||||
|
||||
case "<":
|
||||
case Operator.Less:
|
||||
range.Max = value - tolerance;
|
||||
break;
|
||||
|
||||
case "<=":
|
||||
case "<:":
|
||||
case Operator.LessOrEqual:
|
||||
range.Max = value + tolerance;
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void updateCriteriaRange<T>(ref FilterCriteria.OptionalRange<T> range, string op, T value)
|
||||
/// <summary>
|
||||
/// Used to determine whether the string value <paramref name="val"/> can be converted to type <typeparamref name="T"/>.
|
||||
/// If conversion can be performed, the delegate returns <c>true</c>
|
||||
/// and the conversion result is returned in the <c>out</c> parameter <paramref name="parsed"/>.
|
||||
/// </summary>
|
||||
/// <param name="val">The string value to attempt parsing for.</param>
|
||||
/// <param name="parsed">The parsed value, if conversion is possible.</param>
|
||||
public delegate bool TryParseFunction<T>(string val, out T parsed);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to parse a keyword filter of type <typeparamref name="T"/>,
|
||||
/// from the specified <paramref name="op"/> and <paramref name="val"/>.
|
||||
/// If <paramref name="val"/> can be parsed into <typeparamref name="T"/> using <paramref name="parseFunction"/>, the function returns <c>true</c>
|
||||
/// and the resulting range constraint is stored into <paramref name="range"/>.
|
||||
/// </summary>
|
||||
/// <param name="range">The <see cref="FilterCriteria.OptionalRange{T}"/> to store the parsed data into, if successful.</param>
|
||||
/// <param name="op">The operator for the keyword filter.</param>
|
||||
/// <param name="val">The value of the keyword filter.</param>
|
||||
/// <param name="parseFunction">Function used to determine if <paramref name="val"/> can be converted to type <typeparamref name="T"/>.</param>
|
||||
public static bool TryUpdateCriteriaRange<T>(ref FilterCriteria.OptionalRange<T> range, Operator op, string val, TryParseFunction<T> parseFunction)
|
||||
where T : struct
|
||||
=> parseFunction.Invoke(val, out var converted) && tryUpdateCriteriaRange(ref range, op, converted);
|
||||
|
||||
private static bool tryUpdateCriteriaRange<T>(ref FilterCriteria.OptionalRange<T> range, Operator op, T value)
|
||||
where T : struct
|
||||
{
|
||||
switch (op)
|
||||
{
|
||||
default:
|
||||
return;
|
||||
return false;
|
||||
|
||||
case "=":
|
||||
case ":":
|
||||
case Operator.Equal:
|
||||
range.IsLowerInclusive = range.IsUpperInclusive = true;
|
||||
range.Min = value;
|
||||
range.Max = value;
|
||||
break;
|
||||
|
||||
case ">":
|
||||
case Operator.Greater:
|
||||
range.IsLowerInclusive = false;
|
||||
range.Min = value;
|
||||
break;
|
||||
|
||||
case ">=":
|
||||
case ">:":
|
||||
case Operator.GreaterOrEqual:
|
||||
range.IsLowerInclusive = true;
|
||||
range.Min = value;
|
||||
break;
|
||||
|
||||
case "<":
|
||||
case Operator.Less:
|
||||
range.IsUpperInclusive = false;
|
||||
range.Max = value;
|
||||
break;
|
||||
|
||||
case "<=":
|
||||
case "<:":
|
||||
case Operator.LessOrEqual:
|
||||
range.IsUpperInclusive = true;
|
||||
range.Max = value;
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool tryUpdateLengthRange(FilterCriteria criteria, Operator op, string val)
|
||||
{
|
||||
if (!tryParseDoubleWithPoint(val.TrimEnd('m', 's', 'h'), out var length))
|
||||
return false;
|
||||
|
||||
var scale = getLengthScale(val);
|
||||
return tryUpdateCriteriaRange(ref criteria.Length, op, length * scale, scale / 2.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -28,19 +28,16 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
private readonly List<OverlayContainer> overlays = new List<OverlayContainer>();
|
||||
|
||||
/// <param name="button">THe button to be added.</param>
|
||||
/// <param name="button">The button to be added.</param>
|
||||
/// <param name="overlay">The <see cref="OverlayContainer"/> to be toggled by this button.</param>
|
||||
public void AddButton(FooterButton button, OverlayContainer overlay)
|
||||
{
|
||||
overlays.Add(overlay);
|
||||
button.Action = () => showOverlay(overlay);
|
||||
if (overlay != null)
|
||||
{
|
||||
overlays.Add(overlay);
|
||||
button.Action = () => showOverlay(overlay);
|
||||
}
|
||||
|
||||
AddButton(button);
|
||||
}
|
||||
|
||||
/// <param name="button">Button to be added.</param>
|
||||
public void AddButton(FooterButton button)
|
||||
{
|
||||
button.Hovered = updateModeLight;
|
||||
button.HoverLost = updateModeLight;
|
||||
|
||||
|
@ -9,6 +9,7 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Input.Bindings;
|
||||
@ -22,9 +23,9 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
protected static readonly Vector2 SHEAR = new Vector2(SHEAR_WIDTH / Footer.HEIGHT, 0);
|
||||
|
||||
public string Text
|
||||
public LocalisableString Text
|
||||
{
|
||||
get => SpriteText?.Text;
|
||||
get => SpriteText?.Text ?? default;
|
||||
set
|
||||
{
|
||||
if (SpriteText != null)
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -1,82 +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.Linq;
|
||||
using Humanizer;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Screens.Multi;
|
||||
using osu.Game.Screens.Multi.Components;
|
||||
|
||||
namespace osu.Game.Screens.Select
|
||||
{
|
||||
public class MatchSongSelect : SongSelect, IMultiplayerSubScreen
|
||||
{
|
||||
public Action<PlaylistItem> Selected;
|
||||
|
||||
public string ShortTitle => "song selection";
|
||||
public override string Title => ShortTitle.Humanize();
|
||||
|
||||
public override bool AllowEditing => false;
|
||||
|
||||
[Resolved(typeof(Room), nameof(Room.Playlist))]
|
||||
protected BindableList<PlaylistItem> Playlist { get; private set; }
|
||||
|
||||
[Resolved]
|
||||
private BeatmapManager beatmaps { get; set; }
|
||||
|
||||
public MatchSongSelect()
|
||||
{
|
||||
Padding = new MarginPadding { Horizontal = HORIZONTAL_OVERFLOW_PADDING };
|
||||
}
|
||||
|
||||
protected override BeatmapDetailArea CreateBeatmapDetailArea() => new MatchBeatmapDetailArea
|
||||
{
|
||||
CreateNewItem = createNewItem
|
||||
};
|
||||
|
||||
protected override bool OnStart()
|
||||
{
|
||||
switch (Playlist.Count)
|
||||
{
|
||||
case 0:
|
||||
createNewItem();
|
||||
break;
|
||||
|
||||
case 1:
|
||||
populateItemFromCurrent(Playlist.Single());
|
||||
break;
|
||||
}
|
||||
|
||||
this.Exit();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void createNewItem()
|
||||
{
|
||||
PlaylistItem item = new PlaylistItem
|
||||
{
|
||||
ID = Playlist.Count == 0 ? 0 : Playlist.Max(p => p.ID) + 1
|
||||
};
|
||||
|
||||
populateItemFromCurrent(item);
|
||||
|
||||
Playlist.Add(item);
|
||||
}
|
||||
|
||||
private void populateItemFromCurrent(PlaylistItem item)
|
||||
{
|
||||
item.Beatmap.Value = Beatmap.Value.BeatmapInfo;
|
||||
item.Ruleset.Value = Ruleset.Value;
|
||||
|
||||
item.RequiredMods.Clear();
|
||||
item.RequiredMods.AddRange(Mods.Value);
|
||||
}
|
||||
}
|
||||
}
|
@ -8,11 +8,11 @@ using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
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
|
||||
@ -40,20 +40,18 @@ namespace osu.Game.Screens.Select.Options
|
||||
set => iconText.Icon = value;
|
||||
}
|
||||
|
||||
public string FirstLineText
|
||||
public LocalisableString FirstLineText
|
||||
{
|
||||
get => firstLine.Text;
|
||||
set => firstLine.Text = value;
|
||||
}
|
||||
|
||||
public string SecondLineText
|
||||
public LocalisableString SecondLineText
|
||||
{
|
||||
get => secondLine.Text;
|
||||
set => secondLine.Text = value;
|
||||
}
|
||||
|
||||
public Key? HotKey;
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e)
|
||||
{
|
||||
flash.FadeTo(0.1f, 1000, Easing.OutQuint);
|
||||
@ -75,17 +73,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()
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -9,6 +9,7 @@ using osu.Framework.Screens;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Screens.Ranking;
|
||||
@ -32,20 +33,18 @@ 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;
|
||||
}
|
||||
|
||||
protected void PresentScore(ScoreInfo score) =>
|
||||
FinaliseSelection(score.Beatmap, score.Ruleset, () => this.Push(new SoloResultsScreen(score)));
|
||||
FinaliseSelection(score.Beatmap, score.Ruleset, () => this.Push(new SoloResultsScreen(score, false)));
|
||||
|
||||
protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea();
|
||||
|
||||
private ModAutoplay getAutoplayMod() => Ruleset.Value.CreateInstance().GetAutoplayMod();
|
||||
|
||||
public override void OnResuming(IScreen last)
|
||||
{
|
||||
base.OnResuming(last);
|
||||
@ -54,10 +53,10 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
if (removeAutoModOnResume)
|
||||
{
|
||||
var autoType = Ruleset.Value.CreateInstance().GetAutoplayMod()?.GetType();
|
||||
var autoType = getAutoplayMod()?.GetType();
|
||||
|
||||
if (autoType != null)
|
||||
ModSelect.DeselectTypes(new[] { autoType }, true);
|
||||
Mods.Value = Mods.Value.Where(m => m.GetType() != autoType).ToArray();
|
||||
|
||||
removeAutoModOnResume = false;
|
||||
}
|
||||
@ -85,12 +84,9 @@ namespace osu.Game.Screens.Select
|
||||
// Ctrl+Enter should start map with autoplay enabled.
|
||||
if (GetContainingInputManager().CurrentState?.Keyboard.ControlPressed == true)
|
||||
{
|
||||
var auto = Ruleset.Value.CreateInstance().GetAutoplayMod();
|
||||
var autoType = auto?.GetType();
|
||||
var autoplayMod = getAutoplayMod();
|
||||
|
||||
var mods = Mods.Value;
|
||||
|
||||
if (autoType == null)
|
||||
if (autoplayMod == null)
|
||||
{
|
||||
notifications?.Post(new SimpleNotification
|
||||
{
|
||||
@ -99,16 +95,18 @@ namespace osu.Game.Screens.Select
|
||||
return false;
|
||||
}
|
||||
|
||||
if (mods.All(m => m.GetType() != autoType))
|
||||
var mods = Mods.Value;
|
||||
|
||||
if (mods.All(m => m.GetType() != autoplayMod.GetType()))
|
||||
{
|
||||
Mods.Value = mods.Append(auto).ToArray();
|
||||
Mods.Value = mods.Append(autoplayMod).ToArray();
|
||||
removeAutoModOnResume = true;
|
||||
}
|
||||
}
|
||||
|
||||
SampleConfirm?.Play();
|
||||
|
||||
this.Push(player = new PlayerLoader(() => new Player()));
|
||||
this.Push(player = new PlayerLoader(() => new SoloPlayer()));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -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;
|
||||
@ -20,7 +19,6 @@ using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Mods;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Screens.Backgrounds;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Menu;
|
||||
using osu.Game.Screens.Select.Options;
|
||||
@ -32,14 +30,18 @@ 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;
|
||||
using osu.Game.Screens.Play;
|
||||
|
||||
namespace osu.Game.Screens.Select
|
||||
{
|
||||
public abstract class SongSelect : OsuScreen, IKeyBindingHandler<GlobalAction>
|
||||
public abstract class SongSelect : ScreenWithBeatmapBackground, IKeyBindingHandler<GlobalAction>
|
||||
{
|
||||
public static readonly float WEDGE_HEIGHT = 245;
|
||||
|
||||
@ -74,12 +76,8 @@ namespace osu.Game.Screens.Select
|
||||
[Resolved]
|
||||
private Bindable<IReadOnlyList<Mod>> selectedMods { get; set; }
|
||||
|
||||
protected override BackgroundScreen CreateBackground() => new BackgroundScreenBeatmap(Beatmap.Value);
|
||||
|
||||
protected BeatmapCarousel Carousel { get; private set; }
|
||||
|
||||
private readonly DifficultyRecommender recommender = new DifficultyRecommender();
|
||||
|
||||
private BeatmapInfoWedge beatmapInfoWedge;
|
||||
private DialogOverlay dialogOverlay;
|
||||
|
||||
@ -88,10 +86,10 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
protected ModSelectOverlay ModSelect { get; private set; }
|
||||
|
||||
protected SampleChannel SampleConfirm { get; private set; }
|
||||
protected Sample SampleConfirm { get; private set; }
|
||||
|
||||
private SampleChannel sampleChangeDifficulty;
|
||||
private SampleChannel sampleChangeBeatmap;
|
||||
private Sample sampleChangeDifficulty;
|
||||
private Sample sampleChangeBeatmap;
|
||||
|
||||
private Container carouselContainer;
|
||||
|
||||
@ -99,11 +97,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)
|
||||
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();
|
||||
@ -118,12 +116,11 @@ namespace osu.Game.Screens.Select
|
||||
BleedBottom = Footer.HEIGHT,
|
||||
SelectionChanged = updateSelectedBeatmap,
|
||||
BeatmapSetsChanged = carouselBeatmapsLoaded,
|
||||
GetRecommendedBeatmap = recommender.GetRecommendedBeatmap,
|
||||
GetRecommendedBeatmap = s => recommender?.GetRecommendedBeatmap(s),
|
||||
}, c => carouselContainer.Child = c);
|
||||
|
||||
AddRangeInternal(new Drawable[]
|
||||
{
|
||||
recommender,
|
||||
new ResetScrollContainer(() => Carousel.ScrollToSelected())
|
||||
{
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
@ -173,7 +170,6 @@ namespace osu.Game.Screens.Select
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = FilterControl.HEIGHT,
|
||||
FilterChanged = ApplyFilterToCarousel,
|
||||
Background = { Width = 2 },
|
||||
},
|
||||
new GridContainer // used for max width implementation
|
||||
{
|
||||
@ -255,11 +251,7 @@ namespace osu.Game.Screens.Select
|
||||
Children = new Drawable[]
|
||||
{
|
||||
BeatmapOptions = new BeatmapOptionsOverlay(),
|
||||
ModSelect = new ModSelectOverlay
|
||||
{
|
||||
Origin = Anchor.BottomCentre,
|
||||
Anchor = Anchor.BottomCentre,
|
||||
}
|
||||
ModSelect = CreateModSelectOverlay()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -271,17 +263,13 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
if (Footer != null)
|
||||
{
|
||||
Footer.AddButton(new FooterButtonMods { Current = Mods }, ModSelect);
|
||||
Footer.AddButton(new FooterButtonRandom
|
||||
{
|
||||
NextRandom = () => Carousel.SelectNextRandom(),
|
||||
PreviousRandom = Carousel.SelectPreviousRandom
|
||||
});
|
||||
Footer.AddButton(new FooterButtonOptions(), BeatmapOptions);
|
||||
foreach (var (button, overlay) in CreateFooterButtons())
|
||||
Footer.AddButton(button, overlay);
|
||||
|
||||
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;
|
||||
@ -299,7 +287,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);
|
||||
}));
|
||||
}
|
||||
@ -307,6 +300,23 @@ namespace osu.Game.Screens.Select
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the buttons to be displayed in the footer.
|
||||
/// </summary>
|
||||
/// <returns>A set of <see cref="FooterButton"/> and an optional <see cref="OverlayContainer"/> which the button opens when pressed.</returns>
|
||||
protected virtual IEnumerable<(FooterButton, OverlayContainer)> CreateFooterButtons() => new (FooterButton, OverlayContainer)[]
|
||||
{
|
||||
(new FooterButtonMods { Current = Mods }, ModSelect),
|
||||
(new FooterButtonRandom
|
||||
{
|
||||
NextRandom = () => Carousel.SelectNextRandom(),
|
||||
PreviousRandom = Carousel.SelectPreviousRandom
|
||||
}, null),
|
||||
(new FooterButtonOptions(), BeatmapOptions)
|
||||
};
|
||||
|
||||
protected virtual ModSelectOverlay CreateModSelectOverlay() => new LocalPlayerModSelectOverlay();
|
||||
|
||||
protected virtual void ApplyFilterToCarousel(FilterCriteria criteria)
|
||||
{
|
||||
// if not the current screen, we want to get carousel in a good presentation state before displaying (resume or enter).
|
||||
@ -373,7 +383,7 @@ namespace osu.Game.Screens.Select
|
||||
if (selectionChangedDebounce?.Completed == false)
|
||||
{
|
||||
selectionChangedDebounce.RunTask();
|
||||
selectionChangedDebounce.Cancel(); // cancel the already scheduled task.
|
||||
selectionChangedDebounce?.Cancel(); // cancel the already scheduled task.
|
||||
selectionChangedDebounce = null;
|
||||
}
|
||||
|
||||
@ -428,16 +438,21 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
private void updateSelectedBeatmap(BeatmapInfo beatmap)
|
||||
{
|
||||
if (beatmap == null && beatmapNoDebounce == null)
|
||||
return;
|
||||
|
||||
if (beatmap?.Equals(beatmapNoDebounce) == true)
|
||||
return;
|
||||
|
||||
beatmapNoDebounce = beatmap;
|
||||
|
||||
performUpdateSelected();
|
||||
}
|
||||
|
||||
private void updateSelectedRuleset(RulesetInfo ruleset)
|
||||
{
|
||||
if (ruleset == null && rulesetNoDebounce == null)
|
||||
return;
|
||||
|
||||
if (ruleset?.Equals(rulesetNoDebounce) == true)
|
||||
return;
|
||||
|
||||
@ -462,19 +477,30 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
void run()
|
||||
{
|
||||
// clear pending task immediately to track any potential nested debounce operation.
|
||||
selectionChangedDebounce = null;
|
||||
|
||||
Logger.Log($"updating selection with beatmap:{beatmap?.ID.ToString() ?? "null"} ruleset:{ruleset?.ID.ToString() ?? "null"}");
|
||||
|
||||
if (transferRulesetValue())
|
||||
{
|
||||
Mods.Value = Array.Empty<Mod>();
|
||||
|
||||
// transferRulesetValue() may trigger a refilter. If the current selection does not match the new ruleset, we want to switch away from it.
|
||||
// transferRulesetValue() may trigger a re-filter. If the current selection does not match the new ruleset, we want to switch away from it.
|
||||
// The default logic on WorkingBeatmap change is to switch to a matching ruleset (see workingBeatmapChanged()), but we don't want that here.
|
||||
// We perform an early selection attempt and clear out the beatmap selection to avoid a second ruleset change (revert).
|
||||
if (beatmap != null && !Carousel.SelectBeatmap(beatmap, false))
|
||||
beatmap = null;
|
||||
}
|
||||
|
||||
if (selectionChangedDebounce != null)
|
||||
{
|
||||
// a new nested operation was started; switch to it for further selection.
|
||||
// this avoids having two separate debounces trigger from the same source.
|
||||
selectionChangedDebounce.RunTask();
|
||||
return;
|
||||
}
|
||||
|
||||
// We may be arriving here due to another component changing the bindable Beatmap.
|
||||
// In these cases, the other component has already loaded the beatmap, so we don't need to do so again.
|
||||
if (!EqualityComparer<BeatmapInfo>.Default.Equals(beatmap, Beatmap.Value.BeatmapInfo))
|
||||
@ -486,7 +512,7 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
if (beatmap != null)
|
||||
{
|
||||
if (beatmap.BeatmapSetInfoID == beatmapNoDebounce?.BeatmapSetInfoID)
|
||||
if (beatmap.BeatmapSetInfoID == previous?.BeatmapInfo.BeatmapSetInfoID)
|
||||
sampleChangeDifficulty.Play();
|
||||
else
|
||||
sampleChangeBeatmap.Play();
|
||||
@ -508,6 +534,8 @@ namespace osu.Game.Screens.Select
|
||||
FilterControl.Activate();
|
||||
|
||||
ModSelect.SelectedMods.BindTo(selectedMods);
|
||||
|
||||
beginLooping();
|
||||
}
|
||||
|
||||
private const double logo_transition = 250;
|
||||
@ -558,15 +586,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);
|
||||
@ -583,8 +612,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);
|
||||
|
||||
@ -596,12 +624,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;
|
||||
|
||||
@ -611,8 +633,44 @@ 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);
|
||||
|
||||
isHandlingLooping = true;
|
||||
|
||||
ensureTrackLooping(Beatmap.Value, TrackChangeDirection.None);
|
||||
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)
|
||||
=> beatmap.PrepareTrackForPreviewLooping();
|
||||
|
||||
public override bool OnBackButton()
|
||||
{
|
||||
if (ModSelect.State.Value == Visibility.Visible)
|
||||
{
|
||||
ModSelect.Hide();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@ -622,6 +680,9 @@ namespace osu.Game.Screens.Select
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
decoupledRuleset.UnbindAll();
|
||||
|
||||
if (music != null)
|
||||
music.TrackChanged -= ensureTrackLooping;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -631,22 +692,19 @@ namespace osu.Game.Screens.Select
|
||||
/// <param name="beatmap">The working beatmap.</param>
|
||||
private void updateComponentFromBeatmap(WorkingBeatmap beatmap)
|
||||
{
|
||||
if (Background is BackgroundScreenBeatmap backgroundModeBeatmap)
|
||||
ApplyToBackground(backgroundModeBeatmap =>
|
||||
{
|
||||
backgroundModeBeatmap.Beatmap = beatmap;
|
||||
backgroundModeBeatmap.BlurAmount.Value = BACKGROUND_BLUR;
|
||||
backgroundModeBeatmap.FadeColour(Color4.White, 250);
|
||||
}
|
||||
});
|
||||
|
||||
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.
|
||||
@ -654,14 +712,12 @@ 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);
|
||||
}
|
||||
|
Reference in New Issue
Block a user