1
0
mirror of https://github.com/ppy/osu.git synced 2025-02-13 21:12:55 +08:00

Merge branch 'master' into abstract-hub-connection

This commit is contained in:
Salman Ahmed 2021-02-11 02:14:22 +03:00
commit 0c5e66205b
32 changed files with 686 additions and 559 deletions

View File

@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Osu
protected override bool Handle(UIEvent e) protected override bool Handle(UIEvent e)
{ {
if (e is MouseMoveEvent && !AllowUserCursorMovement) return false; if ((e is MouseMoveEvent || e is TouchMoveEvent) && !AllowUserCursorMovement) return false;
return base.Handle(e); return base.Handle(e);
} }

View File

@ -7,7 +7,10 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.OnlinePlay.Multiplayer.Participants; using osu.Game.Screens.OnlinePlay.Multiplayer.Participants;
@ -19,16 +22,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
public class TestSceneMultiplayerParticipantsList : MultiplayerTestScene public class TestSceneMultiplayerParticipantsList : MultiplayerTestScene
{ {
[SetUp] [SetUp]
public new void Setup() => Schedule(() => public new void Setup() => Schedule(createNewParticipantsList);
{
Child = new ParticipantsList
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Y,
Size = new Vector2(380, 0.7f)
};
});
[Test] [Test]
public void TestAddUser() public void TestAddUser()
@ -75,6 +69,50 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("single panel is for second user", () => this.ChildrenOfType<ParticipantPanel>().Single().User.User == secondUser); AddAssert("single panel is for second user", () => this.ChildrenOfType<ParticipantPanel>().Single().User.User == secondUser);
} }
[Test]
public void TestGameStateHasPriorityOverDownloadState()
{
AddStep("set to downloading map", () => Client.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(0)));
checkProgressBarVisibility(true);
AddStep("make user ready", () => Client.ChangeState(MultiplayerUserState.Results));
checkProgressBarVisibility(false);
AddUntilStep("ready mark visible", () => this.ChildrenOfType<StateDisplay>().Single().IsPresent);
AddStep("make user ready", () => Client.ChangeState(MultiplayerUserState.Idle));
checkProgressBarVisibility(true);
}
[Test]
public void TestCorrectInitialState()
{
AddStep("set to downloading map", () => Client.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(0)));
AddStep("recreate list", createNewParticipantsList);
checkProgressBarVisibility(true);
}
[Test]
public void TestBeatmapDownloadingStates()
{
AddStep("set to no map", () => Client.ChangeBeatmapAvailability(BeatmapAvailability.NotDownloaded()));
AddStep("set to downloading map", () => Client.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(0)));
checkProgressBarVisibility(true);
AddRepeatStep("increment progress", () =>
{
var progress = this.ChildrenOfType<ParticipantPanel>().Single().User.BeatmapAvailability.DownloadProgress ?? 0;
Client.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(progress + RNG.NextSingle(0.1f)));
}, 25);
AddAssert("progress bar increased", () => this.ChildrenOfType<ProgressBar>().Single().Current.Value > 0);
AddStep("set to importing map", () => Client.ChangeBeatmapAvailability(BeatmapAvailability.Importing()));
checkProgressBarVisibility(false);
AddStep("set to available", () => Client.ChangeBeatmapAvailability(BeatmapAvailability.LocallyAvailable()));
}
[Test] [Test]
public void TestToggleReadyState() public void TestToggleReadyState()
{ {
@ -122,6 +160,26 @@ namespace osu.Game.Tests.Visual.Multiplayer
}); });
Client.ChangeUserState(i, (MultiplayerUserState)RNG.Next(0, (int)MultiplayerUserState.Results + 1)); Client.ChangeUserState(i, (MultiplayerUserState)RNG.Next(0, (int)MultiplayerUserState.Results + 1));
if (RNG.NextBool())
{
var beatmapState = (DownloadState)RNG.Next(0, (int)DownloadState.LocallyAvailable + 1);
switch (beatmapState)
{
case DownloadState.NotDownloaded:
Client.ChangeUserBeatmapAvailability(i, BeatmapAvailability.NotDownloaded());
break;
case DownloadState.Downloading:
Client.ChangeUserBeatmapAvailability(i, BeatmapAvailability.Downloading(RNG.NextSingle()));
break;
case DownloadState.Importing:
Client.ChangeUserBeatmapAvailability(i, BeatmapAvailability.Importing());
break;
}
}
} }
}); });
} }
@ -152,5 +210,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep($"set state: {state}", () => Client.ChangeUserState(0, state)); AddStep($"set state: {state}", () => Client.ChangeUserState(0, state));
} }
} }
private void createNewParticipantsList()
{
Child = new ParticipantsList { Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Y, Size = new Vector2(380, 0.7f) };
}
private void checkProgressBarVisibility(bool visible) =>
AddUntilStep($"progress bar {(visible ? "is" : "is not")}visible", () =>
this.ChildrenOfType<ProgressBar>().Single().IsPresent == visible);
} }
} }

View File

@ -87,6 +87,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
Ruleset.Value = new OsuRuleset().RulesetInfo; Ruleset.Value = new OsuRuleset().RulesetInfo;
Beatmap.SetDefault(); Beatmap.SetDefault();
SelectedMods.Value = Array.Empty<Mod>();
}); });
AddStep("create song select", () => LoadScreen(songSelect = new TestPlaylistsSongSelect())); AddStep("create song select", () => LoadScreen(songSelect = new TestPlaylistsSongSelect()));
@ -143,7 +144,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
/// Tests that the same <see cref="Mod"/> instances are not shared between two playlist items. /// Tests that the same <see cref="Mod"/> instances are not shared between two playlist items.
/// </summary> /// </summary>
[Test] [Test]
[Ignore("Temporarily disabled due to a non-trivial test failure")]
public void TestNewItemHasNewModInstances() public void TestNewItemHasNewModInstances()
{ {
AddStep("set dt mod", () => SelectedMods.Value = new[] { new OsuModDoubleTime() }); AddStep("set dt mod", () => SelectedMods.Value = new[] { new OsuModDoubleTime() });

View File

@ -41,7 +41,7 @@ namespace osu.Game.Tests.Visual.Online
private class TestFullscreenOverlay : FullscreenOverlay<OverlayHeader> private class TestFullscreenOverlay : FullscreenOverlay<OverlayHeader>
{ {
public TestFullscreenOverlay() public TestFullscreenOverlay()
: base(OverlayColourScheme.Pink, null) : base(OverlayColourScheme.Pink)
{ {
Children = new Drawable[] Children = new Drawable[]
{ {
@ -52,6 +52,17 @@ namespace osu.Game.Tests.Visual.Online
}, },
}; };
} }
protected override OverlayHeader CreateHeader() => new TestHeader();
internal class TestHeader : OverlayHeader
{
protected override OverlayTitle CreateTitle() => new TestTitle();
internal class TestTitle : OverlayTitle
{
}
}
} }
} }
} }

View File

@ -25,7 +25,7 @@ namespace osu.Game.Tests.Visual.Online
Add(rankingsOverlay = new TestRankingsOverlay Add(rankingsOverlay = new TestRankingsOverlay
{ {
Country = { BindTarget = countryBindable }, Country = { BindTarget = countryBindable },
Scope = { BindTarget = scope }, Header = { Current = { BindTarget = scope } },
}); });
} }
@ -65,8 +65,6 @@ namespace osu.Game.Tests.Visual.Online
private class TestRankingsOverlay : RankingsOverlay private class TestRankingsOverlay : RankingsOverlay
{ {
public new Bindable<Country> Country => base.Country; public new Bindable<Country> Country => base.Country;
public new Bindable<RankingsScope> Scope => base.Scope;
} }
} }
} }

View File

@ -39,7 +39,11 @@ namespace osu.Game.Tests.Visual.UserInterface
} }
[SetUp] [SetUp]
public void SetUp() => Schedule(() => createDisplay(() => new TestModSelectOverlay())); public void SetUp() => Schedule(() =>
{
SelectedMods.Value = Array.Empty<Mod>();
createDisplay(() => new TestModSelectOverlay());
});
[SetUpSteps] [SetUpSteps]
public void SetUpSteps() public void SetUpSteps()
@ -47,6 +51,24 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("show", () => modSelect.Show()); AddStep("show", () => modSelect.Show());
} }
[Test]
public void TestSettingsResetOnDeselection()
{
var osuModDoubleTime = new OsuModDoubleTime { SpeedChange = { Value = 1.2 } };
changeRuleset(0);
AddStep("set dt mod with custom rate", () => { SelectedMods.Value = new[] { osuModDoubleTime }; });
AddAssert("selected mod matches", () => (SelectedMods.Value.Single() as OsuModDoubleTime)?.SpeedChange.Value == 1.2);
AddStep("deselect", () => modSelect.DeselectAllButton.Click());
AddAssert("selected mods empty", () => SelectedMods.Value.Count == 0);
AddStep("reselect", () => modSelect.GetModButton(osuModDoubleTime).Click());
AddAssert("selected mod has default value", () => (SelectedMods.Value.Single() as OsuModDoubleTime)?.SpeedChange.IsDefault == true);
}
[Test] [Test]
public void TestAnimationFlushOnClose() public void TestAnimationFlushOnClose()
{ {
@ -152,6 +174,45 @@ namespace osu.Game.Tests.Visual.UserInterface
}); });
} }
[Test]
public void TestSettingsAreRetainedOnReload()
{
changeRuleset(0);
AddStep("set customized mod externally", () => SelectedMods.Value = new[] { new OsuModDoubleTime { SpeedChange = { Value = 1.01 } } });
AddAssert("setting remains", () => (SelectedMods.Value.SingleOrDefault() as OsuModDoubleTime)?.SpeedChange.Value == 1.01);
AddStep("create overlay", () => createDisplay(() => new TestNonStackedModSelectOverlay()));
AddAssert("setting remains", () => (SelectedMods.Value.SingleOrDefault() as OsuModDoubleTime)?.SpeedChange.Value == 1.01);
}
[Test]
public void TestExternallySetModIsReplacedByOverlayInstance()
{
Mod external = new OsuModDoubleTime();
Mod overlayButtonMod = null;
changeRuleset(0);
AddStep("set mod externally", () => { SelectedMods.Value = new[] { external }; });
AddAssert("ensure button is selected", () =>
{
var button = modSelect.GetModButton(SelectedMods.Value.Single());
overlayButtonMod = button.SelectedMod;
return overlayButtonMod.GetType() == external.GetType();
});
// Right now, when an external change occurs, the ModSelectOverlay will replace the global instance with its own
AddAssert("mod instance doesn't match", () => external != overlayButtonMod);
AddAssert("one mod present in global selected", () => SelectedMods.Value.Count == 1);
AddAssert("globally selected matches button's mod instance", () => SelectedMods.Value.Contains(overlayButtonMod));
AddAssert("globally selected doesn't contain original external change", () => !SelectedMods.Value.Contains(external));
}
[Test] [Test]
public void TestNonStacked() public void TestNonStacked()
{ {
@ -313,7 +374,6 @@ namespace osu.Game.Tests.Visual.UserInterface
private void createDisplay(Func<TestModSelectOverlay> createOverlayFunc) private void createDisplay(Func<TestModSelectOverlay> createOverlayFunc)
{ {
SelectedMods.Value = Array.Empty<Mod>();
Children = new Drawable[] Children = new Drawable[]
{ {
modSelect = createOverlayFunc().With(d => modSelect = createOverlayFunc().With(d =>

View File

@ -40,8 +40,19 @@ namespace osu.Game.Graphics.UserInterface
set => CurrentNumber.Value = value; set => CurrentNumber.Value = value;
} }
public ProgressBar() private readonly bool allowSeek;
public override bool HandlePositionalInput => allowSeek;
public override bool HandleNonPositionalInput => allowSeek;
/// <summary>
/// Construct a new progress bar.
/// </summary>
/// <param name="allowSeek">Whether the user should be allowed to click/drag to adjust the value.</param>
public ProgressBar(bool allowSeek)
{ {
this.allowSeek = allowSeek;
CurrentNumber.MinValue = 0; CurrentNumber.MinValue = 0;
CurrentNumber.MaxValue = 1; CurrentNumber.MaxValue = 1;
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;

View File

@ -47,6 +47,7 @@ namespace osu.Game.Online.Multiplayer
connection.On(nameof(IMultiplayerClient.MatchStarted), ((IMultiplayerClient)this).MatchStarted); connection.On(nameof(IMultiplayerClient.MatchStarted), ((IMultiplayerClient)this).MatchStarted);
connection.On(nameof(IMultiplayerClient.ResultsReady), ((IMultiplayerClient)this).ResultsReady); connection.On(nameof(IMultiplayerClient.ResultsReady), ((IMultiplayerClient)this).ResultsReady);
connection.On<int, IEnumerable<APIMod>>(nameof(IMultiplayerClient.UserModsChanged), ((IMultiplayerClient)this).UserModsChanged); connection.On<int, IEnumerable<APIMod>>(nameof(IMultiplayerClient.UserModsChanged), ((IMultiplayerClient)this).UserModsChanged);
connection.On<int, BeatmapAvailability>(nameof(IMultiplayerClient.UserBeatmapAvailabilityChanged), ((IMultiplayerClient)this).UserBeatmapAvailabilityChanged);
}, },
}; };

View File

@ -5,6 +5,7 @@ using System;
using System.Linq; using System.Linq;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Threading;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
namespace osu.Game.Online.Rooms namespace osu.Game.Online.Rooms
@ -24,19 +25,33 @@ namespace osu.Game.Online.Rooms
/// </summary> /// </summary>
public IBindable<BeatmapAvailability> Availability => availability; public IBindable<BeatmapAvailability> Availability => availability;
private readonly Bindable<BeatmapAvailability> availability = new Bindable<BeatmapAvailability>(); private readonly Bindable<BeatmapAvailability> availability = new Bindable<BeatmapAvailability>(BeatmapAvailability.LocallyAvailable());
public OnlinePlayBeatmapAvailablilityTracker() private ScheduledDelegate progressUpdate;
{
State.BindValueChanged(_ => updateAvailability());
Progress.BindValueChanged(_ => updateAvailability(), true);
}
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
SelectedItem.BindValueChanged(item => Model.Value = item.NewValue?.Beatmap.Value.BeatmapSet, true); SelectedItem.BindValueChanged(item =>
{
// the underlying playlist is regularly cleared for maintenance purposes (things which probably need to be fixed eventually).
// to avoid exposing a state change when there may actually be none, ignore all nulls for now.
if (item.NewValue == null)
return;
Model.Value = item.NewValue.Beatmap.Value.BeatmapSet;
}, true);
Progress.BindValueChanged(_ =>
{
// incoming progress changes are going to be at a very high rate.
// we don't want to flood the network with this, so rate limit how often we send progress updates.
if (progressUpdate?.Completed != false)
progressUpdate = Scheduler.AddDelayed(updateAvailability, progressUpdate == null ? 0 : 500);
});
State.BindValueChanged(_ => updateAvailability(), true);
} }
protected override bool VerifyDatabasedModel(BeatmapSetInfo databasedSet) protected override bool VerifyDatabasedModel(BeatmapSetInfo databasedSet)

View File

@ -98,7 +98,14 @@ namespace osu.Game
[Cached(typeof(IBindable<RulesetInfo>))] [Cached(typeof(IBindable<RulesetInfo>))]
protected readonly Bindable<RulesetInfo> Ruleset = new Bindable<RulesetInfo>(); protected readonly Bindable<RulesetInfo> Ruleset = new Bindable<RulesetInfo>();
// todo: move this to SongSelect once Screen has the ability to unsuspend. /// <summary>
/// The current mod selection for the local user.
/// </summary>
/// <remarks>
/// If a mod select overlay is present, mod instances set to this value are not guaranteed to remain as the provided instance and will be overwritten by a copy.
/// In such a case, changes to settings of a mod will *not* propagate after a mod is added to this collection.
/// As such, all settings should be finalised before adding a mod to this collection.
/// </remarks>
[Cached] [Cached]
[Cached(typeof(IBindable<IReadOnlyList<Mod>>))] [Cached(typeof(IBindable<IReadOnlyList<Mod>>))]
protected readonly Bindable<IReadOnlyList<Mod>> SelectedMods = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>()); protected readonly Bindable<IReadOnlyList<Mod>> SelectedMods = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>());

View File

@ -19,7 +19,7 @@ namespace osu.Game.Overlays.BeatmapListing.Panels
public DownloadProgressBar(BeatmapSetInfo beatmapSet) public DownloadProgressBar(BeatmapSetInfo beatmapSet)
: base(beatmapSet) : base(beatmapSet)
{ {
AddInternal(progressBar = new InteractionDisabledProgressBar AddInternal(progressBar = new ProgressBar(false)
{ {
Height = 0, Height = 0,
Alpha = 0, Alpha = 0,
@ -64,11 +64,5 @@ namespace osu.Game.Overlays.BeatmapListing.Panels
} }
}, true); }, true);
} }
private class InteractionDisabledProgressBar : ProgressBar
{
public override bool HandlePositionalInput => false;
public override bool HandleNonPositionalInput => false;
}
} }
} }

View File

@ -15,57 +15,40 @@ using osu.Framework.Graphics.Textures;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.BeatmapListing; using osu.Game.Overlays.BeatmapListing;
using osu.Game.Overlays.BeatmapListing.Panels; using osu.Game.Overlays.BeatmapListing.Panels;
using osuTK; using osuTK;
using osuTK.Graphics;
namespace osu.Game.Overlays namespace osu.Game.Overlays
{ {
public class BeatmapListingOverlay : FullscreenOverlay<BeatmapListingHeader> public class BeatmapListingOverlay : OnlineOverlay<BeatmapListingHeader>
{ {
[Resolved] [Resolved]
private PreviewTrackManager previewTrackManager { get; set; } private PreviewTrackManager previewTrackManager { get; set; }
private Drawable currentContent; private Drawable currentContent;
private LoadingLayer loadingLayer;
private Container panelTarget; private Container panelTarget;
private FillFlowContainer<BeatmapPanel> foundContent; private FillFlowContainer<BeatmapPanel> foundContent;
private NotFoundDrawable notFoundContent; private NotFoundDrawable notFoundContent;
private BeatmapListingFilterControl filterControl;
private OverlayScrollContainer resultScrollContainer;
public BeatmapListingOverlay() public BeatmapListingOverlay()
: base(OverlayColourScheme.Blue, new BeatmapListingHeader()) : base(OverlayColourScheme.Blue)
{ {
} }
private BeatmapListingFilterControl filterControl;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
Children = new Drawable[] Child = new FillFlowContainer
{ {
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = ColourProvider.Background6
},
resultScrollContainer = new OverlayScrollContainer
{
RelativeSizeAxes = Axes.Both,
ScrollbarVisible = false,
Child = new ReverseChildIDFillFlowContainer<Drawable>
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical, Direction = FillDirection.Vertical,
Children = new Drawable[] Children = new Drawable[]
{ {
Header,
filterControl = new BeatmapListingFilterControl filterControl = new BeatmapListingFilterControl
{ {
TypingStarted = onTypingStarted, TypingStarted = onTypingStarted,
@ -97,16 +80,17 @@ namespace osu.Game.Overlays
}, },
}, },
} }
},
},
loadingLayer = new LoadingLayer(true)
}; };
} }
protected override BeatmapListingHeader CreateHeader() => new BeatmapListingHeader();
protected override Color4 BackgroundColour => ColourProvider.Background6;
private void onTypingStarted() private void onTypingStarted()
{ {
// temporary until the textbox/header is updated to always stay on screen. // temporary until the textbox/header is updated to always stay on screen.
resultScrollContainer.ScrollToStart(); ScrollFlow.ScrollToStart();
} }
protected override void OnFocus(FocusEvent e) protected override void OnFocus(FocusEvent e)
@ -125,7 +109,7 @@ namespace osu.Game.Overlays
previewTrackManager.StopAnyPlaying(this); previewTrackManager.StopAnyPlaying(this);
if (panelTarget.Any()) if (panelTarget.Any())
loadingLayer.Show(); Loading.Show();
} }
private Task panelLoadDelegate; private Task panelLoadDelegate;
@ -173,7 +157,7 @@ namespace osu.Game.Overlays
private void addContentToPlaceholder(Drawable content) private void addContentToPlaceholder(Drawable content)
{ {
loadingLayer.Hide(); Loading.Hide();
lastFetchDisplayedTime = Time.Current; lastFetchDisplayedTime = Time.Current;
if (content == currentContent) if (content == currentContent)
@ -267,7 +251,7 @@ namespace osu.Game.Overlays
bool shouldShowMore = panelLoadDelegate?.IsCompleted != false bool shouldShowMore = panelLoadDelegate?.IsCompleted != false
&& Time.Current - lastFetchDisplayedTime > time_between_fetches && Time.Current - lastFetchDisplayedTime > time_between_fetches
&& (resultScrollContainer.ScrollableExtent > 0 && resultScrollContainer.IsScrolledToEnd(pagination_scroll_distance)); && (ScrollFlow.ScrollableExtent > 0 && ScrollFlow.IsScrolledToEnd(pagination_scroll_distance));
if (shouldShowMore) if (shouldShowMore)
filterControl.FetchNextPage(); filterControl.FetchNextPage();

View File

@ -6,20 +6,19 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics.Containers;
using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests;
using osu.Game.Overlays.BeatmapSet; using osu.Game.Overlays.BeatmapSet;
using osu.Game.Overlays.BeatmapSet.Scores; using osu.Game.Overlays.BeatmapSet.Scores;
using osu.Game.Overlays.Comments; using osu.Game.Overlays.Comments;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osuTK; using osuTK;
using osuTK.Graphics;
namespace osu.Game.Overlays namespace osu.Game.Overlays
{ {
public class BeatmapSetOverlay : FullscreenOverlay<BeatmapSetHeader> public class BeatmapSetOverlay : OnlineOverlay<BeatmapSetHeader>
{ {
public const float X_PADDING = 40; public const float X_PADDING = 40;
public const float Y_PADDING = 25; public const float Y_PADDING = 25;
@ -33,55 +32,27 @@ namespace osu.Game.Overlays
// receive input outside our bounds so we can trigger a close event on ourselves. // receive input outside our bounds so we can trigger a close event on ourselves.
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
private readonly Box background;
public BeatmapSetOverlay() public BeatmapSetOverlay()
: base(OverlayColourScheme.Blue, new BeatmapSetHeader()) : base(OverlayColourScheme.Blue)
{ {
OverlayScrollContainer scroll;
Info info; Info info;
CommentsSection comments; CommentsSection comments;
Children = new Drawable[] Child = new FillFlowContainer
{
background = new Box
{
RelativeSizeAxes = Axes.Both
},
scroll = new OverlayScrollContainer
{
RelativeSizeAxes = Axes.Both,
ScrollbarVisible = false,
Child = new ReverseChildIDFillFlowContainer<BeatmapSetLayoutSection>
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical, Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 20), Spacing = new Vector2(0, 20),
Children = new[]
{
new BeatmapSetLayoutSection
{
Child = new ReverseChildIDFillFlowContainer<Drawable>
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Direction = FillDirection.Vertical,
Children = new Drawable[] Children = new Drawable[]
{ {
Header, info = new Info(),
info = new Info()
}
},
},
new ScoresContainer new ScoresContainer
{ {
Beatmap = { BindTarget = Header.HeaderContent.Picker.Beatmap } Beatmap = { BindTarget = Header.HeaderContent.Picker.Beatmap }
}, },
comments = new CommentsSection() comments = new CommentsSection()
}, }
},
},
}; };
Header.BeatmapSet.BindTo(beatmapSet); Header.BeatmapSet.BindTo(beatmapSet);
@ -91,16 +62,13 @@ namespace osu.Game.Overlays
Header.HeaderContent.Picker.Beatmap.ValueChanged += b => Header.HeaderContent.Picker.Beatmap.ValueChanged += b =>
{ {
info.Beatmap = b.NewValue; info.Beatmap = b.NewValue;
ScrollFlow.ScrollToStart();
scroll.ScrollToStart();
}; };
} }
[BackgroundDependencyLoader] protected override BeatmapSetHeader CreateHeader() => new BeatmapSetHeader();
private void load()
{ protected override Color4 BackgroundColour => ColourProvider.Background6;
background.Colour = ColourProvider.Background6;
}
protected override void PopOutComplete() protected override void PopOutComplete()
{ {

View File

@ -11,22 +11,18 @@ using osu.Framework.Audio;
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics.Containers;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.Changelog; using osu.Game.Overlays.Changelog;
using osuTK.Graphics;
namespace osu.Game.Overlays namespace osu.Game.Overlays
{ {
public class ChangelogOverlay : FullscreenOverlay<ChangelogHeader> public class ChangelogOverlay : OnlineOverlay<ChangelogHeader>
{ {
public readonly Bindable<APIChangelogBuild> Current = new Bindable<APIChangelogBuild>(); public readonly Bindable<APIChangelogBuild> Current = new Bindable<APIChangelogBuild>();
private Container<ChangelogContent> content;
private SampleChannel sampleBack; private SampleChannel sampleBack;
private List<APIChangelogBuild> builds; private List<APIChangelogBuild> builds;
@ -34,45 +30,14 @@ namespace osu.Game.Overlays
protected List<APIUpdateStream> Streams; protected List<APIUpdateStream> Streams;
public ChangelogOverlay() public ChangelogOverlay()
: base(OverlayColourScheme.Purple, new ChangelogHeader()) : base(OverlayColourScheme.Purple)
{ {
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(AudioManager audio) private void load(AudioManager audio)
{ {
Children = new Drawable[] Header.Build.BindTarget = Current;
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = ColourProvider.Background4,
},
new OverlayScrollContainer
{
RelativeSizeAxes = Axes.Both,
ScrollbarVisible = false,
Child = new ReverseChildIDFillFlowContainer<Drawable>
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
Header.With(h =>
{
h.ListingSelected = ShowListing;
h.Build.BindTarget = Current;
}),
content = new Container<ChangelogContent>
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
}
},
},
},
};
sampleBack = audio.Samples.Get(@"UI/generic-select-soft"); sampleBack = audio.Samples.Get(@"UI/generic-select-soft");
@ -85,6 +50,13 @@ namespace osu.Game.Overlays
}); });
} }
protected override ChangelogHeader CreateHeader() => new ChangelogHeader
{
ListingSelected = ShowListing,
};
protected override Color4 BackgroundColour => ColourProvider.Background4;
public void ShowListing() public void ShowListing()
{ {
Current.Value = null; Current.Value = null;
@ -198,16 +170,16 @@ namespace osu.Game.Overlays
private void loadContent(ChangelogContent newContent) private void loadContent(ChangelogContent newContent)
{ {
content.FadeTo(0.2f, 300, Easing.OutQuint); Content.FadeTo(0.2f, 300, Easing.OutQuint);
loadContentCancellation?.Cancel(); loadContentCancellation?.Cancel();
LoadComponentAsync(newContent, c => LoadComponentAsync(newContent, c =>
{ {
content.FadeIn(300, Easing.OutQuint); Content.FadeIn(300, Easing.OutQuint);
c.BuildSelected = ShowBuild; c.BuildSelected = ShowBuild;
content.Child = c; Child = c;
}, (loadContentCancellation = new CancellationTokenSource()).Token); }, (loadContentCancellation = new CancellationTokenSource()).Token);
} }
} }

View File

@ -2,155 +2,35 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Overlays.Dashboard; using osu.Game.Overlays.Dashboard;
using osu.Game.Overlays.Dashboard.Friends; using osu.Game.Overlays.Dashboard.Friends;
namespace osu.Game.Overlays namespace osu.Game.Overlays
{ {
public class DashboardOverlay : FullscreenOverlay<DashboardOverlayHeader> public class DashboardOverlay : TabbableOnlineOverlay<DashboardOverlayHeader, DashboardOverlayTabs>
{ {
private CancellationTokenSource cancellationToken;
private Container content;
private LoadingLayer loading;
private OverlayScrollContainer scrollFlow;
public DashboardOverlay() public DashboardOverlay()
: base(OverlayColourScheme.Purple, new DashboardOverlayHeader : base(OverlayColourScheme.Purple)
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Depth = -float.MaxValue
})
{ {
} }
private readonly IBindable<APIState> apiState = new Bindable<APIState>(); protected override DashboardOverlayHeader CreateHeader() => new DashboardOverlayHeader();
[BackgroundDependencyLoader] protected override void CreateDisplayToLoad(DashboardOverlayTabs tab)
private void load(IAPIProvider api)
{ {
apiState.BindTo(api.State); switch (tab)
apiState.BindValueChanged(onlineStateChanged, true);
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = ColourProvider.Background5
},
scrollFlow = new OverlayScrollContainer
{
RelativeSizeAxes = Axes.Both,
ScrollbarVisible = false,
Child = new FillFlowContainer
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
Header,
content = new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y
}
}
}
},
loading = new LoadingLayer(true),
};
}
protected override void LoadComplete()
{
base.LoadComplete();
Header.Current.BindValueChanged(onTabChanged);
}
private bool displayUpdateRequired = true;
protected override void PopIn()
{
base.PopIn();
// We don't want to create a new display on every call, only when exiting from fully closed state.
if (displayUpdateRequired)
{
Header.Current.TriggerChange();
displayUpdateRequired = false;
}
}
protected override void PopOutComplete()
{
base.PopOutComplete();
loadDisplay(Empty());
displayUpdateRequired = true;
}
private void loadDisplay(Drawable display)
{
scrollFlow.ScrollToStart();
LoadComponentAsync(display, loaded =>
{
if (API.IsLoggedIn)
loading.Hide();
content.Child = loaded;
}, (cancellationToken = new CancellationTokenSource()).Token);
}
private void onTabChanged(ValueChangedEvent<DashboardOverlayTabs> tab)
{
cancellationToken?.Cancel();
loading.Show();
if (!API.IsLoggedIn)
{
loadDisplay(Empty());
return;
}
switch (tab.NewValue)
{ {
case DashboardOverlayTabs.Friends: case DashboardOverlayTabs.Friends:
loadDisplay(new FriendDisplay()); LoadDisplay(new FriendDisplay());
break; break;
case DashboardOverlayTabs.CurrentlyPlaying: case DashboardOverlayTabs.CurrentlyPlaying:
loadDisplay(new CurrentlyPlayingDisplay()); LoadDisplay(new CurrentlyPlayingDisplay());
break; break;
default: default:
throw new NotImplementedException($"Display for {tab.NewValue} tab is not implemented"); throw new NotImplementedException($"Display for {tab} tab is not implemented");
} }
} }
private void onlineStateChanged(ValueChangedEvent<APIState> state) => Schedule(() =>
{
if (State.Value == Visibility.Hidden)
return;
Header.Current.TriggerChange();
});
protected override void Dispose(bool isDisposing)
{
cancellationToken?.Cancel();
base.Dispose(isDisposing);
}
} }
} }

View File

@ -1,11 +1,13 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Online.API; using osu.Game.Online.API;
using osuTK.Graphics; using osuTK.Graphics;
@ -15,21 +17,27 @@ namespace osu.Game.Overlays
public abstract class FullscreenOverlay<T> : WaveOverlayContainer, INamedOverlayComponent public abstract class FullscreenOverlay<T> : WaveOverlayContainer, INamedOverlayComponent
where T : OverlayHeader where T : OverlayHeader
{ {
public virtual string IconTexture => Header?.Title.IconTexture ?? string.Empty; public virtual string IconTexture => Header.Title.IconTexture ?? string.Empty;
public virtual string Title => Header?.Title.Title ?? string.Empty; public virtual string Title => Header.Title.Title ?? string.Empty;
public virtual string Description => Header?.Title.Description ?? string.Empty; public virtual string Description => Header.Title.Description ?? string.Empty;
public T Header { get; } public T Header { get; }
protected virtual Color4 BackgroundColour => ColourProvider.Background5;
[Resolved] [Resolved]
protected IAPIProvider API { get; private set; } protected IAPIProvider API { get; private set; }
[Cached] [Cached]
protected readonly OverlayColourProvider ColourProvider; protected readonly OverlayColourProvider ColourProvider;
protected FullscreenOverlay(OverlayColourScheme colourScheme, T header) protected override Container<Drawable> Content => content;
private readonly Container content;
protected FullscreenOverlay(OverlayColourScheme colourScheme)
{ {
Header = header; Header = CreateHeader();
ColourProvider = new OverlayColourProvider(colourScheme); ColourProvider = new OverlayColourProvider(colourScheme);
@ -47,6 +55,19 @@ namespace osu.Game.Overlays
Type = EdgeEffectType.Shadow, Type = EdgeEffectType.Shadow,
Radius = 10 Radius = 10
}; };
base.Content.AddRange(new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = BackgroundColour
},
content = new Container
{
RelativeSizeAxes = Axes.Both
}
});
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -58,6 +79,9 @@ namespace osu.Game.Overlays
Waves.FourthWaveColour = ColourProvider.Dark3; Waves.FourthWaveColour = ColourProvider.Dark3;
} }
[NotNull]
protected abstract T CreateHeader();
public override void Show() public override void Show()
{ {
if (State.Value == Visibility.Visible) if (State.Value == Visibility.Visible)

View File

@ -46,8 +46,9 @@ namespace osu.Game.Overlays.Mods
/// Change the selected mod index of this button. /// Change the selected mod index of this button.
/// </summary> /// </summary>
/// <param name="newIndex">The new index.</param> /// <param name="newIndex">The new index.</param>
/// <param name="resetSettings">Whether any settings applied to the mod should be reset on selection.</param>
/// <returns>Whether the selection changed.</returns> /// <returns>Whether the selection changed.</returns>
private bool changeSelectedIndex(int newIndex) private bool changeSelectedIndex(int newIndex, bool resetSettings = true)
{ {
if (newIndex == selectedIndex) return false; if (newIndex == selectedIndex) return false;
@ -69,6 +70,9 @@ namespace osu.Game.Overlays.Mods
Mod newSelection = SelectedMod ?? Mods[0]; Mod newSelection = SelectedMod ?? Mods[0];
if (resetSettings)
newSelection.ResetSettingsToDefaults();
Schedule(() => Schedule(() =>
{ {
if (beforeSelected != Selected) if (beforeSelected != Selected)
@ -209,11 +213,17 @@ namespace osu.Game.Overlays.Mods
Deselect(); Deselect();
} }
public bool SelectAt(int index) /// <summary>
/// Select the mod at the provided index.
/// </summary>
/// <param name="index">The index to select.</param>
/// <param name="resetSettings">Whether any settings applied to the mod should be reset on selection.</param>
/// <returns>Whether the selection changed.</returns>
public bool SelectAt(int index, bool resetSettings = true)
{ {
if (!Mods[index].HasImplementation) return false; if (!Mods[index].HasImplementation) return false;
changeSelectedIndex(index); changeSelectedIndex(index, resetSettings);
return true; return true;
} }

View File

@ -197,8 +197,11 @@ namespace osu.Game.Overlays.Mods
continue; continue;
var buttonMod = button.Mods[index]; var buttonMod = button.Mods[index];
// as this is likely coming from an external change, ensure the settings of the mod are in sync.
buttonMod.CopyFrom(mod); buttonMod.CopyFrom(mod);
button.SelectAt(index);
button.SelectAt(index, false);
return; return;
} }

View File

@ -372,7 +372,10 @@ namespace osu.Game.Overlays.Mods
base.LoadComplete(); base.LoadComplete();
availableMods.BindValueChanged(_ => updateAvailableMods(), true); availableMods.BindValueChanged(_ => updateAvailableMods(), true);
SelectedMods.BindValueChanged(_ => updateSelectedButtons(), true);
// intentionally bound after the above line to avoid a potential update feedback cycle.
// i haven't actually observed this happening but as updateAvailableMods() changes the selection it is plausible.
SelectedMods.BindValueChanged(_ => updateSelectedButtons());
} }
protected override void PopOut() protected override void PopOut()
@ -479,10 +482,10 @@ namespace osu.Game.Overlays.Mods
foreach (var section in ModSectionsContainer.Children) foreach (var section in ModSectionsContainer.Children)
section.UpdateSelectedButtons(selectedMods); section.UpdateSelectedButtons(selectedMods);
updateMods(); updateMultiplier();
} }
private void updateMods() private void updateMultiplier()
{ {
var multiplier = 1.0; var multiplier = 1.0;

View File

@ -2,67 +2,22 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Threading; using System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.News; using osu.Game.Overlays.News;
using osu.Game.Overlays.News.Displays; using osu.Game.Overlays.News.Displays;
namespace osu.Game.Overlays namespace osu.Game.Overlays
{ {
public class NewsOverlay : FullscreenOverlay<NewsHeader> public class NewsOverlay : OnlineOverlay<NewsHeader>
{ {
private readonly Bindable<string> article = new Bindable<string>(null); private readonly Bindable<string> article = new Bindable<string>(null);
private Container content;
private LoadingLayer loading;
private OverlayScrollContainer scrollFlow;
public NewsOverlay() public NewsOverlay()
: base(OverlayColourScheme.Purple, new NewsHeader()) : base(OverlayColourScheme.Purple)
{ {
} }
[BackgroundDependencyLoader]
private void load()
{
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = ColourProvider.Background5,
},
scrollFlow = new OverlayScrollContainer
{
RelativeSizeAxes = Axes.Both,
ScrollbarVisible = false,
Child = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
Header.With(h =>
{
h.ShowFrontPage = ShowFrontPage;
}),
content = new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
}
},
},
},
loading = new LoadingLayer(true),
};
}
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
@ -71,6 +26,11 @@ namespace osu.Game.Overlays
article.BindValueChanged(onArticleChanged); article.BindValueChanged(onArticleChanged);
} }
protected override NewsHeader CreateHeader() => new NewsHeader
{
ShowFrontPage = ShowFrontPage
};
private bool displayUpdateRequired = true; private bool displayUpdateRequired = true;
protected override void PopIn() protected override void PopIn()
@ -107,7 +67,7 @@ namespace osu.Game.Overlays
private void onArticleChanged(ValueChangedEvent<string> e) private void onArticleChanged(ValueChangedEvent<string> e)
{ {
cancellationToken?.Cancel(); cancellationToken?.Cancel();
loading.Show(); Loading.Show();
if (e.NewValue == null) if (e.NewValue == null)
{ {
@ -122,11 +82,11 @@ namespace osu.Game.Overlays
protected void LoadDisplay(Drawable display) protected void LoadDisplay(Drawable display)
{ {
scrollFlow.ScrollToStart(); ScrollFlow.ScrollToStart();
LoadComponentAsync(display, loaded => LoadComponentAsync(display, loaded =>
{ {
content.Child = loaded; Child = loaded;
loading.Hide(); Loading.Hide();
}, (cancellationToken = new CancellationTokenSource()).Token); }, (cancellationToken = new CancellationTokenSource()).Token);
} }

View File

@ -84,11 +84,6 @@ namespace osu.Game.Overlays
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Children = new Drawable[] Children = new Drawable[]
{ {
playlist = new PlaylistOverlay
{
RelativeSizeAxes = Axes.X,
Y = player_height + 10,
},
playerContainer = new Container playerContainer = new Container
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
@ -171,7 +166,7 @@ namespace osu.Game.Overlays
Anchor = Anchor.CentreRight, Anchor = Anchor.CentreRight,
Position = new Vector2(-bottom_black_area_height / 2, 0), Position = new Vector2(-bottom_black_area_height / 2, 0),
Icon = FontAwesome.Solid.Bars, Icon = FontAwesome.Solid.Bars,
Action = () => playlist.ToggleVisibility(), Action = togglePlaylist
}, },
} }
}, },
@ -191,13 +186,35 @@ namespace osu.Game.Overlays
}; };
} }
protected override void LoadComplete() private void togglePlaylist()
{ {
base.LoadComplete(); if (playlist == null)
{
LoadComponentAsync(playlist = new PlaylistOverlay
{
RelativeSizeAxes = Axes.X,
Y = player_height + 10,
}, _ =>
{
dragContainer.Add(playlist);
playlist.BeatmapSets.BindTo(musicController.BeatmapSets); playlist.BeatmapSets.BindTo(musicController.BeatmapSets);
playlist.State.BindValueChanged(s => playlistButton.FadeColour(s.NewValue == Visibility.Visible ? colours.Yellow : Color4.White, 200, Easing.OutQuint), true); playlist.State.BindValueChanged(s => playlistButton.FadeColour(s.NewValue == Visibility.Visible ? colours.Yellow : Color4.White, 200, Easing.OutQuint), true);
togglePlaylist();
});
return;
}
if (!beatmap.Disabled)
playlist.ToggleVisibility();
}
protected override void LoadComplete()
{
base.LoadComplete();
beatmap.BindDisabledChanged(beatmapDisabledChanged, true); beatmap.BindDisabledChanged(beatmapDisabledChanged, true);
musicController.TrackChanged += trackChanged; musicController.TrackChanged += trackChanged;
@ -306,7 +323,7 @@ namespace osu.Game.Overlays
private void beatmapDisabledChanged(bool disabled) private void beatmapDisabledChanged(bool disabled)
{ {
if (disabled) if (disabled)
playlist.Hide(); playlist?.Hide();
prevButton.Enabled.Value = !disabled; prevButton.Enabled.Value = !disabled;
nextButton.Enabled.Value = !disabled; nextButton.Enabled.Value = !disabled;
@ -411,6 +428,11 @@ namespace osu.Game.Overlays
private class HoverableProgressBar : ProgressBar private class HoverableProgressBar : ProgressBar
{ {
public HoverableProgressBar()
: base(true)
{
}
protected override bool OnHover(HoverEvent e) protected override bool OnHover(HoverEvent e)
{ {
this.ResizeHeightTo(progress_height, 500, Easing.OutQuint); this.ResizeHeightTo(progress_height, 500, Easing.OutQuint);

View File

@ -0,0 +1,48 @@
// 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.Containers;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Overlays
{
public abstract class OnlineOverlay<T> : FullscreenOverlay<T>
where T : OverlayHeader
{
protected override Container<Drawable> Content => content;
protected readonly OverlayScrollContainer ScrollFlow;
protected readonly LoadingLayer Loading;
private readonly Container content;
protected OnlineOverlay(OverlayColourScheme colourScheme)
: base(colourScheme)
{
base.Content.AddRange(new Drawable[]
{
ScrollFlow = new OverlayScrollContainer
{
RelativeSizeAxes = Axes.Both,
ScrollbarVisible = false,
Child = new FillFlowContainer
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
Header.With(h => h.Depth = float.MinValue),
content = new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y
}
}
}
},
Loading = new LoadingLayer(true)
});
}
}
}

View File

@ -4,96 +4,32 @@
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Overlays.Rankings; using osu.Game.Overlays.Rankings;
using osu.Game.Users; using osu.Game.Users;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Online.API; using osu.Game.Online.API;
using System.Threading;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests;
using osu.Game.Overlays.Rankings.Tables; using osu.Game.Overlays.Rankings.Tables;
namespace osu.Game.Overlays namespace osu.Game.Overlays
{ {
public class RankingsOverlay : FullscreenOverlay<RankingsOverlayHeader> public class RankingsOverlay : TabbableOnlineOverlay<RankingsOverlayHeader, RankingsScope>
{ {
protected Bindable<Country> Country => Header.Country; protected Bindable<Country> Country => Header.Country;
protected Bindable<RankingsScope> Scope => Header.Current;
private readonly OverlayScrollContainer scrollFlow;
private readonly Container contentContainer;
private readonly LoadingLayer loading;
private readonly Box background;
private APIRequest lastRequest; private APIRequest lastRequest;
private CancellationTokenSource cancellationToken;
[Resolved] [Resolved]
private IAPIProvider api { get; set; } private IAPIProvider api { get; set; }
public RankingsOverlay()
: base(OverlayColourScheme.Green, new RankingsOverlayHeader
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Depth = -float.MaxValue
})
{
loading = new LoadingLayer(true);
Children = new Drawable[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both
},
scrollFlow = new OverlayScrollContainer
{
RelativeSizeAxes = Axes.Both,
ScrollbarVisible = false,
Child = new FillFlowContainer
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
Header,
new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Children = new Drawable[]
{
contentContainer = new Container
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Margin = new MarginPadding { Bottom = 10 }
},
}
}
}
}
},
loading
};
}
[BackgroundDependencyLoader]
private void load()
{
background.Colour = ColourProvider.Background5;
}
[Resolved] [Resolved]
private Bindable<RulesetInfo> ruleset { get; set; } private Bindable<RulesetInfo> ruleset { get; set; }
public RankingsOverlay()
: base(OverlayColourScheme.Green)
{
}
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
@ -104,31 +40,33 @@ namespace osu.Game.Overlays
{ {
// if a country is requested, force performance scope. // if a country is requested, force performance scope.
if (Country.Value != null) if (Country.Value != null)
Scope.Value = RankingsScope.Performance; Header.Current.Value = RankingsScope.Performance;
Scheduler.AddOnce(loadNewContent); Scheduler.AddOnce(triggerTabChanged);
});
Scope.BindValueChanged(_ =>
{
// country filtering is only valid for performance scope.
if (Scope.Value != RankingsScope.Performance)
Country.Value = null;
Scheduler.AddOnce(loadNewContent);
}); });
ruleset.BindValueChanged(_ => ruleset.BindValueChanged(_ =>
{ {
if (Scope.Value == RankingsScope.Spotlights) if (Header.Current.Value == RankingsScope.Spotlights)
return; return;
Scheduler.AddOnce(loadNewContent); Scheduler.AddOnce(triggerTabChanged);
}); });
Scheduler.AddOnce(loadNewContent);
} }
protected override void OnTabChanged(RankingsScope tab)
{
// country filtering is only valid for performance scope.
if (Header.Current.Value != RankingsScope.Performance)
Country.Value = null;
Scheduler.AddOnce(triggerTabChanged);
}
private void triggerTabChanged() => base.OnTabChanged(Header.Current.Value);
protected override RankingsOverlayHeader CreateHeader() => new RankingsOverlayHeader();
public void ShowCountry(Country requested) public void ShowCountry(Country requested)
{ {
if (requested == null) if (requested == null)
@ -139,22 +77,13 @@ namespace osu.Game.Overlays
Country.Value = requested; Country.Value = requested;
} }
public void ShowSpotlights() protected override void CreateDisplayToLoad(RankingsScope tab)
{ {
Scope.Value = RankingsScope.Spotlights;
Show();
}
private void loadNewContent()
{
loading.Show();
cancellationToken?.Cancel();
lastRequest?.Cancel(); lastRequest?.Cancel();
if (Scope.Value == RankingsScope.Spotlights) if (Header.Current.Value == RankingsScope.Spotlights)
{ {
loadContent(new SpotlightsLayout LoadDisplay(new SpotlightsLayout
{ {
Ruleset = { BindTarget = ruleset } Ruleset = { BindTarget = ruleset }
}); });
@ -166,19 +95,19 @@ namespace osu.Game.Overlays
if (request == null) if (request == null)
{ {
loadContent(null); LoadDisplay(Empty());
return; return;
} }
request.Success += () => Schedule(() => loadContent(createTableFromResponse(request))); request.Success += () => Schedule(() => LoadDisplay(createTableFromResponse(request)));
request.Failure += _ => Schedule(() => loadContent(null)); request.Failure += _ => Schedule(() => LoadDisplay(Empty()));
api.Queue(request); api.Queue(request);
} }
private APIRequest createScopedRequest() private APIRequest createScopedRequest()
{ {
switch (Scope.Value) switch (Header.Current.Value)
{ {
case RankingsScope.Performance: case RankingsScope.Performance:
return new GetUserRankingsRequest(ruleset.Value, country: Country.Value?.FlagName); return new GetUserRankingsRequest(ruleset.Value, country: Country.Value?.FlagName);
@ -216,29 +145,9 @@ namespace osu.Game.Overlays
return null; return null;
} }
private void loadContent(Drawable content)
{
scrollFlow.ScrollToStart();
if (content == null)
{
contentContainer.Clear();
loading.Hide();
return;
}
LoadComponentAsync(content, loaded =>
{
loading.Hide();
contentContainer.Child = loaded;
}, (cancellationToken = new CancellationTokenSource()).Token);
}
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
{ {
lastRequest?.Cancel(); lastRequest?.Cancel();
cancellationToken?.Cancel();
base.Dispose(isDisposing); base.Dispose(isDisposing);
} }
} }

View File

@ -0,0 +1,101 @@
// 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.Threading;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Online.API;
namespace osu.Game.Overlays
{
public abstract class TabbableOnlineOverlay<THeader, TEnum> : OnlineOverlay<THeader>
where THeader : TabControlOverlayHeader<TEnum>
{
private readonly IBindable<APIState> apiState = new Bindable<APIState>();
private CancellationTokenSource cancellationToken;
private bool displayUpdateRequired = true;
protected TabbableOnlineOverlay(OverlayColourScheme colourScheme)
: base(colourScheme)
{
}
[BackgroundDependencyLoader]
private void load(IAPIProvider api)
{
apiState.BindTo(api.State);
apiState.BindValueChanged(onlineStateChanged, true);
}
protected override void LoadComplete()
{
base.LoadComplete();
Header.Current.BindValueChanged(tab => OnTabChanged(tab.NewValue));
}
protected override void PopIn()
{
base.PopIn();
// We don't want to create a new display on every call, only when exiting from fully closed state.
if (displayUpdateRequired)
{
Header.Current.TriggerChange();
displayUpdateRequired = false;
}
}
protected override void PopOutComplete()
{
base.PopOutComplete();
LoadDisplay(Empty());
displayUpdateRequired = true;
}
protected void LoadDisplay(Drawable display)
{
ScrollFlow.ScrollToStart();
LoadComponentAsync(display, loaded =>
{
if (API.IsLoggedIn)
Loading.Hide();
Child = loaded;
}, (cancellationToken = new CancellationTokenSource()).Token);
}
protected virtual void OnTabChanged(TEnum tab)
{
cancellationToken?.Cancel();
Loading.Show();
if (!API.IsLoggedIn)
{
LoadDisplay(Empty());
return;
}
CreateDisplayToLoad(tab);
}
protected abstract void CreateDisplayToLoad(TEnum tab);
private void onlineStateChanged(ValueChangedEvent<APIState> state) => Schedule(() =>
{
if (State.Value == Visibility.Hidden)
return;
Header.Current.TriggerChange();
});
protected override void Dispose(bool isDisposing)
{
cancellationToken?.Cancel();
base.Dispose(isDisposing);
}
}
}

View File

@ -15,6 +15,7 @@ using osu.Game.Overlays.Profile;
using osu.Game.Overlays.Profile.Sections; using osu.Game.Overlays.Profile.Sections;
using osu.Game.Users; using osu.Game.Users;
using osuTK; using osuTK;
using osuTK.Graphics;
namespace osu.Game.Overlays namespace osu.Game.Overlays
{ {
@ -29,10 +30,14 @@ namespace osu.Game.Overlays
public const float CONTENT_X_MARGIN = 70; public const float CONTENT_X_MARGIN = 70;
public UserProfileOverlay() public UserProfileOverlay()
: base(OverlayColourScheme.Pink, new ProfileHeader()) : base(OverlayColourScheme.Pink)
{ {
} }
protected override ProfileHeader CreateHeader() => new ProfileHeader();
protected override Color4 BackgroundColour => ColourProvider.Background6;
public void ShowUser(int userId) => ShowUser(new User { Id = userId }); public void ShowUser(int userId) => ShowUser(new User { Id = userId });
public void ShowUser(User user, bool fetchOnline = true) public void ShowUser(User user, bool fetchOnline = true)
@ -72,12 +77,6 @@ namespace osu.Game.Overlays
Origin = Anchor.TopCentre, Origin = Anchor.TopCentre,
}; };
Add(new Box
{
RelativeSizeAxes = Axes.Both,
Colour = ColourProvider.Background6
});
Add(sectionsContainer = new ProfileSectionsContainer Add(sectionsContainer = new ProfileSectionsContainer
{ {
ExpandableHeader = Header, ExpandableHeader = Header,

View File

@ -134,7 +134,7 @@ namespace osu.Game.Rulesets.Mods
} }
/// <summary> /// <summary>
/// Copies mod setting values from <paramref name="source"/> into this instance. /// Copies mod setting values from <paramref name="source"/> into this instance, overwriting all existing settings.
/// </summary> /// </summary>
/// <param name="source">The mod to copy properties from.</param> /// <param name="source">The mod to copy properties from.</param>
public void CopyFrom(Mod source) public void CopyFrom(Mod source)
@ -147,8 +147,6 @@ namespace osu.Game.Rulesets.Mods
var targetBindable = (IBindable)prop.GetValue(this); var targetBindable = (IBindable)prop.GetValue(this);
var sourceBindable = (IBindable)prop.GetValue(source); var sourceBindable = (IBindable)prop.GetValue(source);
// we only care about changes that have been made away from defaults.
if (!sourceBindable.IsDefault)
CopyAdjustedSetting(targetBindable, sourceBindable); CopyAdjustedSetting(targetBindable, sourceBindable);
} }
} }
@ -175,5 +173,10 @@ namespace osu.Game.Rulesets.Mods
} }
public bool Equals(IMod other) => GetType() == other?.GetType(); public bool Equals(IMod other) => GetType() == other?.GetType();
/// <summary>
/// Reset all custom settings for this mod back to their defaults.
/// </summary>
public virtual void ResetSettingsToDefaults() => CopyFrom((Mod)Activator.CreateInstance(GetType()));
} }
} }

View File

@ -141,5 +141,16 @@ namespace osu.Game.Rulesets.Mods
ApplySetting(DrainRate, dr => difficulty.DrainRate = dr); ApplySetting(DrainRate, dr => difficulty.DrainRate = dr);
ApplySetting(OverallDifficulty, od => difficulty.OverallDifficulty = od); ApplySetting(OverallDifficulty, od => difficulty.OverallDifficulty = od);
} }
public override void ResetSettingsToDefaults()
{
base.ResetSettingsToDefaults();
if (difficulty != null)
{
// base implementation potentially overwrite modified defaults that came from a beatmap selection.
TransferSettings(difficulty);
}
}
} }
} }

View File

@ -151,7 +151,10 @@ namespace osu.Game.Screens.Edit.Timing
return new RowAttribute("difficulty", () => $"{difficulty.SpeedMultiplier:n2}x", colour); return new RowAttribute("difficulty", () => $"{difficulty.SpeedMultiplier:n2}x", colour);
case EffectControlPoint effect: case EffectControlPoint effect:
return new RowAttribute("effect", () => $"{(effect.KiaiMode ? "Kiai " : "")}{(effect.OmitFirstBarLine ? "NoBarLine " : "")}", colour); return new RowAttribute("effect", () => string.Join(" ",
effect.KiaiMode ? "Kiai" : string.Empty,
effect.OmitFirstBarLine ? "NoBarLine" : string.Empty
).Trim(), colour);
case SampleControlPoint sample: case SampleControlPoint sample:
return new RowAttribute("sample", () => $"{sample.SampleBank} {sample.SampleVolume}%", colour); return new RowAttribute("sample", () => $"{sample.SampleBank} {sample.SampleVolume}%", colour);

View File

@ -49,6 +49,8 @@ namespace osu.Game.Screens.OnlinePlay.Match
[Cached] [Cached]
protected OnlinePlayBeatmapAvailablilityTracker BeatmapAvailablilityTracker { get; } protected OnlinePlayBeatmapAvailablilityTracker BeatmapAvailablilityTracker { get; }
protected IBindable<BeatmapAvailability> BeatmapAvailability => BeatmapAvailablilityTracker.Availability;
protected RoomSubScreen() protected RoomSubScreen()
{ {
AddInternal(BeatmapAvailablilityTracker = new OnlinePlayBeatmapAvailablilityTracker AddInternal(BeatmapAvailablilityTracker = new OnlinePlayBeatmapAvailablilityTracker

View File

@ -267,6 +267,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
base.LoadComplete(); base.LoadComplete();
Playlist.BindCollectionChanged(onPlaylistChanged, true); Playlist.BindCollectionChanged(onPlaylistChanged, true);
BeatmapAvailability.BindValueChanged(updateBeatmapAvailability, true);
UserMods.BindValueChanged(onUserModsChanged); UserMods.BindValueChanged(onUserModsChanged);
client.LoadRequested += onLoadRequested; client.LoadRequested += onLoadRequested;
@ -321,6 +322,19 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
client.ChangeUserMods(mods.NewValue); client.ChangeUserMods(mods.NewValue);
} }
private void updateBeatmapAvailability(ValueChangedEvent<BeatmapAvailability> availability)
{
if (client.Room == null)
return;
client.ChangeBeatmapAvailability(availability.NewValue);
// while this flow is handled server-side, this covers the edge case of the local user being in a ready state and then deleting the current beatmap.
if (availability.NewValue != Online.Rooms.BeatmapAvailability.LocallyAvailable()
&& client.LocalUser?.State == MultiplayerUserState.Ready)
client.ChangeState(MultiplayerUserState.Idle);
}
private void onReadyClick() private void onReadyClick()
{ {
Debug.Assert(readyClickOperation == null); Debug.Assert(readyClickOperation == null);

View File

@ -162,7 +162,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
const double fade_time = 50; const double fade_time = 50;
userStateDisplay.Status = User.State; userStateDisplay.UpdateStatus(User.State, User.BeatmapAvailability);
if (Room.Host?.Equals(User) == true) if (Room.Host?.Equals(User) == true)
crown.FadeIn(fade_time); crown.FadeIn(fade_time);

View File

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Diagnostics;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -8,83 +10,94 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osuTK; using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
{ {
public class StateDisplay : CompositeDrawable public class StateDisplay : CompositeDrawable
{ {
private const double fade_time = 50;
private SpriteIcon icon;
private OsuSpriteText text;
private ProgressBar progressBar;
public StateDisplay() public StateDisplay()
{ {
AutoSizeAxes = Axes.Both; AutoSizeAxes = Axes.Both;
Alpha = 0; Alpha = 0;
} }
private MultiplayerUserState status;
private OsuSpriteText text;
private SpriteIcon icon;
private const double fade_time = 50;
public MultiplayerUserState Status
{
set
{
if (value == status)
return;
status = value;
if (IsLoaded)
updateStatus();
}
}
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load(OsuColour colours)
{ {
this.colours = colours;
InternalChild = new FillFlowContainer InternalChild = new FillFlowContainer
{ {
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Spacing = new Vector2(5), Spacing = new Vector2(5),
Children = new Drawable[] Children = new Drawable[]
{ {
icon = new SpriteIcon
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Icon = FontAwesome.Solid.CheckCircle,
Size = new Vector2(12),
},
new CircularContainer
{
Masking = true,
AutoSizeAxes = Axes.Both,
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Children = new Drawable[]
{
progressBar = new ProgressBar(false)
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
BackgroundColour = Color4.Black.Opacity(0.4f),
FillColour = colours.Blue,
Alpha = 0f,
},
text = new OsuSpriteText text = new OsuSpriteText
{ {
Anchor = Anchor.CentreLeft, Padding = new MarginPadding { Horizontal = 5f, Vertical = 1f },
Origin = Anchor.CentreLeft, Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 12), Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 12),
Colour = Color4Extensions.FromHex("#DDFFFF") Colour = Color4Extensions.FromHex("#DDFFFF")
}, },
icon = new SpriteIcon
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Icon = FontAwesome.Solid.CheckCircle,
Size = new Vector2(12),
} }
},
} }
}; };
} }
protected override void LoadComplete() private OsuColour colours;
{
base.LoadComplete();
updateStatus();
}
[Resolved] public void UpdateStatus(MultiplayerUserState state, BeatmapAvailability availability)
private OsuColour colours { get; set; } {
// the only case where the progress bar is used does its own local fade in.
// starting by fading out is a sane default.
progressBar.FadeOut(fade_time);
this.FadeIn(fade_time);
private void updateStatus() switch (state)
{ {
switch (status) case MultiplayerUserState.Idle:
{ showBeatmapAvailability(availability);
default: break;
this.FadeOut(fade_time);
return;
case MultiplayerUserState.Ready: case MultiplayerUserState.Ready:
text.Text = "ready"; text.Text = "ready";
@ -121,9 +134,43 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
icon.Icon = FontAwesome.Solid.ArrowAltCircleUp; icon.Icon = FontAwesome.Solid.ArrowAltCircleUp;
icon.Colour = colours.BlueLighter; icon.Colour = colours.BlueLighter;
break; break;
default:
throw new ArgumentOutOfRangeException(nameof(state), state, null);
}
} }
this.FadeIn(fade_time); private void showBeatmapAvailability(BeatmapAvailability availability)
{
switch (availability.State)
{
default:
this.FadeOut(fade_time);
break;
case DownloadState.NotDownloaded:
text.Text = "no map";
icon.Icon = FontAwesome.Solid.MinusCircle;
icon.Colour = colours.RedLight;
break;
case DownloadState.Downloading:
Debug.Assert(availability.DownloadProgress != null);
progressBar.FadeIn(fade_time);
progressBar.CurrentTime = availability.DownloadProgress.Value;
text.Text = "downloading map";
icon.Icon = FontAwesome.Solid.ArrowAltCircleDown;
icon.Colour = colours.Blue;
break;
case DownloadState.Importing:
text.Text = "importing map";
icon.Icon = FontAwesome.Solid.ArrowAltCircleDown;
icon.Colour = colours.Yellow;
break;
}
} }
} }
} }