1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-18 14:50:54 +08:00

Merge branch 'master' into add-custom-samples-via-setup

This commit is contained in:
Dean Herbert
2026-01-16 20:11:28 +09:00
committed by GitHub
Unverified
136 changed files with 3673 additions and 1363 deletions
+1 -1
View File
@@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.1229.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2026.108.0" />
</ItemGroup>
<PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged.
@@ -2,7 +2,10 @@
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
namespace osu.Game.Rulesets.Osu.Tests.Mods
{
@@ -18,5 +21,39 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
Autoplay = false,
});
}
[Test]
public void TestSkipToFirstCircleNotSuppressed()
{
CreateModTest(new ModTestData
{
Mod = new OsuModFreezeFrame(),
CreateBeatmap = () => new OsuBeatmap
{
HitObjects =
{
new HitCircle { StartTime = 5000, Position = OsuPlayfield.BASE_SIZE / 2 }
}
},
PassCondition = () => Player.GameplayClockContainer.GameplayStartTime > 0
});
}
[Test]
public void TestSkipToFirstSpinnerNotSuppressed()
{
CreateModTest(new ModTestData
{
Mod = new OsuModFreezeFrame(),
CreateBeatmap = () => new OsuBeatmap
{
HitObjects =
{
new Spinner { StartTime = 5000, Position = OsuPlayfield.BASE_SIZE / 2 }
}
},
PassCondition = () => Player.GameplayClockContainer.GameplayStartTime > 0
});
}
}
}
@@ -0,0 +1,63 @@
// 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.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Replays;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Osu.Tests
{
[TestFixture]
[HeadlessTest]
public partial class TestSceneAutoGeneration : OsuTestScene
{
[TestCase(-1, true)]
[TestCase(0, false)]
[TestCase(1, false)]
public void TestAlternating(double offset, bool shouldAlternate)
{
const double first_object_time = 1000;
double secondObjectTime = first_object_time + AutoGenerator.KEY_UP_DELAY + OsuAutoGenerator.MIN_FRAME_SEPARATION_FOR_ALTERNATING + offset;
var beatmap = new OsuBeatmap();
beatmap.HitObjects.Add(new HitCircle { StartTime = first_object_time });
beatmap.HitObjects.Add(new HitCircle { StartTime = secondObjectTime });
var generated = new OsuAutoGenerator(beatmap, []).Generate();
var frames = generated.Frames.OfType<OsuReplayFrame>().ToList();
Assert.That(frames.Exists(f => f.Time == first_object_time && f.Actions.SingleOrDefault() == OsuAction.LeftButton));
Assert.That(frames.Exists(f => f.Time == first_object_time + AutoGenerator.KEY_UP_DELAY && !f.Actions.Any()));
Assert.That(frames.Exists(f => f.Time == secondObjectTime && f.Actions.SingleOrDefault() == (shouldAlternate ? OsuAction.RightButton : OsuAction.LeftButton)));
Assert.That(frames.Exists(f => f.Time == secondObjectTime + AutoGenerator.KEY_UP_DELAY && !f.Actions.Any()));
}
[TestCase(300)]
[TestCase(600)]
[TestCase(1200)]
public void TestAlternatingSpecificBPM(double bpm)
{
const double first_object_time = 1000;
double secondObjectTime = first_object_time + 60000 / bpm;
var beatmap = new OsuBeatmap();
beatmap.HitObjects.Add(new HitCircle { StartTime = first_object_time });
beatmap.HitObjects.Add(new HitCircle { StartTime = secondObjectTime });
var generated = new OsuAutoGenerator(beatmap, []).Generate();
var frames = generated.Frames.OfType<OsuReplayFrame>().ToList();
Assert.That(frames.Exists(f => f.Time == first_object_time && f.Actions.SingleOrDefault() == OsuAction.LeftButton));
Assert.That(frames.Exists(f => f.Time == first_object_time + AutoGenerator.KEY_UP_DELAY && !f.Actions.Any()));
Assert.That(frames.Exists(f => f.Time == secondObjectTime && f.Actions.SingleOrDefault() == OsuAction.RightButton));
Assert.That(frames.Exists(f => f.Time == secondObjectTime + AutoGenerator.KEY_UP_DELAY && !f.Actions.Any()));
}
}
}
@@ -25,10 +25,10 @@ namespace osu.Game.Rulesets.Osu.Edit
{
public partial class PolygonGenerationPopover : OsuPopover
{
private SliderWithTextBoxInput<double> distanceSnapInput = null!;
private SliderWithTextBoxInput<int> offsetAngleInput = null!;
private SliderWithTextBoxInput<int> repeatCountInput = null!;
private SliderWithTextBoxInput<int> pointInput = null!;
private FormSliderBar<double> distanceSnapInput { get; set; } = null!;
private FormSliderBar<int> offsetAngleInput { get; set; } = null!;
private FormSliderBar<int> repeatCountInput { get; set; } = null!;
private FormSliderBar<int> pointInput { get; set; } = null!;
private RoundedButton commitButton = null!;
private readonly List<HitCircle> insertedCircles = new List<HitCircle>();
@@ -64,11 +64,12 @@ namespace osu.Game.Rulesets.Osu.Edit
{
Width = 220,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(20),
Spacing = new Vector2(5),
Children = new Drawable[]
{
distanceSnapInput = new SliderWithTextBoxInput<double>("Distance snap:")
distanceSnapInput = new FormSliderBar<double>
{
Caption = "Distance snap",
Current = new BindableNumber<double>(1)
{
MinValue = 0.1,
@@ -76,37 +77,40 @@ namespace osu.Game.Rulesets.Osu.Edit
Precision = 0.1,
Value = ((OsuHitObjectComposer)composer).DistanceSnapProvider.DistanceSpacingMultiplier.Value,
},
Instantaneous = true
TabbableContentContainer = this
},
offsetAngleInput = new SliderWithTextBoxInput<int>("Offset angle:")
offsetAngleInput = new FormSliderBar<int>
{
Caption = "Offset angle",
Current = new BindableNumber<int>
{
MinValue = 0,
MaxValue = 180,
Precision = 1
},
Instantaneous = true
TabbableContentContainer = this
},
repeatCountInput = new SliderWithTextBoxInput<int>("Repeats:")
repeatCountInput = new FormSliderBar<int>
{
Caption = "Repeats",
Current = new BindableNumber<int>(1)
{
MinValue = 1,
MaxValue = 10,
Precision = 1
},
Instantaneous = true
TabbableContentContainer = this
},
pointInput = new SliderWithTextBoxInput<int>("Vertices:")
pointInput = new FormSliderBar<int>
{
Caption = "Vertices",
Current = new BindableNumber<int>(3)
{
MinValue = 3,
MaxValue = 32,
Precision = 1,
},
Instantaneous = true
TabbableContentContainer = this
},
commitButton = new RoundedButton
{
@@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.Edit
private BindableNumber<float> xBindable = null!;
private BindableNumber<float> yBindable = null!;
private SliderWithTextBoxInput<float> xInput = null!;
private FormSliderBar<float> xInput { get; set; } = null!;
private OsuCheckbox relativeCheckbox = null!;
public PreciseMovementPopover()
@@ -52,31 +52,31 @@ namespace osu.Game.Rulesets.Osu.Edit
{
Width = 220,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(20),
Spacing = new Vector2(5),
Children = new Drawable[]
{
xInput = new SliderWithTextBoxInput<float>("X:")
xInput = new FormSliderBar<float>
{
Caption = "X",
Current = xBindable = new BindableNumber<float>
{
Precision = 1,
},
Instantaneous = true,
TabbableContentContainer = this,
TabbableContentContainer = this
},
new SliderWithTextBoxInput<float>("Y:")
new FormSliderBar<float>
{
Caption = "Y",
Current = yBindable = new BindableNumber<float>
{
Precision = 1,
},
Instantaneous = true,
TabbableContentContainer = this,
TabbableContentContainer = this
},
relativeCheckbox = new OsuCheckbox(false)
{
RelativeSizeAxes = Axes.X,
LabelText = "Relative movement",
LabelText = "Relative movement"
}
}
};
@@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Osu.Edit
private readonly Bindable<PreciseRotationInfo> rotationInfo = new Bindable<PreciseRotationInfo>(new PreciseRotationInfo(0, EditorOrigin.GridCentre));
private SliderWithTextBoxInput<float> angleInput = null!;
private FormSliderBar<float> angleInput { get; set; } = null!;
private EditorRadioButtonCollection rotationOrigin = null!;
private RadioButton gridCentreButton = null!;
@@ -54,11 +54,12 @@ namespace osu.Game.Rulesets.Osu.Edit
{
Width = 220,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(20),
Spacing = new Vector2(5),
Children = new Drawable[]
{
angleInput = new SliderWithTextBoxInput<float>("Angle (degrees):")
angleInput = new FormSliderBar<float>
{
Caption = "Angle (degrees)",
Current = new BindableNumber<float>
{
MinValue = -360,
@@ -66,7 +67,7 @@ namespace osu.Game.Rulesets.Osu.Edit
Precision = 1
},
KeyboardStep = 1f,
Instantaneous = true
TabbableContentContainer = this
},
rotationOrigin = new EditorRadioButtonCollection
{
@@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Osu.Edit
private readonly Bindable<PreciseScaleInfo> scaleInfo = new Bindable<PreciseScaleInfo>(new PreciseScaleInfo(1, EditorOrigin.GridCentre, true, true));
private SliderWithTextBoxInput<float> scaleInput = null!;
private FormSliderBar<float> scaleInput { get; set; } = null!;
private BindableNumber<float> scaleInputBindable = null!;
private EditorRadioButtonCollection scaleOrigin = null!;
@@ -66,11 +66,12 @@ namespace osu.Game.Rulesets.Osu.Edit
{
Width = 220,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(20),
Spacing = new Vector2(5),
Children = new Drawable[]
{
scaleInput = new SliderWithTextBoxInput<float>("Scale:")
scaleInput = new FormSliderBar<float>
{
Caption = "Scale",
Current = scaleInputBindable = new BindableNumber<float>
{
MinValue = 0.05f,
@@ -80,7 +81,7 @@ namespace osu.Game.Rulesets.Osu.Edit
Default = 1,
},
KeyboardStep = 0.01f,
Instantaneous = true
TabbableContentContainer = this
},
scaleOrigin = new EditorRadioButtonCollection
{
@@ -57,7 +57,8 @@ namespace osu.Game.Rulesets.Osu.Mods
void applyFadeInAdjustment(OsuHitObject osuObject)
{
osuObject.TimePreempt += osuObject.StartTime - lastNewComboTime;
if (osuObject is not Spinner)
osuObject.TimePreempt += osuObject.StartTime - lastNewComboTime;
foreach (var nested in osuObject.NestedHitObjects.OfType<OsuHitObject>())
{
@@ -21,6 +21,8 @@ namespace osu.Game.Rulesets.Osu.Replays
{
public class OsuAutoGenerator : OsuAutoGeneratorBase
{
public const double MIN_FRAME_SEPARATION_FOR_ALTERNATING = 266;
public new OsuBeatmap Beatmap => (OsuBeatmap)base.Beatmap;
#region Parameters
@@ -245,7 +247,7 @@ namespace osu.Game.Rulesets.Osu.Replays
double timeDifference = ApplyModsToTimeDelta(lastFrame.Time, h.StartTime);
OsuReplayFrame? lastLastFrame = Frames.Count >= 2 ? (OsuReplayFrame)Frames[^2] : null;
if (timeDifference > 0)
if (timeDifference >= 0)
{
// If the last frame is a key-up frame and there has been no wait period, adjust the last frame's position such that it begins eased movement instantaneously.
if (lastLastFrame != null && lastFrame is OsuKeyUpReplayFrame && !hasWaited)
@@ -266,7 +268,7 @@ namespace osu.Game.Rulesets.Osu.Replays
}
// Start alternating once the time separation is too small (faster than ~225BPM).
if (timeDifference > 0 && timeDifference < 266)
if (timeDifference >= 0 && timeDifference < MIN_FRAME_SEPARATION_FOR_ALTERNATING)
buttonIndex++;
else
buttonIndex = 0;
@@ -7,7 +7,9 @@ using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osuTK;
@@ -31,7 +33,7 @@ namespace osu.Game.Tests.Visual.Colours
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(5f),
ChildrenEnumerable = Enumerable.Range(0, 10).Select(i => new FillFlowContainer
ChildrenEnumerable = Enumerable.Range(0, 15).Select(i => new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -40,7 +42,9 @@ namespace osu.Game.Tests.Visual.Colours
Spacing = new Vector2(10f),
ChildrenEnumerable = Enumerable.Range(0, 10).Select(j =>
{
var colour = colours.ForStarDifficulty(1f * i + 0.1f * j);
float difficulty = 1f * i + 0.1f * j;
var colour = colours.ForStarDifficulty(difficulty);
var textColour = colours.ForStarDifficultyText(difficulty);
return new FillFlowContainer
{
@@ -48,36 +52,27 @@ namespace osu.Game.Tests.Visual.Colours
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0f, 10f),
Spacing = new Vector2(0f, 5f),
Children = new Drawable[]
{
new CircularContainer
new OsuSpriteText
{
Masking = true,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Size = new Vector2(75f, 25f),
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colour,
},
new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Colour = OsuColour.ForegroundTextColourFor(colour),
Text = colour.ToHex(),
},
}
Font = FontUsage.Default.With(size: 10),
Text = $"BG: {colour.ToHex()}",
},
new OsuSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Text = $"*{(1f * i + 0.1f * j):0.00}",
Font = FontUsage.Default.With(size: 10),
Text = $"Text: {textColour.ToHex()}",
},
new StarRatingDisplay(new StarDifficulty(difficulty, 0))
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
}
}
};
@@ -34,7 +34,7 @@ namespace osu.Game.Tests.Visual.Editing
SaveEditor();
ReloadEditorToSameBeatmap();
AddAssert("beatmap marked as locally modified", () => EditorBeatmap.BeatmapInfo.Status, () => Is.EqualTo(BeatmapOnlineStatus.LocallyModified));
AddUntilStep("beatmap marked as locally modified", () => EditorBeatmap.BeatmapInfo.Status, () => Is.EqualTo(BeatmapOnlineStatus.LocallyModified));
AddAssert("beatmap hash changed", () => EditorBeatmap.BeatmapInfo.MD5Hash, () => Is.Not.EqualTo(initialHash));
}
}
@@ -37,6 +37,42 @@ namespace osu.Game.Tests.Visual.Editing
private GlobalActionContainer globalActionContainer => this.ChildrenOfType<GlobalActionContainer>().Single();
[Test]
public void TestPlaceThenUndo()
{
AddStep("select circle placement tool", () => InputManager.Key(Key.Number2));
AddStep("move mouse to center of playfield", () => InputManager.MoveMouseTo(this.ChildrenOfType<Playfield>().Single()));
AddStep("place circle", () => InputManager.Click(MouseButton.Left));
AddAssert("one circle added", () => EditorBeatmap.HitObjects, () => Has.One.Items);
AddStep("undo", () => Editor.Undo());
AddAssert("circle removed", () => EditorBeatmap.HitObjects, () => Is.Empty);
}
[Test]
public void TestTimingLost()
{
AddStep("select circle placement tool", () => InputManager.Key(Key.Number2));
AddStep("move mouse to center of playfield", () => InputManager.MoveMouseTo(this.ChildrenOfType<Playfield>().Single()));
AddAssert("placement ready", () => this.ChildrenOfType<ComposeBlueprintContainer>().Single().CurrentPlacement, () => Is.Not.Null);
AddStep("nuke timing", () => EditorBeatmap.ControlPointInfo.Clear());
AddAssert("placement not available", () => this.ChildrenOfType<ComposeBlueprintContainer>().Single().CurrentPlacement, () => Is.Null);
AddStep("select circle placement tool", () => InputManager.Key(Key.Number2));
AddAssert("placement not available", () => this.ChildrenOfType<ComposeBlueprintContainer>().Single().CurrentPlacement, () => Is.Null);
AddStep("add back timing", () => EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint()));
AddStep("select circle placement tool", () => InputManager.Key(Key.Number2));
AddAssert("placement ready", () => this.ChildrenOfType<ComposeBlueprintContainer>().Single().CurrentPlacement, () => Is.Not.Null);
}
[Test]
public void TestDeleteUsingMiddleMouse()
{
@@ -270,7 +270,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
AddStep("create overlay", () =>
{
hudOverlay = new HUDOverlay(null, Array.Empty<Mod>());
hudOverlay = new HUDOverlay(null, Array.Empty<Mod>(), new PlayerConfiguration());
// Add any key just to display the key counter visually.
hudOverlay.InputCountController.Add(new KeyCounterKeyboardTrigger(Key.Space));
@@ -119,7 +119,7 @@ namespace osu.Game.Tests.Visual.Gameplay
Children = new Drawable[]
{
drawableRuleset,
new HUDOverlay(drawableRuleset, [])
new HUDOverlay(drawableRuleset, [], new PlayerConfiguration())
{
RelativeSizeAxes = Axes.Both,
}
@@ -60,7 +60,7 @@ namespace osu.Game.Tests.Visual.Gameplay
var drawableRuleset = ruleset.CreateDrawableRulesetWith(beatmap, mods);
var hudOverlay = new HUDOverlay(drawableRuleset, mods)
var hudOverlay = new HUDOverlay(drawableRuleset, mods, new PlayerConfiguration())
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -96,7 +96,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
SetContents(_ =>
{
hudOverlay = new HUDOverlay(new DrawableOsuRuleset(new OsuRuleset(), new OsuBeatmap()), Array.Empty<Mod>());
hudOverlay = new HUDOverlay(new DrawableOsuRuleset(new OsuRuleset(), new OsuBeatmap()), Array.Empty<Mod>(), new PlayerConfiguration());
action?.Invoke(hudOverlay);
@@ -5,6 +5,7 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Cursor;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
@@ -45,10 +46,46 @@ namespace osu.Game.Tests.Visual.Matchmaking
AddStep("add panel", () =>
{
var beatmap = CreateAPIBeatmap();
beatmap.TopTags =
[
new APIBeatmapTag { TagId = 4, VoteCount = 1 },
new APIBeatmapTag { TagId = 2, VoteCount = 1 },
new APIBeatmapTag { TagId = 23, VoteCount = 5 },
];
beatmap.BeatmapSet!.HasExplicitContent = true;
beatmap.BeatmapSet!.HasVideo = true;
beatmap.BeatmapSet!.HasStoryboard = true;
beatmap.BeatmapSet.FeaturedInSpotlight = true;
beatmap.BeatmapSet.TrackId = 1;
beatmap.BeatmapSet!.RelatedTags =
[
new APITag
{
Id = 2,
Name = "song representation/simple",
Description = "Accessible and straightforward map design."
},
new APITag
{
Id = 4,
Name = "style/clean",
Description = "Visually uncluttered and organised patterns, often involving few overlaps and equal visual spacing between objects."
},
new APITag
{
Id = 23,
Name = "aim/aim control",
Description = "Patterns with velocity or direction changes which strongly go against a player's natural movement pattern."
}
];
Child = new OsuContextMenuContainer
{
RelativeSizeAxes = Axes.Both,
Child = panel = new MatchmakingSelectPanelBeatmap(new MatchmakingPlaylistItem(new MultiplayerPlaylistItem(), CreateAPIBeatmap(), []))
Child = panel = new MatchmakingSelectPanelBeatmap(new MatchmakingPlaylistItem(new MultiplayerPlaylistItem(), beatmap, []))
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -96,6 +133,12 @@ namespace osu.Game.Tests.Visual.Matchmaking
};
});
AddStep("add peppy", () => panel!.AddUser(new APIUser
{
Id = 2,
Username = "peppy",
}));
AddToggleStep("allow selection", value => panel!.AllowSelection = value);
AddStep("reveal beatmap", () => panel!.PresentAsChosenBeatmap(new MatchmakingPlaylistItem(new MultiplayerPlaylistItem(), CreateAPIBeatmap(), [])));
@@ -122,5 +122,51 @@ namespace osu.Game.Tests.Visual.Matchmaking
AddStep("set download progress 90%", () => MultiplayerClient.ChangeUserBeatmapAvailability(2, BeatmapAvailability.Downloading(0.9f)));
AddStep("set locally available", () => MultiplayerClient.ChangeUserBeatmapAvailability(2, BeatmapAvailability.LocallyAvailable()));
}
[Test]
public void TestLongUsername()
{
AddStep("set long username", () =>
{
MultiplayerClient.ChangeMatchRoomState(new MatchmakingRoomState
{
Users =
{
UserDictionary =
{
{
2, new MatchmakingUser
{
UserId = 2,
Placement = 1
}
}
}
}
}).WaitSafely();
Child = panel = new PlayerPanel(new MultiplayerRoomUser(2)
{
User = new APIUser
{
Username = @"ThisIsALongUsername",
Id = 2,
Colour = "99EB47",
CountryCode = CountryCode.AU,
CoverUrl = @"https://assets.ppy.sh/user-profile-covers/2/baba245ef60834b769694178f8f6d4f6166c5188c740de084656ad2b80f1eea7.jpeg",
Statistics = new UserStatistics { GlobalRank = null, CountryRank = null }
}
})
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre
};
});
foreach (var layout in Enum.GetValues<PlayerPanelDisplayMode>())
{
AddStep($"set layout to {layout}", () => panel.DisplayMode = layout);
}
}
}
}
@@ -9,9 +9,7 @@ using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
@@ -22,7 +20,6 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.OnlinePlay;
using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.OnlinePlay.Playlists;
using osu.Game.Tests.Resources;
using osu.Game.Tests.Visual.OnlinePlay;
@@ -30,7 +27,7 @@ using osuTK.Input;
namespace osu.Game.Tests.Visual.Multiplayer
{
public partial class TestScenePlaylistsSongSelect : OnlinePlayTestScene
public partial class TestScenePlaylistsSongSelectV2 : OnlinePlayTestScene
{
private RulesetStore rulesets = null!;
private BeatmapManager manager = null!;
@@ -69,47 +66,45 @@ namespace osu.Game.Tests.Visual.Multiplayer
});
AddStep("create song select", () => LoadScreen(songSelect = new TestPlaylistsSongSelect(room)));
AddUntilStep("wait for present", () => songSelect.IsCurrentScreen() && songSelect.BeatmapSetsLoaded);
AddUntilStep("wait for song select", () => songSelect.IsLoaded && !songSelect.IsFiltering);
}
[Test]
public void TestShowScreen()
{
AddStep("show screen", () => { });
}
[Test]
public void TestItemAddedIfEmptyOnStart()
{
AddStep("finalise selection", () => songSelect.FinaliseSelection());
AddStep("finalise selection", () => InputManager.Key(Key.Enter));
AddAssert("playlist has 1 item", () => room.Playlist.Count == 1);
}
[Test]
public void TestItemAddedWhenCreateNewItemClicked()
{
AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem!());
AddAssert("playlist has 1 item", () => room.Playlist.Count == 1);
}
[Test]
public void TestItemNotAddedIfExistingOnStart()
{
AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem!());
AddStep("finalise selection", () => songSelect.FinaliseSelection());
AddStep("create new item", () => songSelect.AddNewItem());
AddAssert("playlist has 1 item", () => room.Playlist.Count == 1);
}
[Test]
public void TestAddSameItemMultipleTimes()
{
AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem!());
AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem!());
AddStep("create new item", () => songSelect.AddNewItem());
AddStep("create new item", () => songSelect.AddNewItem());
AddAssert("playlist has 2 items", () => room.Playlist.Count == 2);
}
[Test]
public void TestAddItemAfterRearrangement()
{
AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem!());
AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem!());
AddStep("create new item", () => songSelect.AddNewItem());
AddStep("create new item", () => songSelect.AddNewItem());
AddStep("rearrange", () => room.Playlist = room.Playlist.Skip(1).Append(room.Playlist[0]).ToArray());
AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem!());
AddStep("create new item", () => songSelect.AddNewItem());
AddAssert("new item has id 2", () => room.Playlist.Last().ID == 2);
}
@@ -120,9 +115,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
public void TestNewItemHasNewModInstances()
{
AddStep("set dt mod", () => SelectedMods.Value = new[] { new OsuModDoubleTime() });
AddStep("create item", () => songSelect.BeatmapDetails.CreateNewItem!());
AddStep("create item", () => songSelect.AddNewItem());
AddStep("change mod rate", () => ((OsuModDoubleTime)SelectedMods.Value[0]).SpeedChange.Value = 2);
AddStep("create item", () => songSelect.BeatmapDetails.CreateNewItem!());
AddStep("create item", () => songSelect.AddNewItem());
AddAssert("item 1 has rate 1.5", () =>
{
@@ -153,7 +148,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
mod = (OsuModDoubleTime)SelectedMods.Value[0];
});
AddStep("create item", () => songSelect.BeatmapDetails.CreateNewItem!());
AddStep("create item", () => songSelect.AddNewItem());
AddStep("change stored mod rate", () => mod.SpeedChange.Value = 2);
AddAssert("item has rate 1.5", () =>
@@ -166,26 +161,23 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestFreeModSelectionDisable()
{
FooterButtonFreeMods freeMods = null!;
AddAssert("freestyle enabled", () => songSelect.Freestyle.Value, () => Is.True);
AddStep("click icon in free mods button", () =>
{
freeMods = this.ChildrenOfType<FooterButtonFreeMods>().Single();
InputManager.MoveMouseTo(freeMods.ChildrenOfType<SpriteIcon>().Single());
InputManager.MoveMouseTo(this.ChildrenOfType<FooterButtonFreeModsV2>().Single());
InputManager.Click(MouseButton.Left);
});
AddAssert("mod select not visible", () => this.ChildrenOfType<FreeModSelectOverlay>().Single().State.Value, () => Is.EqualTo(Visibility.Hidden));
AddStep("toggle freestyle off", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<FooterButtonFreestyle>().Single());
InputManager.MoveMouseTo(this.ChildrenOfType<FooterButtonFreestyleV2>().Single());
InputManager.Click(MouseButton.Left);
});
AddAssert("freestyle disabled", () => songSelect.Freestyle.Value, () => Is.False);
AddStep("click icon in free mods button", () =>
{
InputManager.MoveMouseTo(freeMods.ChildrenOfType<SpriteIcon>().Single());
InputManager.MoveMouseTo(this.ChildrenOfType<FooterButtonFreeModsV2>().Single());
InputManager.Click(MouseButton.Left);
});
AddAssert("mod select visible", () => this.ChildrenOfType<FreeModSelectOverlay>().Single().State.Value, () => Is.EqualTo(Visibility.Visible));
@@ -199,10 +191,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
rulesets.Dispose();
}
private partial class TestPlaylistsSongSelect : PlaylistsSongSelect
private partial class TestPlaylistsSongSelect : PlaylistsSongSelectV2
{
public new MatchBeatmapDetailArea BeatmapDetails => (MatchBeatmapDetailArea)base.BeatmapDetails;
public new IBindable<bool> Freestyle => base.Freestyle;
public TestPlaylistsSongSelect(Room room)
@@ -94,21 +94,22 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("edit playlist", () => InputManager.Key(Key.Enter));
AddUntilStep("wait for song select", () => (playlistScreen.CurrentSubScreen as PlaylistsSongSelect)?.BeatmapSetsLoaded == true);
AddUntilStep("wait for song select", () => playlistScreen.CurrentSubScreen is PlaylistsSongSelectV2 songSelect && songSelect.IsLoaded && !songSelect.IsFiltering);
AddUntilStep("wait for selection", () => !Game.Beatmap.IsDefault);
AddStep("add item", () => InputManager.Key(Key.Enter));
AddStep("exit screen", () => InputManager.Key(Key.Escape));
AddUntilStep("wait for return to playlist screen", () => playlistScreen.CurrentSubScreen is PlaylistsRoomSubScreen);
AddStep("go back to song select", () =>
{
InputManager.MoveMouseTo(playlistScreen.ChildrenOfType<PurpleRoundedButton>().Single(b => b.Text == "Edit playlist"));
InputManager.MoveMouseTo(playlistScreen.ChildrenOfType<PurpleRoundedButton>().Single(b => b.Text == "+ Add more beatmaps"));
InputManager.Click(MouseButton.Left);
});
AddUntilStep("wait for song select", () => (playlistScreen.CurrentSubScreen as PlaylistsSongSelect)?.BeatmapSetsLoaded == true);
AddUntilStep("wait for song select", () => playlistScreen.CurrentSubScreen is PlaylistsSongSelectV2 songSelect && songSelect.IsLoaded && !songSelect.IsFiltering);
AddStep("press home button", () =>
{
@@ -141,13 +142,12 @@ namespace osu.Game.Tests.Visual.Navigation
[Test]
public void TestExitSongSelectWithEscape()
{
SoloSongSelect songSelect = null;
ModSelectOverlay modSelect = null;
PushAndConfirm(() => songSelect = new SoloSongSelect());
PushAndConfirm(() => new SoloSongSelect());
AddStep("Show mods overlay", () =>
{
modSelect = songSelect!.ChildrenOfType<ModSelectOverlay>().Single();
modSelect = Game!.ChildrenOfType<ModSelectOverlay>().Single();
modSelect.Show();
});
AddAssert("Overlay was shown", () => modSelect.State.Value == Visibility.Visible);
@@ -309,11 +309,9 @@ namespace osu.Game.Tests.Visual.Navigation
[Test]
public void TestOpenModSelectOverlayUsingAction()
{
SoloSongSelect songSelect = null;
PushAndConfirm(() => songSelect = new SoloSongSelect());
PushAndConfirm(() => new SoloSongSelect());
AddStep("Show mods overlay", () => InputManager.Key(Key.F1));
AddAssert("Overlay was shown", () => songSelect!.ChildrenOfType<ModSelectOverlay>().Single().State.Value == Visibility.Visible);
AddAssert("Overlay was shown", () => Game!.ChildrenOfType<ModSelectOverlay>().Single().State.Value == Visibility.Visible);
}
[Test]
@@ -730,7 +728,7 @@ namespace osu.Game.Tests.Visual.Navigation
PushAndConfirm(() => songSelect = new SoloSongSelect());
AddStep("Show mods overlay", () =>
{
modSelect = songSelect!.ChildrenOfType<ModSelectOverlay>().Single();
modSelect = Game!.ChildrenOfType<ModSelectOverlay>().Single();
modSelect.Show();
});
AddAssert("Overlay was shown", () => modSelect.State.Value == Visibility.Visible);
@@ -805,13 +803,12 @@ namespace osu.Game.Tests.Visual.Navigation
{
AddUntilStep("Wait for toolbar to load", () => Game.Toolbar.IsLoaded);
SoloSongSelect songSelect = null;
ModSelectOverlay modSelect = null;
PushAndConfirm(() => songSelect = new SoloSongSelect());
PushAndConfirm(() => new SoloSongSelect());
AddStep("Show mods overlay", () =>
{
modSelect = songSelect!.ChildrenOfType<ModSelectOverlay>().Single();
modSelect = Game!.ChildrenOfType<ModSelectOverlay>().Single();
modSelect.Show();
});
AddAssert("Overlay was shown", () => modSelect.State.Value == Visibility.Visible);
@@ -38,7 +38,7 @@ namespace osu.Game.Tests.Visual.Navigation
public partial class TestSceneSkinEditorNavigation : OsuGameTestScene
{
private SoloSongSelect songSelect;
private ModSelectOverlay modSelect => songSelect.ChildrenOfType<ModSelectOverlay>().First();
private ModSelectOverlay modSelect => Game.ChildrenOfType<ModSelectOverlay>().First();
private SkinEditor skinEditor => Game.ChildrenOfType<SkinEditor>().FirstOrDefault();
@@ -0,0 +1,40 @@
// 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 NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Overlays;
using osu.Game.Screens.OnlinePlay.Playlists;
namespace osu.Game.Tests.Visual.Playlists
{
public partial class TestSceneAddToPlaylistFooterButton : OsuTestScene
{
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
private AddToPlaylistFooterButton button = null!;
[SetUp]
public void Setup() => Schedule(() =>
{
Child = button = new AddToPlaylistFooterButton
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Action = () => { }
};
});
[Test]
public void TestAppearDisappear()
{
AddStep("appear", () => button.Appear());
AddWaitStep("wait for animation", 3);
AddStep("disappear", () => button.Disappear());
AddWaitStep("wait for animation", 3);
AddStep("appear", () => button.Appear());
}
}
}
@@ -0,0 +1,61 @@
// 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.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.OnlinePlay;
namespace osu.Game.Tests.Visual.Playlists
{
public partial class TestSceneFooterButtonFreeModsV2 : OsuTestScene
{
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
private readonly FooterButtonFreeModsV2 button;
public TestSceneFooterButtonFreeModsV2()
{
ModSelectOverlay modSelectOverlay;
Add(modSelectOverlay = new TestModSelectOverlay());
Add(button = new FooterButtonFreeModsV2(modSelectOverlay)
{
Anchor = Anchor.Centre,
Origin = Anchor.CentreLeft,
X = -100,
});
}
[Test]
public void TestAllMods()
{
AddStep("all mods", () => button.FreeMods.Value = new OsuRuleset().CreateAllMods().ToArray());
}
[Test]
public void TestNoMods()
{
AddStep("no mods", () => button.FreeMods.Value = []);
}
[Test]
public void TestFreestyle()
{
AddToggleStep("toggle freestyle", v => button.Freestyle.Value = v);
}
private partial class TestModSelectOverlay : UserModSelectOverlay
{
public TestModSelectOverlay()
: base(OverlayColourScheme.Aquamarine)
{
IsValidMod = _ => true;
}
}
}
}
@@ -0,0 +1,26 @@
// 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.Graphics;
using osu.Game.Overlays;
using osu.Game.Screens.OnlinePlay;
namespace osu.Game.Tests.Visual.Playlists
{
public partial class TestSceneFooterButtonFreestyleV2 : OsuTestScene
{
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
public TestSceneFooterButtonFreestyleV2()
{
Add(new FooterButtonFreestyleV2
{
Anchor = Anchor.Centre,
Origin = Anchor.CentreLeft,
X = -100,
});
}
}
}
@@ -0,0 +1,37 @@
// 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.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Playlists;
using osu.Game.Tests.Visual.OnlinePlay;
namespace osu.Game.Tests.Visual.Playlists
{
public partial class TestScenePlaylistTray : OnlinePlayTestScene
{
private Room room = null!;
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("add tray", () => Child = new PlaylistsSongSelectV2.PlaylistTray(room = new Room())
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre
});
}
[Test]
public void TestAddItem()
{
AddStep("add playlist item", () =>
{
room.Playlist = room.Playlist.Append(new PlaylistItem(CreateAPIBeatmap())).ToArray();
});
}
}
}
@@ -659,6 +659,51 @@ namespace osu.Game.Tests.Visual.SongSelectV2
AddAssert("options disabled", () => !this.ChildrenOfType<FooterButtonOptions>().Single().Enabled.Value);
}
/// <summary>
/// tests that clicking the osu! logo immediately after selecting a different difficulty
/// (before the selection debounce completes) starts the correct beatmap.
/// this tests the fix for https://github.com/ppy/osu/issues/36074
/// </summary>
[Test]
public void TestPlayCorrectBeatmapWhenSelectionNotFullyLoaded()
{
// import a beatmap set with multiple difficulties
ImportBeatmapForRuleset(0);
LoadSongSelect();
// wait for initial beatmap to be selected
AddUntilStep("wait for first beatmap selected", () => !Beatmap.IsDefault);
BeatmapInfo? firstBeatmap = null;
AddStep("store first difficulty", () => firstBeatmap = Beatmap.Value.BeatmapInfo);
// start loading the first difficulty
AddStep("click logo to start loading", () => this.ChildrenOfType<OsuLogo>().Single().TriggerClick());
AddUntilStep("wait for player loader", () => Stack.CurrentScreen is PlayerLoader);
// return to song select
AddStep("press escape to return", () => InputManager.Key(Key.Escape));
AddUntilStep("wait for return to song select", () => SongSelect.IsCurrentScreen());
// press down and schedule logo click to happen shortly after (but before 150ms debounce)
// this reproduces the race condition where Beatmap.Value hasn't updated yet
AddStep("select next difficulty and click logo immediately", () =>
{
InputManager.Key(Key.Down);
Schedule(() => this.ChildrenOfType<OsuLogo>().Single().TriggerClick());
});
AddUntilStep("wait for player loader", () => Stack.CurrentScreen is PlayerLoader);
// verify we're loading the second difficulty, not the first
// without the fix, this would fail because Beatmap.Value still has the old value
AddAssert("player is loading second difficulty", () =>
Beatmap.Value.BeatmapInfo.ID != firstBeatmap!.ID);
AddUntilStep("wait for return to song select", () => SongSelect.IsCurrentScreen());
}
#endregion
}
}
@@ -379,6 +379,49 @@ namespace osu.Game.Tests.Visual.SongSelectV2
checkMatchedBeatmaps(6);
}
[Test]
public void TestScopeToBeatmapWhenDifficultiesGroupedBySet()
{
ImportBeatmapForRuleset(0);
ImportBeatmapForRuleset(0);
LoadSongSelect();
SortBy(SortMode.Artist);
checkMatchedBeatmaps(6);
AddStep("click spread indicator", () => this.ChildrenOfType<PanelBeatmapSet.SpreadDisplay>().Single(d => d.Enabled.Value).TriggerClick());
WaitForFiltering();
checkMatchedBeatmaps(3);
AddStep("press Escape", () => InputManager.Key(Key.Escape));
WaitForFiltering();
checkMatchedBeatmaps(6);
}
[Test]
public void TestDismissingScopeDoesNotClearSearchTextBox()
{
ImportBeatmapForRuleset(0);
ImportBeatmapForRuleset(0);
LoadSongSelect();
SortBy(SortMode.Artist);
checkMatchedBeatmaps(6);
AddStep("set text filter", () => filterTextBox.Current.Value = Beatmaps.GetAllUsableBeatmapSets().First().Metadata.Title);
WaitForFiltering();
checkMatchedBeatmaps(3);
AddStep("click spread indicator", () => this.ChildrenOfType<PanelBeatmapSet.SpreadDisplay>().Single(d => d.Enabled.Value).TriggerClick());
WaitForFiltering();
checkMatchedBeatmaps(3);
AddStep("press Escape", () => InputManager.Key(Key.Escape));
WaitForFiltering();
checkMatchedBeatmaps(3);
AddAssert("text filter not emptied", () => filterTextBox.Current.Value, () => Is.Not.Empty);
}
private NoResultsPlaceholder? getPlaceholder() => SongSelect.ChildrenOfType<NoResultsPlaceholder>().FirstOrDefault();
private void checkMatchedBeatmaps(int expected) => AddUntilStep($"{expected} matching shown", () => Carousel.MatchedBeatmapsCount, () => Is.EqualTo(expected));
@@ -0,0 +1,139 @@
// 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.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays;
using osuTK;
namespace osu.Game.Tests.Visual.UserInterface
{
public partial class TestSceneFormButton : ThemeComparisonTestScene
{
public TestSceneFormButton()
: base(false)
{
}
protected override Drawable CreateContent() => new OsuContextMenuContainer
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new BackgroundBox
{
RelativeSizeAxes = Axes.Both,
},
new PopoverContainer
{
RelativeSizeAxes = Axes.Both,
Child = new OsuScrollContainer
{
RelativeSizeAxes = Axes.Both,
Child = new FillFlowContainer
{
AutoSizeAxes = Axes.Y,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Width = 400,
Direction = FillDirection.Vertical,
Spacing = new Vector2(5),
Padding = new MarginPadding(10),
Children = new Drawable[]
{
new FormButton
{
Caption = "Button with default style",
Action = () => { },
},
new FormButton
{
Caption = "Button with default style",
Enabled = { Value = false },
},
new FormButton
{
Caption = "Button with custom style",
BackgroundColour = new OsuColour().DangerousButtonColour,
ButtonIcon = FontAwesome.Solid.Hamburger,
Action = () => { },
},
new FormButton
{
Caption = "Button with custom style",
BackgroundColour = new OsuColour().DangerousButtonColour,
ButtonIcon = FontAwesome.Solid.Hamburger,
Enabled = { Value = false },
},
new FormButton
{
Caption = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua",
BackgroundColour = new OsuColour().Blue3,
ButtonIcon = FontAwesome.Solid.Book,
Action = () => { },
},
new FormButton
{
Caption = "Button with text inside",
ButtonText = "Text in button",
Action = () => { },
},
new FormButton
{
Caption = "Button with text inside",
ButtonText = "Text in button",
Enabled = { Value = false },
},
new FormButton
{
Caption = "Button with text inside",
ButtonText = "Text in button",
BackgroundColour = new OsuColour().DangerousButtonColour,
Action = () => { },
},
new FormButton
{
Caption = "Button with text inside",
ButtonText = "Text in button",
BackgroundColour = new OsuColour().DangerousButtonColour,
Enabled = { Value = false },
},
new FormButton
{
Caption = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor",
ButtonText = "Text in button",
BackgroundColour = new OsuColour().Blue3,
Action = () => { },
},
new FormButton
{
Caption = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor",
ButtonText = "Text in button",
BackgroundColour = new OsuColour().Blue3,
Enabled = { Value = false },
},
},
},
},
}
}
};
private partial class BackgroundBox : Box
{
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
Colour = colourProvider.Background4;
}
}
}
}
@@ -42,171 +42,236 @@ namespace osu.Game.Tests.Visual.UserInterface
RelativeSizeAxes = Axes.Both,
Child = new FillFlowContainer
{
AutoSizeAxes = Axes.Y,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 400,
Direction = FillDirection.Vertical,
Spacing = new Vector2(5),
Padding = new MarginPadding(10),
Children = new Drawable[]
AutoSizeAxes = Axes.Both,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Direction = FillDirection.Horizontal,
Children = new[]
{
new FormTextBox
new FillFlowContainer
{
Caption = "Artist",
HintText = "Poot artist here!",
PlaceholderText = "Here is an artist",
TabbableContentContainer = this,
},
new FormTextBox
{
Caption = "Artist",
HintText = "Poot artist here!",
PlaceholderText = "Here is an artist",
Current = { Disabled = true },
TabbableContentContainer = this,
},
new FormNumberBox(allowDecimals: true)
{
Caption = "Number",
HintText = "Insert your favourite number",
PlaceholderText = "Mine is 42!",
TabbableContentContainer = this,
},
new FormCheckBox
{
Caption = EditorSetupStrings.LetterboxDuringBreaks,
HintText = EditorSetupStrings.LetterboxDuringBreaksDescription,
},
new FormCheckBox
{
Caption = EditorSetupStrings.LetterboxDuringBreaks,
HintText = EditorSetupStrings.LetterboxDuringBreaksDescription,
Current = { Disabled = true },
},
new FormCheckBox
{
Caption = EditorSetupStrings.LetterboxDuringBreaks,
HintText = EditorSetupStrings.LetterboxDuringBreaksDescription,
Current = { Value = true, Disabled = true },
},
new FormSliderBar<float>
{
Caption = "Slider",
HintText = "Slider hint",
Current = new BindableFloat
AutoSizeAxes = Axes.Y,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Width = 400,
Direction = FillDirection.Vertical,
Spacing = new Vector2(5),
Padding = new MarginPadding(10),
Children = new Drawable[]
{
MinValue = 0,
MaxValue = 10,
Value = 5,
Precision = 0.1f,
new FormTextBox
{
Caption = "Artist",
HintText = "Poot artist here!",
PlaceholderText = "Here is an artist",
TabbableContentContainer = this,
},
new FormTextBox
{
Caption = "Artist",
HintText = "Poot artist here!",
PlaceholderText = "Here is an artist",
Current = { Disabled = true },
TabbableContentContainer = this,
},
new FormNumberBox(allowDecimals: true)
{
Caption = "Number",
HintText = "Insert your favourite number",
PlaceholderText = "Mine is 42!",
TabbableContentContainer = this,
},
new FormCheckBox
{
Caption = EditorSetupStrings.LetterboxDuringBreaks,
HintText = EditorSetupStrings.LetterboxDuringBreaksDescription,
},
new FormCheckBox
{
Caption = EditorSetupStrings.LetterboxDuringBreaks,
HintText = EditorSetupStrings.LetterboxDuringBreaksDescription,
Current = { Disabled = true },
},
new FormCheckBox
{
Caption = EditorSetupStrings.LetterboxDuringBreaks,
HintText = EditorSetupStrings.LetterboxDuringBreaksDescription,
Current = { Value = true, Disabled = true },
},
new FormSliderBar<float>
{
Caption = "Slider",
HintText = "Slider hint",
Current = new BindableFloat
{
MinValue = 0,
MaxValue = 10,
Value = 5,
Precision = 0.1f,
},
TabbableContentContainer = this,
},
new FormSliderBar<float>
{
Caption = "Slider",
HintText = "Slider hint",
Current = new BindableFloat
{
MinValue = 0,
MaxValue = 10,
Value = 5,
Precision = 0.1f,
Disabled = true,
},
TransferValueOnCommit = true,
TabbableContentContainer = this,
},
new FormSliderBar<float>
{
Caption = "Slider (percentage)",
HintText = "Percentage slider hint",
Current = new BindableFloat
{
MinValue = 0,
MaxValue = 1,
Value = 0.2f,
Precision = 0.0001f,
},
DisplayAsPercentage = true,
TabbableContentContainer = this,
},
new FormSliderBar<float>
{
Caption = "Slider (custom)",
HintText = "Custom slider hint",
Current = new BindableFloat
{
MinValue = 0,
MaxValue = 1,
Value = 0.2f,
Precision = 0.0001f,
},
LabelFormat = v => $"{v * 100:0.00} funometer",
TooltipFormat = v => $"This setting has the value set to {v * 100:0.00} funometer.",
TabbableContentContainer = this,
},
new FormSliderBar<float>
{
Caption = "Slider (custom)",
HintText = "Custom slider hint",
Current = new BindableFloat
{
MinValue = 0,
MaxValue = 1,
Value = 0.2f,
Precision = 0.0001f,
Disabled = true,
},
TransferValueOnCommit = true,
LabelFormat = v => $"{v * 100:0.00} funometer",
TooltipFormat = v => $"This setting has the value set to {v * 100:0.00} funometer.",
TabbableContentContainer = this,
},
new FormEnumDropdown<CountdownType>
{
Caption = EditorSetupStrings.EnableCountdown,
HintText = EditorSetupStrings.CountdownDescription,
},
new FormEnumDropdown<CountdownType>
{
Caption = EditorSetupStrings.EnableCountdown,
HintText = EditorSetupStrings.CountdownDescription,
Current = { Disabled = true },
},
new FormFileSelector
{
Caption = "File selector",
PlaceholderText = "Select a file",
},
new FormBeatmapFileSelector(true)
{
Caption = "File selector with intermediate choice dialog",
PlaceholderText = "Select a file",
},
new FormColourPalette
{
Caption = "Combo colours",
Colours =
{
Colour4.Red,
Colour4.Green,
Colour4.Blue,
Colour4.Yellow,
}
},
new FormButton
{
Caption = "No text in button",
Action = () => { },
},
},
TabbableContentContainer = this,
},
new FormSliderBar<float>
new FillFlowContainer
{
Caption = "Slider",
HintText = "Slider hint",
Current = new BindableFloat
AutoSizeAxes = Axes.Y,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Width = 400,
Direction = FillDirection.Vertical,
Spacing = new Vector2(5),
Padding = new MarginPadding(10),
Children = new Drawable[]
{
MinValue = 0,
MaxValue = 10,
Value = 5,
Precision = 0.1f,
Disabled = true,
new FormNumberBox(allowDecimals: true)
{
Caption = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua",
HintText = "Insert your favourite number",
PlaceholderText = "Mine is 42!",
TabbableContentContainer = this,
},
new FormCheckBox
{
Caption = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua",
HintText = EditorSetupStrings.LetterboxDuringBreaksDescription,
},
new FormSliderBar<float>
{
Caption = "Lorem ipsum dolor sit amet, conse adipiscing elit, sed do eiusmod",
HintText = "Slider hint",
Current = new BindableFloat
{
MinValue = 0,
MaxValue = 10,
Value = 5,
Precision = 0.1f,
},
TabbableContentContainer = this,
},
new FormEnumDropdown<CountdownType>
{
Caption = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua",
HintText = EditorSetupStrings.CountdownDescription,
},
new FormFileSelector
{
Caption = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua",
HintText = EditorSetupStrings.CountdownDescription,
PlaceholderText = "Select a file",
},
new FormColourPalette
{
Caption = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua",
HintText = EditorSetupStrings.CountdownDescription,
Colours =
{
Colour4.Red,
Colour4.Green,
Colour4.Blue,
Colour4.Yellow,
}
},
},
TransferValueOnCommit = true,
TabbableContentContainer = this,
},
new FormSliderBar<float>
{
Caption = "Slider (percentage)",
HintText = "Percentage slider hint",
Current = new BindableFloat
{
MinValue = 0,
MaxValue = 1,
Value = 0.2f,
Precision = 0.0001f,
},
DisplayAsPercentage = true,
TabbableContentContainer = this,
},
new FormSliderBar<float>
{
Caption = "Slider (custom)",
HintText = "Custom slider hint",
Current = new BindableFloat
{
MinValue = 0,
MaxValue = 1,
Value = 0.2f,
Precision = 0.0001f,
},
LabelFormat = v => $"{v * 100:0.00} funometer",
TooltipFormat = v => $"This setting has the value set to {v * 100:0.00} funometer.",
TabbableContentContainer = this,
},
new FormSliderBar<float>
{
Caption = "Slider (custom)",
HintText = "Custom slider hint",
Current = new BindableFloat
{
MinValue = 0,
MaxValue = 1,
Value = 0.2f,
Precision = 0.0001f,
Disabled = true,
},
TransferValueOnCommit = true,
LabelFormat = v => $"{v * 100:0.00} funometer",
TooltipFormat = v => $"This setting has the value set to {v * 100:0.00} funometer.",
TabbableContentContainer = this,
},
new FormEnumDropdown<CountdownType>
{
Caption = EditorSetupStrings.EnableCountdown,
HintText = EditorSetupStrings.CountdownDescription,
},
new FormEnumDropdown<CountdownType>
{
Caption = EditorSetupStrings.EnableCountdown,
HintText = EditorSetupStrings.CountdownDescription,
Current = { Disabled = true },
},
new FormFileSelector
{
Caption = "File selector",
PlaceholderText = "Select a file",
},
new FormBeatmapFileSelector(true)
{
Caption = "File selector with intermediate choice dialog",
PlaceholderText = "Select a file",
},
new FormColourPalette
{
Caption = "Combo colours",
Colours =
{
Colour4.Red,
Colour4.Green,
Colour4.Blue,
Colour4.Yellow,
}
},
new FormButton
{
Caption = "No text in button",
Action = () => { },
},
new FormButton
{
Caption = "Text in button which is pretty long and is very likely to wrap",
ButtonText = "Foo the bar",
Action = () => { },
},
}
},
},
},
@@ -0,0 +1,104 @@
// 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.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Localisation;
using osu.Game.Overlays;
using osuTK;
namespace osu.Game.Tests.Visual.UserInterface
{
public partial class TestSceneFormDropdown : ThemeComparisonTestScene
{
public TestSceneFormDropdown()
: base(false)
{
}
protected override Drawable CreateContent() => new OsuContextMenuContainer
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new BackgroundBox
{
RelativeSizeAxes = Axes.Both,
},
new PopoverContainer
{
RelativeSizeAxes = Axes.Both,
Child = new OsuScrollContainer
{
RelativeSizeAxes = Axes.Both,
Child = new FillFlowContainer
{
AutoSizeAxes = Axes.Y,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Width = 400,
Direction = FillDirection.Vertical,
Spacing = new Vector2(5),
Padding = new MarginPadding(10),
Children = new Drawable[]
{
new FormEnumDropdown<CountdownType>
{
Caption = EditorSetupStrings.EnableCountdown,
HintText = EditorSetupStrings.CountdownDescription,
},
new FormEnumDropdown<CountdownType>
{
Caption = EditorSetupStrings.EnableCountdown,
HintText = EditorSetupStrings.CountdownDescription,
Current = { Disabled = true },
},
new FormDropdown<string>
{
Caption = "Custom dropdown",
HintText = "Custom dropdown hint",
Items = new[]
{
"A verrry looooongggg thiiiinngggggg toooooo fittttt iiinnnn thhiisssss droooppdddoowwwnn",
"B verrry looooongggg thiiiinngggggg toooooo fittttt iiinnnn thhiisssss droooppdddoowwwnn",
"C verrry looooongggg thiiiinngggggg toooooo fittttt iiinnnn thhiisssss droooppdddoowwwnn",
"D verrry looooongggg thiiiinngggggg toooooo fittttt iiinnnn thhiisssss droooppdddoowwwnn",
},
},
new FormDropdown<string>
{
Caption = "Custom dropdown",
HintText = "Custom dropdown hint",
AlwaysShowSearchBar = true,
Items = new[]
{
"A verrry looooongggg thiiiinngggggg toooooo fittttt iiinnnn thhiisssss droooppdddoowwwnn",
"B verrry looooongggg thiiiinngggggg toooooo fittttt iiinnnn thhiisssss droooppdddoowwwnn",
"C verrry looooongggg thiiiinngggggg toooooo fittttt iiinnnn thhiisssss droooppdddoowwwnn",
"D verrry looooongggg thiiiinngggggg toooooo fittttt iiinnnn thhiisssss droooppdddoowwwnn",
},
},
},
},
},
}
}
};
private partial class BackgroundBox : Box
{
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
Colour = colourProvider.Background4;
}
}
}
}
@@ -64,9 +64,11 @@ namespace osu.Game.Tests.Visual.UserInterface
});
}
[Test]
public void TestNubDoubleClickRevertToDefault()
[TestCase(false)]
[TestCase(true)]
public void TestNubDoubleClickRevertToDefault(bool transferValueOnCommit)
{
OsuSpriteText text;
FormSliderBar<float> slider = null!;
AddStep("create content", () =>
@@ -81,9 +83,11 @@ namespace osu.Game.Tests.Visual.UserInterface
Spacing = new Vector2(10),
Children = new Drawable[]
{
text = new OsuSpriteText(),
slider = new FormSliderBar<float>
{
Caption = "Slider",
TransferValueOnCommit = transferValueOnCommit,
Current = new BindableFloat
{
MinValue = 0,
@@ -94,6 +98,7 @@ namespace osu.Game.Tests.Visual.UserInterface
},
}
};
slider.Current.BindValueChanged(_ => text.Text = $"Current value is: {slider.Current.Value}", true);
});
AddStep("set slider to 1", () => slider.Current.Value = 1);
@@ -96,7 +96,7 @@ namespace osu.Game.Tests.Visual.UserInterface
private partial class EmptyToast : Toast
{
public EmptyToast()
: base("", "", "")
: base("", "")
{
}
}
@@ -104,8 +104,9 @@ namespace osu.Game.Tests.Visual.UserInterface
private partial class LengthyToast : Toast
{
public LengthyToast()
: base("Toast with a very very very long text", "A very very very very very very long text also", "A very very very very very long shortcut")
: base("Toast with a very very very long text", "A very very very very very very long text also")
{
ExtraText = "A very very very very very long shortcut";
}
}
@@ -1,131 +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.Linq;
using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Input;
using osu.Framework.Testing;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osuTK.Input;
namespace osu.Game.Tests.Visual.UserInterface
{
public partial class TestSceneSliderWithTextBoxInput : OsuManualInputManagerTestScene
{
private SliderWithTextBoxInput<float> sliderWithTextBoxInput = null!;
private OsuSliderBar<float> slider => sliderWithTextBoxInput.ChildrenOfType<OsuSliderBar<float>>().Single();
private Nub nub => sliderWithTextBoxInput.ChildrenOfType<Nub>().Single();
private OsuTextBox textBox => sliderWithTextBoxInput.ChildrenOfType<OsuTextBox>().Single();
[SetUpSteps]
public void SetUpSteps()
{
AddStep("create slider", () => Child = sliderWithTextBoxInput = new SliderWithTextBoxInput<float>("Test Slider")
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 0.5f,
Current = new BindableFloat
{
MinValue = -5,
MaxValue = 5,
Precision = 0.2f
}
});
}
[Test]
public void TestNonInstantaneousMode()
{
AddStep("set instantaneous to false", () => sliderWithTextBoxInput.Instantaneous = false);
AddStep("focus textbox", () => ((IFocusManager)InputManager).ChangeFocus(textBox));
AddStep("change text", () => textBox.Text = "3");
AddAssert("slider not moved", () => slider.Current.Value, () => Is.Zero);
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.Zero);
AddStep("commit text", () => InputManager.Key(Key.Enter));
AddAssert("slider moved", () => slider.Current.Value, () => Is.EqualTo(3));
AddAssert("current changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(3));
AddStep("move mouse to nub", () => InputManager.MoveMouseTo(nub));
AddStep("hold left mouse", () => InputManager.PressButton(MouseButton.Left));
AddStep("move mouse to minimum", () => InputManager.MoveMouseTo(sliderWithTextBoxInput.ScreenSpaceDrawQuad.BottomLeft));
AddAssert("textbox not changed", () => textBox.Current.Value, () => Is.EqualTo("3"));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(3));
AddStep("release left mouse", () => InputManager.ReleaseButton(MouseButton.Left));
AddAssert("textbox changed", () => textBox.Current.Value, () => Is.EqualTo("-5"));
AddAssert("current changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
AddStep("focus textbox", () => ((IFocusManager)InputManager).ChangeFocus(textBox));
AddStep("set text to invalid", () => textBox.Text = "garbage");
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
AddStep("commit text", () => InputManager.Key(Key.Enter));
AddAssert("text restored", () => textBox.Text, () => Is.EqualTo("-5"));
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
AddStep("focus textbox", () => ((IFocusManager)InputManager).ChangeFocus(textBox));
AddStep("set text to invalid", () => textBox.Text = "garbage");
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
AddStep("lose focus", () => ((IFocusManager)InputManager).ChangeFocus(null));
AddAssert("text restored", () => textBox.Text, () => Is.EqualTo("-5"));
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
}
[Test]
public void TestInstantaneousMode()
{
AddStep("set instantaneous to true", () => sliderWithTextBoxInput.Instantaneous = true);
AddStep("focus textbox", () => ((IFocusManager)InputManager).ChangeFocus(textBox));
AddStep("change text", () => textBox.Text = "3");
AddAssert("slider moved", () => slider.Current.Value, () => Is.EqualTo(3));
AddAssert("current changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(3));
AddStep("commit text", () => InputManager.Key(Key.Enter));
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(3));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(3));
AddStep("move mouse to nub", () => InputManager.MoveMouseTo(nub));
AddStep("hold left mouse", () => InputManager.PressButton(MouseButton.Left));
AddStep("move mouse to minimum", () => InputManager.MoveMouseTo(sliderWithTextBoxInput.ScreenSpaceDrawQuad.BottomLeft));
AddAssert("textbox changed", () => textBox.Current.Value, () => Is.EqualTo("-5"));
AddAssert("current changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
AddStep("release left mouse", () => InputManager.ReleaseButton(MouseButton.Left));
AddAssert("textbox not changed", () => textBox.Current.Value, () => Is.EqualTo("-5"));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
AddStep("focus textbox", () => ((IFocusManager)InputManager).ChangeFocus(textBox));
AddStep("set text to invalid", () => textBox.Text = "garbage");
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
AddStep("commit text", () => InputManager.Key(Key.Enter));
AddAssert("text restored", () => textBox.Text, () => Is.EqualTo("-5"));
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
AddStep("focus textbox", () => ((IFocusManager)InputManager).ChangeFocus(textBox));
AddStep("set text to invalid", () => textBox.Text = "garbage");
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
AddStep("lose focus", () => ((IFocusManager)InputManager).ChangeFocus(null));
AddAssert("text restored", () => textBox.Text, () => Is.EqualTo("-5"));
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
}
}
}
@@ -23,6 +23,8 @@ namespace osu.Game.Tournament.Tests.Screens
{
FullName = { Value = @"Japan" },
Acronym = { Value = "JPN" },
Seed = { Value = "#28" },
LastYearPlacing = { Value = "#17-24" },
SeedingResults =
{
new SeedingResult
@@ -36,20 +38,38 @@ namespace osu.Game.Tournament.Tests.Screens
Seed = { Value = 8 }
}
}
},
new TournamentTeam
{
Acronym = { Value = "USA" },
FlagName = { Value = "US" },
FullName = { Value = "United States" },
}
}
};
[Test]
public void TestBasic()
[BackgroundDependencyLoader]
private void load()
{
AddStep("create seeding screen", () => Add(new SeedingScreen
Add(new SeedingScreen
{
FillMode = FillMode.Fit,
FillAspectRatio = 16 / 9f
}));
});
}
AddStep("set team to Japan", () => this.ChildrenOfType<SettingsTeamDropdown>().Single().Current.Value = ladder.Teams.Single());
[Test]
public void TestBasic()
{
AddStep("set team to Japan", () =>
this.ChildrenOfType<SettingsTeamDropdown>().Single().Current.Value = ladder.Teams.Single(t => t.FullName.Value == "Japan"));
}
[Test]
public void TestNoSeed()
{
AddStep("set team to USA", () =>
this.ChildrenOfType<SettingsTeamDropdown>().Single().Current.Value = ladder.Teams.Single(t => t.FullName.Value == "United States"));
}
}
}
@@ -66,7 +66,7 @@ namespace osu.Game.Tournament.Tests
Acronym = { Value = "JPN" },
FlagName = { Value = "JP" },
FullName = { Value = "Japan" },
LastYearPlacing = { Value = 10 },
LastYearPlacing = { Value = "#10" },
Seed = { Value = "#12" },
SeedingResults =
{
+36 -5
View File
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Diagnostics;
using System.Linq;
using Newtonsoft.Json;
using osu.Framework.Bindables;
@@ -49,11 +50,9 @@ namespace osu.Game.Tournament.Models
public Bindable<string> Seed = new Bindable<string>(string.Empty);
public Bindable<int> LastYearPlacing = new BindableInt
{
MinValue = 0,
MaxValue = 256
};
[JsonProperty]
[JsonConverter(typeof(LastYearPlacingConverter))]
public Bindable<string> LastYearPlacing = new Bindable<string>(@"N/A");
[JsonProperty]
public BindableList<TournamentUser> Players { get; } = new BindableList<TournamentUser>();
@@ -76,5 +75,37 @@ namespace osu.Game.Tournament.Models
}
public override string ToString() => FullName.Value ?? Acronym.Value;
public class LastYearPlacingConverter : JsonConverter
{
public override bool CanConvert(Type objectType) => objectType == typeof(Bindable<string>);
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
=> serializer.Serialize(writer, ((Bindable<string>)value!).Value);
public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
var lastYearPlacing = existingValue as Bindable<string>;
Debug.Assert(lastYearPlacing != null);
switch (reader.TokenType)
{
case JsonToken.String:
lastYearPlacing.Value = (string?)reader.Value ?? lastYearPlacing.Default;
break;
case JsonToken.Integer:
long value = (long)reader.Value!;
lastYearPlacing.Value = value > 0 ? $@"#{value}" : lastYearPlacing.Default;
break;
default:
reader.Read();
break;
}
return lastYearPlacing;
}
}
}
}
@@ -10,9 +10,7 @@ using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osu.Game.Overlays.Settings;
using osu.Game.Tournament.Models;
@@ -111,43 +109,43 @@ namespace osu.Game.Tournament.Screens.Editors
new SettingsTextBox
{
LabelText = "Name",
Width = 0.2f,
Width = 0.33f,
Current = Model.FullName
},
acronymTextBox = new SettingsTextBox
{
LabelText = "Acronym",
Width = 0.2f,
Width = 0.25f,
Current = Model.Acronym
},
new SettingsTextBox
{
LabelText = "Flag",
Width = 0.2f,
Width = 0.25f,
Current = Model.FlagName
},
new SettingsTextBox
{
LabelText = "Seed",
Width = 0.2f,
Current = Model.Seed
},
new SettingsSlider<int, LastYearPlacementSlider>
{
LabelText = "Last Year Placement",
Width = 0.33f,
Current = Model.LastYearPlacing
},
new SettingsButton
{
Width = 0.2f,
Margin = new MarginPadding(10),
Width = 0.33f,
Margin = new MarginPadding { Top = 20 },
Text = "Edit seeding results",
Action = () =>
{
sceneManager?.SetScreen(new SeedingEditorScreen(team, parent));
}
},
new SettingsTextBox
{
LabelText = "Seed",
Width = 0.25f,
Current = Model.Seed
},
new SettingsTextBox
{
LabelText = "Last Year Placement",
Width = 0.25f,
Current = Model.LastYearPlacing
},
playerEditor,
new SettingsButton
{
@@ -200,11 +198,6 @@ namespace osu.Game.Tournament.Screens.Editors
}, true);
}
private partial class LastYearPlacementSlider : RoundedSliderBar<int>
{
public override LocalisableString TooltipText => Current.Value == 0 ? "N/A" : base.TooltipText;
}
public partial class PlayerEditor : CompositeDrawable
{
private readonly TournamentTeam team;
@@ -274,7 +274,7 @@ namespace osu.Game.Tournament.Screens.TeamIntro
new TeamDisplay(team) { Margin = new MarginPadding { Bottom = 30 } },
new RowDisplay("Average Rank:", $"#{team.AverageRank:#,0}"),
new RowDisplay("Seed:", team.Seed.Value),
new RowDisplay("Last year's placing:", team.LastYearPlacing.Value > 0 ? $"#{team.LastYearPlacing:#,0}" : "N/A"),
new RowDisplay("Last year's placing:", team.LastYearPlacing.Value),
new Container { Margin = new MarginPadding { Bottom = 30 } },
}
},
+5 -1
View File
@@ -334,7 +334,11 @@ namespace osu.Game.Beatmaps
/// <returns>A matching local beatmap info if existing and in a valid state.</returns>
public BeatmapInfo? QueryOnlineBeatmapId(int id) => Realm.Run(r =>
r.All<BeatmapInfo>()
.ForOnlineId(id).SingleOrDefault()?.Detach());
.ForOnlineId(id)
// See https://github.com/ppy/osu/issues/36234 for why this isn't a SingleOrDefault().
.FirstOrDefault()
?.Detach()
);
/// <summary>
/// A default representation of a WorkingBeatmap to use when no beatmap is available.
@@ -467,7 +467,6 @@ namespace osu.Game.Beatmaps.Drawables
@"2055329 miraie & blackwinterwells - facade.osz",
@"2069877 Sephid - Thunderstrike 1988.osz",
@"2119716 Aethoro - Snowy.osz",
@"2120379 Synthion - VIVIDVELOCITY.osz",
@"2124805 Frums (unknown ""lambda"") - 19ZZ.osz",
@"2127811 Wiklund - Joy of Living (Cut Ver.).osz",
};
@@ -2,10 +2,10 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Utils;
namespace osu.Game.Beatmaps.Drawables.Cards.Statistics
{
@@ -18,7 +18,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Statistics
this.dateTime = dateTime;
Icon = FontAwesome.Regular.CheckCircle;
Text = dateTime.ToLocalisableString(@"d MMM yyyy");
Text = dateTime.ToLocalisedMediumDate();
}
public override object TooltipContent => dateTime;
@@ -4,7 +4,6 @@
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@@ -12,7 +11,6 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Overlays;
using osu.Game.Utils;
using osuTK;
using osuTK.Graphics;
@@ -43,6 +41,12 @@ namespace osu.Game.Beatmaps.Drawables
/// </summary>
public Color4 DisplayedDifficultyColour => background.Colour;
/// <summary>
/// The difficulty text colour currently displayed.
/// Can be used to have other components match the spectrum animation.
/// </summary>
public Color4 DisplayedDifficultyTextColour => starsText.Colour;
private readonly Bindable<double> displayedStars = new BindableDouble();
/// <summary>
@@ -54,9 +58,6 @@ namespace osu.Game.Beatmaps.Drawables
[Resolved]
private OsuColour colours { get; set; } = null!;
[Resolved]
private OverlayColourProvider? colourProvider { get; set; }
/// <summary>
/// Creates a new <see cref="StarRatingDisplay"/> using an already computed <see cref="StarDifficulty"/>.
/// </summary>
@@ -160,8 +161,8 @@ namespace osu.Game.Beatmaps.Drawables
background.Colour = colours.ForStarDifficulty(s.NewValue);
starIcon.Colour = s.NewValue >= OsuColour.STAR_DIFFICULTY_DEFINED_COLOUR_CUTOFF ? colours.Orange1 : colourProvider?.Background5 ?? Color4Extensions.FromHex("303d47");
starsText.Colour = s.NewValue >= OsuColour.STAR_DIFFICULTY_DEFINED_COLOUR_CUTOFF ? colours.Orange1 : colourProvider?.Background5 ?? Color4.Black.Opacity(0.75f);
starIcon.Colour = colours.ForStarDifficultyText(s.NewValue);
starsText.Colour = colours.ForStarDifficultyText(s.NewValue);
}, true);
}
}
@@ -10,6 +10,7 @@ using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Game.Collections;
using osu.Game.IO.Legacy;
using osu.Game.Localisation;
using osu.Game.Overlays.Notifications;
namespace osu.Game.Database
@@ -63,7 +64,7 @@ namespace osu.Game.Database
var notification = new ProgressNotification
{
State = ProgressNotificationState.Active,
Text = "Collections import is initialising..."
Text = NotificationsStrings.CollectionsImportInitialising,
};
PostNotification?.Invoke(notification);
@@ -71,7 +72,7 @@ namespace osu.Game.Database
var importedCollections = readCollections(stream, notification);
await importCollections(importedCollections).ConfigureAwait(false);
notification.CompletionText = $"Imported {importedCollections.Count} collections";
notification.CompletionText = NotificationsStrings.CollectionsImportProgress(importedCollections.Count);
notification.State = ProgressNotificationState.Completed;
}
@@ -115,7 +116,7 @@ namespace osu.Game.Database
{
if (notification != null)
{
notification.Text = "Reading collections...";
notification.Text = NotificationsStrings.ReadingCollections;
notification.Progress = 0;
}
@@ -150,7 +151,7 @@ namespace osu.Game.Database
if (notification != null)
{
notification.Text = $"Imported {i + 1} of {collectionCount} collections";
notification.Text = NotificationsStrings.CollectionsImportProgressTotal(i + 1, collectionCount);
notification.Progress = (float)(i + 1) / collectionCount;
}
+2 -1
View File
@@ -9,6 +9,7 @@ using System.Threading.Tasks;
using Humanizer;
using osu.Framework.Logging;
using osu.Game.Extensions;
using osu.Game.Localisation;
using osu.Game.Online.API;
using osu.Game.Overlays.Notifications;
@@ -55,7 +56,7 @@ namespace osu.Game.Database
DownloadNotification notification = new DownloadNotification
{
Text = $"Downloading {request.Model.GetDisplayString()}",
Text = NotificationsStrings.Downloading(request.Model.GetDisplayString()),
};
request.DownloadProgressed += progress =>
+12 -2
View File
@@ -8,6 +8,8 @@ using System.Threading;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Extensions.ExceptionExtensions;
using osu.Framework.Logging;
using osu.Game.Online.API;
namespace osu.Game.Database
@@ -81,7 +83,7 @@ namespace osu.Game.Database
pendingTasks.Enqueue((id, tcs));
// Create a request task if there's not already one.
if (pendingRequestTask == null)
if (pendingRequestTask == null || pendingRequestTask.IsFaulted)
createNewTask();
return tcs.Task;
@@ -163,6 +165,14 @@ namespace osu.Game.Database
}
}
private void createNewTask() => pendingRequestTask = Task.Run(performLookup);
private void createNewTask()
{
var nextTask = Task.Run(performLookup);
nextTask.ContinueWith(t =>
{
Logger.Error(t.Exception.AsSingular(), $"{nameof(OnlineLookupCache<TLookup, TValue, TRequest>)} lookup request failed!");
}, TaskContinuationOptions.OnlyOnFaulted);
pendingRequestTask = nextTask;
}
}
}
@@ -220,16 +220,18 @@ namespace osu.Game.Graphics.Cursor
{
activeCursor.FadeTo(1, 250, Easing.OutQuint);
activeCursor.ScaleTo(1, 400, Easing.OutQuint);
activeCursor.RotateTo(0, 400, Easing.OutQuint);
dragRotationState = DragRotationState.NotDragging;
if (dragRotationState == DragRotationState.NotDragging)
activeCursor.RotateTo(0, 400, Easing.OutQuint);
}
protected override void PopOut()
{
activeCursor.FadeTo(0, 250, Easing.OutQuint);
activeCursor.ScaleTo(0.6f, 250, Easing.In);
activeCursor.RotateTo(0, 400, Easing.OutQuint);
dragRotationState = DragRotationState.NotDragging;
if (dragRotationState == DragRotationState.NotDragging)
activeCursor.RotateTo(0, 400, Easing.OutQuint);
}
private void playTapSample(double baseFrequency = 1f)
+28
View File
@@ -25,6 +25,11 @@ namespace osu.Game.Graphics
/// </summary>
public const float STAR_DIFFICULTY_DEFINED_COLOUR_CUTOFF = 6.5f;
/// <summary>
/// Star rating at which display text switches from static colours to a gradient.
/// </summary>
public const float STAR_DIFFICULTY_TEXT_GRADIENT_CUTOFF = 9.0f;
public static readonly (float, Color4)[] STAR_DIFFICULTY_SPECTRUM =
{
(0.1f, Color4Extensions.FromHex("aaaaaa")),
@@ -42,11 +47,34 @@ namespace osu.Game.Graphics
(10.0f, Color4.Black),
};
public static readonly (float, Color4)[] STAR_DIFFICULTY_TEXT_SPECTRUM =
{
(9.0f, Color4Extensions.FromHex("f6f05c")),
(9.9f, Color4Extensions.FromHex("ff8068")),
(10.6f, Color4Extensions.FromHex("ff4e6f")),
(11.5f, Color4Extensions.FromHex("c645b8")),
(12.4f, Color4Extensions.FromHex("6563de")),
};
/// <summary>
/// Retrieves the colour for a given point in the star range.
/// </summary>
public Color4 ForStarDifficulty(double starDifficulty) => ColourUtils.SampleFromLinearGradient(STAR_DIFFICULTY_SPECTRUM, (float)Math.Round(starDifficulty, 2, MidpointRounding.AwayFromZero));
/// <summary>
/// Retrieves the colour for the text inside the star rating display.
/// </summary>
public Color4 ForStarDifficultyText(double starDifficulty)
{
if (starDifficulty < STAR_DIFFICULTY_DEFINED_COLOUR_CUTOFF)
return Color4.Black.Opacity(0.75f);
if (starDifficulty < STAR_DIFFICULTY_TEXT_GRADIENT_CUTOFF)
return Orange1;
return ColourUtils.SampleFromLinearGradient(STAR_DIFFICULTY_TEXT_SPECTRUM, (float)Math.Round(starDifficulty, 2, MidpointRounding.AwayFromZero));
}
/// <summary>
/// Retrieves the colour for a <see cref="ScoreRank"/>.
/// </summary>
@@ -232,14 +232,14 @@ namespace osu.Game.Graphics.UserInterface
private void updateFpsDisplay()
{
counterDrawFPS.Colour = getColour(displayedFpsCount / aimDrawFPS);
counterDrawFPS.Text = $"{displayedFpsCount:#,0}fps";
counterDrawFPS.Text = $"{displayedFpsCount:#,0} fps";
}
private void updateFrameTimeDisplay()
{
counterUpdateFrameTime.Text = displayedFrameTime < 5
? $"{displayedFrameTime:N1}ms"
: $"{displayedFrameTime:N0}ms";
? $"{displayedFrameTime:N1} ms"
: $"{displayedFrameTime:N0} ms";
counterUpdateFrameTime.Colour = getColour((1000 / displayedFrameTime) / aimUpdateFPS);
}
@@ -82,7 +82,7 @@ namespace osu.Game.Graphics.UserInterface
? $"/{(clock.MaximumUpdateHz > 0 && clock.MaximumUpdateHz < 10000 ? clock.MaximumUpdateHz.ToString("0") : ""),4}"
: string.Empty;
textFlow.AddParagraph($"{clock.FramesPerSecond:0}{maximum}fps ({clock.ElapsedFrameTime:0.00}ms)");
textFlow.AddParagraph($"{clock.FramesPerSecond:0}{maximum} fps ({clock.ElapsedFrameTime:0.00} ms)");
}
}
}
@@ -92,11 +92,11 @@ namespace osu.Game.Graphics.UserInterface
{
base.LoadComplete();
Colour = dimColour;
Enabled.BindValueChanged(_ => this.FadeColour(dimColour, 200, Easing.OutQuint));
Enabled.BindValueChanged(_ => content.FadeColour(DimColour, 200, Easing.OutQuint), true);
FinishTransforms(true);
}
private Color4 dimColour => Enabled.Value ? Color4.White : colours.Gray9;
protected virtual Colour4 DimColour => Enabled.Value ? Color4.White : colours.Gray9;
protected override bool OnHover(HoverEvent e)
{
+136 -39
View File
@@ -4,6 +4,7 @@
using System;
using System.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
@@ -12,6 +13,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Graphics.Backgrounds;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
@@ -28,62 +30,133 @@ namespace osu.Game.Graphics.UserInterfaceV2
/// </summary>
public LocalisableString Caption { get; init; }
/// <summary>
/// Sets text inside the button.
/// </summary>
public LocalisableString ButtonText { get; init; }
public Action? Action { get; init; }
/// <summary>
/// Sets a custom button icon. Not shown when <see cref="ButtonText"/> is set.
/// </summary>
public IconUsage ButtonIcon { get; init; } = FontAwesome.Solid.ChevronRight;
private readonly Color4? backgroundColour;
/// <summary>
/// Sets a custom background colour for the button.
/// </summary>
public Color4? BackgroundColour
{
get => backgroundColour;
init
{
backgroundColour = value;
if (IsLoaded)
updateState();
}
}
/// <summary>
/// The action to invoke when the button is clicked.
/// </summary>
public Action? Action { get; set; }
/// <summary>
/// Whether the button is enabled.
/// </summary>
public readonly BindableBool Enabled = new BindableBool(true);
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
private Container content = null!;
private Box background = null!;
private OsuTextFlowContainer text = null!;
private Button button = null!;
[BackgroundDependencyLoader]
private void load()
{
RelativeSizeAxes = Axes.X;
Height = 50;
AutoSizeAxes = Axes.Y;
Masking = true;
CornerRadius = 5;
CornerExponent = 2.5f;
InternalChildren = new Drawable[]
InternalChild = content = new Container
{
new Box
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Masking = true,
CornerRadius = 5,
CornerExponent = 2.5f,
Children = new Drawable[]
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background5,
},
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding
background = new Box
{
Left = 9,
Right = 5,
Vertical = 5,
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background5,
},
Children = new Drawable[]
new TrianglesV2
{
new OsuTextFlowContainer
SpawnRatio = 0.5f,
RelativeSizeAxes = Axes.Both,
Colour = ColourInfo.GradientVertical(colourProvider.Background4, colourProvider.Background5),
},
new HoverClickSounds(HoverSampleSet.Button)
{
Enabled = { BindTarget = Enabled },
},
new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Width = 0.45f,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Text = Caption,
Left = 9,
Right = 5,
Vertical = 5,
},
new Button
Children = new Drawable[]
{
Action = Action,
Text = ButtonText,
RelativeSizeAxes = ButtonText == default ? Axes.None : Axes.X,
Width = ButtonText == default ? 90 : 0.45f,
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
}
text = new OsuTextFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Text = Caption,
},
button = new Button
{
Action = () => Action?.Invoke(),
Text = ButtonText,
Icon = ButtonIcon,
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Enabled = { BindTarget = Enabled },
}
},
},
},
}
};
if (ButtonText == default)
{
text.Padding = new MarginPadding { Right = 100 };
button.Width = 90;
}
else
{
text.Width = 0.55f;
text.Padding = new MarginPadding { Right = 10 };
button.RelativeSizeAxes = Axes.X;
button.Width = 0.45f;
}
}
protected override void LoadComplete()
{
base.LoadComplete();
Enabled.BindValueChanged(_ => updateState(), true);
}
protected override bool OnHover(HoverEvent e)
@@ -98,12 +171,34 @@ namespace osu.Game.Graphics.UserInterfaceV2
updateState();
}
protected override bool OnClick(ClickEvent e)
{
if (Enabled.Value)
{
background.FlashColour(ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark2), 800, Easing.OutQuint);
button.TriggerClick();
}
return true;
}
private void updateState()
{
BorderThickness = IsHovered ? 2 : 0;
text.Colour = Enabled.Value ? colourProvider.Content1 : colourProvider.Background1;
if (IsHovered)
BorderColour = colourProvider.Light4;
background.FadeColour(IsHovered
? ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark4)
: colourProvider.Background5, 200, Easing.OutQuint);
content.BorderThickness = IsHovered ? 2 : 0;
if (BackgroundColour != null)
{
button.BackgroundColour = BackgroundColour.Value;
content.BorderColour = Enabled.Value ? BackgroundColour.Value : Interpolation.ValueAt(0.75, BackgroundColour.Value, colourProvider.Dark1, 0, 1);
}
else
content.BorderColour = Enabled.Value ? colourProvider.Light4 : colourProvider.Dark1;
}
public partial class Button : OsuButton
@@ -125,6 +220,8 @@ namespace osu.Game.Graphics.UserInterfaceV2
}
}
public IconUsage Icon { get; init; }
[BackgroundDependencyLoader]
private void load(OverlayColourProvider overlayColourProvider)
{
@@ -135,7 +232,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
{
Add(new SpriteIcon
{
Icon = FontAwesome.Solid.ChevronRight,
Icon = Icon,
Size = new Vector2(16),
Shadow = true,
Anchor = Anchor.Centre,
@@ -18,6 +18,7 @@ using osu.Framework.Localisation;
using osu.Game.Graphics.Sprites;
using osu.Game.Localisation;
using osu.Game.Overlays;
using osuTK;
namespace osu.Game.Graphics.UserInterfaceV2
{
@@ -56,7 +57,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
private void load(AudioManager audio)
{
RelativeSizeAxes = Axes.X;
Height = 50;
AutoSizeAxes = Axes.Y;
Masking = true;
CornerRadius = 5;
@@ -71,22 +72,30 @@ namespace osu.Game.Graphics.UserInterfaceV2
},
new Container
{
RelativeSizeAxes = Axes.Both,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding(9),
Children = new Drawable[]
{
caption = new FormFieldCaption
{
Caption = Caption,
TooltipText = HintText,
Anchor = Anchor.TopLeft,
Origin = Anchor.TopLeft,
},
text = new OsuSpriteText
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Padding = new MarginPadding { Right = SwitchButton.WIDTH + 5 },
Spacing = new Vector2(0f, 4f),
Children = new Drawable[]
{
caption = new FormFieldCaption
{
Caption = Caption,
TooltipText = HintText,
},
text = new OsuSpriteText
{
RelativeSizeAxes = Axes.X,
},
},
},
new SwitchButton
{
@@ -97,7 +106,6 @@ namespace osu.Game.Graphics.UserInterfaceV2
},
},
};
sampleChecked = audio.Samples.Get(@"UI/check-on");
sampleUnchecked = audio.Samples.Get(@"UI/check-off");
sampleDisabled = audio.Samples.Get(@"UI/default-select-disabled");
@@ -7,6 +7,7 @@ using osu.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
@@ -14,6 +15,7 @@ using osu.Framework.Localisation;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osu.Game.Resources.Localisation.Web;
using osuTK;
namespace osu.Game.Graphics.UserInterfaceV2
@@ -140,30 +142,32 @@ namespace osu.Game.Graphics.UserInterfaceV2
[BackgroundDependencyLoader]
private void load()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.None;
Height = 50;
Masking = true;
CornerRadius = 5;
Foreground.AutoSizeAxes = Axes.None;
Foreground.RelativeSizeAxes = Axes.Both;
Foreground.Padding = new MarginPadding(9);
Foreground.Children = new Drawable[]
{
caption = new FormFieldCaption
{
Anchor = Anchor.TopLeft,
Origin = Anchor.TopLeft,
Caption = Caption,
TooltipText = HintText,
},
label = new OsuSpriteText
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 4),
Children = new Drawable[]
{
caption = new FormFieldCaption
{
Caption = Caption,
TooltipText = HintText,
},
label = new TruncatingSpriteText
{
RelativeSizeAxes = Axes.X,
Padding = new MarginPadding { Right = 25 },
AlwaysPresent = true,
},
}
},
chevron = new SpriteIcon
{
@@ -212,8 +216,6 @@ namespace osu.Game.Graphics.UserInterfaceV2
private void updateState()
{
label.Alpha = string.IsNullOrEmpty(SearchBar.SearchTerm.Value) ? 1 : 0;
caption.Colour = Dropdown.Current.Disabled ? colourProvider.Background1 : colourProvider.Content2;
label.Colour = Dropdown.Current.Disabled ? colourProvider.Background1 : colourProvider.Content1;
chevron.Colour = Dropdown.Current.Disabled ? colourProvider.Background1 : colourProvider.Content1;
@@ -221,6 +223,11 @@ namespace osu.Game.Graphics.UserInterfaceV2
bool dropdownOpen = Dropdown.Menu.State == MenuState.Open;
if (dropdownOpen)
label.Alpha = AlwaysShowSearchBar || !string.IsNullOrEmpty(SearchBar.SearchTerm.Value) ? 0 : 1;
else
label.Alpha = 1;
BorderThickness = IsHovered || dropdownOpen ? 2 : 0;
if (Dropdown.Current.Disabled)
@@ -250,7 +257,10 @@ namespace osu.Game.Graphics.UserInterfaceV2
protected override void PopIn() => this.FadeIn();
protected override void PopOut() => this.FadeOut();
protected override TextBox CreateTextBox() => TextBox = new FormTextBox.InnerTextBox();
protected override TextBox CreateTextBox() => TextBox = new FormTextBox.InnerTextBox
{
PlaceholderText = HomeStrings.SearchPlaceholder,
};
[BackgroundDependencyLoader]
private void load()
@@ -258,7 +268,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
TextBox.Anchor = Anchor.BottomLeft;
TextBox.Origin = Anchor.BottomLeft;
TextBox.RelativeSizeAxes = Axes.X;
TextBox.Margin = new MarginPadding(9);
Padding = new MarginPadding { Left = 9, Bottom = 9, Right = 34 };
}
}
@@ -2,19 +2,20 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.Containers;
using osuTK;
namespace osu.Game.Graphics.UserInterfaceV2
{
public partial class FormFieldCaption : CompositeDrawable, IHasTooltip
{
private OsuTextFlowContainer textFlow = null!;
private LocalisableString caption;
public LocalisableString Caption
@@ -24,45 +25,60 @@ namespace osu.Game.Graphics.UserInterfaceV2
{
caption = value;
if (captionText.IsNotNull())
captionText.Text = value;
if (IsLoaded)
updateDisplay();
}
}
private OsuSpriteText captionText = null!;
private LocalisableString tooltipText;
public LocalisableString TooltipText { get; set; }
public LocalisableString TooltipText
{
get => tooltipText;
set
{
tooltipText = value;
if (IsLoaded)
updateDisplay();
}
}
[BackgroundDependencyLoader]
private void load()
{
AutoSizeAxes = Axes.Both;
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
InternalChild = new FillFlowContainer
InternalChild = textFlow = new OsuTextFlowContainer(t => t.Font = OsuFont.Default.With(size: 12, weight: FontWeight.SemiBold))
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(5),
Children = new Drawable[]
{
captionText = new OsuSpriteText
{
Text = caption,
Font = OsuFont.Default.With(size: 12, weight: FontWeight.SemiBold),
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
new SpriteIcon
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Alpha = TooltipText == default ? 0 : 1,
Size = new Vector2(10),
Icon = FontAwesome.Solid.QuestionCircle,
Margin = new MarginPadding { Top = 1, },
}
},
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
};
}
protected override void LoadComplete()
{
base.LoadComplete();
updateDisplay();
}
private void updateDisplay()
{
textFlow.Text = caption;
if (TooltipText != default)
{
textFlow.AddArbitraryDrawable(new SpriteIcon
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Size = new Vector2(10),
Icon = FontAwesome.Solid.QuestionCircle,
Margin = new MarginPadding { Left = 5 },
Y = 1f,
});
}
}
}
}
@@ -117,34 +117,46 @@ namespace osu.Game.Graphics.UserInterfaceV2
new Container
{
RelativeSizeAxes = Axes.X,
Height = 50,
AutoSizeAxes = Axes.Y,
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Padding = new MarginPadding(9),
Children = new Drawable[]
{
caption = new FormFieldCaption
new FillFlowContainer
{
Anchor = Anchor.TopLeft,
Origin = Anchor.TopLeft,
Caption = Caption,
TooltipText = HintText,
},
placeholderText = new OsuSpriteText
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
RelativeSizeAxes = Axes.X,
Width = 1,
Text = PlaceholderText,
Colour = colourProvider.Foreground1,
},
filenameText = new OsuSpriteText
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
RelativeSizeAxes = Axes.X,
Width = 1,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0f, 4f),
Children = new Drawable[]
{
caption = new FormFieldCaption
{
Caption = Caption,
TooltipText = HintText,
},
new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Children = new[]
{
placeholderText = new OsuSpriteText
{
RelativeSizeAxes = Axes.X,
Width = 1,
Text = PlaceholderText,
Colour = colourProvider.Foreground1,
},
filenameText = new OsuSpriteText
{
RelativeSizeAxes = Axes.X,
Width = 1,
},
}
}
},
},
new SpriteIcon
{
@@ -242,7 +254,8 @@ namespace osu.Game.Graphics.UserInterfaceV2
Task ICanAcceptFiles.Import(ImportTask[] tasks, ImportParameters parameters) => throw new NotImplementedException();
protected virtual FileChooserPopover CreatePopover(string[] handledExtensions, Bindable<FileInfo?> current, string? chooserPath) => new FileChooserPopover(handledExtensions, current, chooserPath);
protected virtual FileChooserPopover CreatePopover(string[] handledExtensions, Bindable<FileInfo?> current, string? chooserPath) =>
new FileChooserPopover(handledExtensions, current, chooserPath);
public Popover GetPopover()
{
@@ -22,6 +22,7 @@ using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
using osu.Game.Overlays;
using Vector2 = osuTK.Vector2;
namespace osu.Game.Graphics.UserInterfaceV2
{
@@ -133,6 +134,8 @@ namespace osu.Game.Graphics.UserInterfaceV2
private readonly Bindable<Language> currentLanguage = new Bindable<Language>();
public bool TakeFocus() => GetContainingFocusManager()?.ChangeFocus(textBox) == true;
public FormSliderBar()
{
LabelFormat ??= defaultLabelFormat;
@@ -143,7 +146,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
private void load(OsuColour colours, OsuGame? game)
{
RelativeSizeAxes = Axes.X;
Height = 50;
AutoSizeAxes = Axes.Y;
Masking = true;
CornerRadius = 5;
@@ -162,47 +165,64 @@ namespace osu.Game.Graphics.UserInterfaceV2
},
new Container
{
RelativeSizeAxes = Axes.Both,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding
{
Vertical = 9,
Vertical = 5,
Left = 9,
Right = 5,
},
Children = new Drawable[]
{
captionText = new FormFieldCaption
new FillFlowContainer
{
Anchor = Anchor.TopLeft,
Origin = Anchor.TopLeft,
TooltipText = HintText,
},
textBox = new FormNumberBox.InnerNumberBox(allowDecimals: true)
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0f, 4f),
Width = 0.5f,
// the textbox is hidden when the control is unfocused,
// but clicking on the label should reach the textbox,
// therefore make it always present.
AlwaysPresent = true,
CommitOnFocusLost = true,
SelectAllOnFocus = true,
OnInputError = () =>
Padding = new MarginPadding
{
flashLayer.Colour = ColourInfo.GradientVertical(colours.Red3.Opacity(0), colours.Red3);
flashLayer.FadeOutFromOne(200, Easing.OutQuint);
Right = 10,
Vertical = 4,
},
Children = new Drawable[]
{
captionText = new FormFieldCaption
{
TooltipText = HintText,
},
new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Children = new Drawable[]
{
textBox = new FormNumberBox.InnerNumberBox(allowDecimals: true)
{
RelativeSizeAxes = Axes.X,
// the textbox is hidden when the control is unfocused,
// but clicking on the label should reach the textbox,
// therefore make it always present.
AlwaysPresent = true,
CommitOnFocusLost = true,
SelectAllOnFocus = true,
OnInputError = () =>
{
flashLayer.Colour = ColourInfo.GradientVertical(colours.Red3.Opacity(0), colours.Red3);
flashLayer.FadeOutFromOne(200, Easing.OutQuint);
},
TabbableContentContainer = tabbableContentContainer,
},
valueLabel = new TruncatingSpriteText
{
RelativeSizeAxes = Axes.X,
Padding = new MarginPadding { Right = 5 },
},
},
},
},
TabbableContentContainer = tabbableContentContainer,
},
valueLabel = new TruncatingSpriteText
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
RelativeSizeAxes = Axes.X,
Width = 0.5f,
Padding = new MarginPadding { Right = 5 },
},
slider = new InnerSlider
{
@@ -215,6 +235,11 @@ namespace osu.Game.Graphics.UserInterfaceV2
TooltipFormat = TooltipFormat,
DisplayAsPercentage = DisplayAsPercentage,
PlaySamplesOnAdjust = PlaySamplesOnAdjust,
ResetToDefault = () =>
{
if (!IsDisabled)
SetDefault();
}
}
},
},
@@ -382,9 +407,11 @@ namespace osu.Game.Graphics.UserInterfaceV2
{
public BindableBool Focused { get; } = new BindableBool();
public BindableBool IsDragging { get; set; } = new BindableBool();
public BindableBool IsDragging { get; } = new BindableBool();
public Action? OnCommit { get; set; }
public Action? ResetToDefault { get; init; }
public Action? OnCommit { get; init; }
public sealed override LocalisableString TooltipText => base.TooltipText;
@@ -435,11 +462,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
Padding = new MarginPadding { Horizontal = RangePadding, },
Child = nub = new InnerSliderNub
{
ResetToDefault = () =>
{
if (!Current.Disabled)
Current.SetDefault();
}
ResetToDefault = ResetToDefault,
}
},
sounds = new HoverClickSounds()
@@ -19,6 +19,7 @@ using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osuTK;
namespace osu.Game.Graphics.UserInterfaceV2
{
@@ -89,7 +90,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
private void load(OsuColour colours)
{
RelativeSizeAxes = Axes.X;
Height = 50;
AutoSizeAxes = Axes.Y;
Masking = true;
CornerRadius = 5;
@@ -107,10 +108,12 @@ namespace osu.Game.Graphics.UserInterfaceV2
RelativeSizeAxes = Axes.Both,
Colour = Colour4.Transparent,
},
new Container
new FillFlowContainer
{
RelativeSizeAxes = Axes.Both,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding(9),
Spacing = new Vector2(0, 4),
Children = new Drawable[]
{
caption = new FormFieldCaption
@@ -122,8 +125,6 @@ namespace osu.Game.Graphics.UserInterfaceV2
},
textBox = CreateTextBox().With(t =>
{
t.Anchor = Anchor.BottomRight;
t.Origin = Anchor.BottomRight;
t.RelativeSizeAxes = Axes.X;
t.Width = 1;
t.PlaceholderText = PlaceholderText;
@@ -1,151 +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.Numerics;
using System.Globalization;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation;
using osu.Game.Overlays.Settings;
using osu.Game.Utils;
using Vector2 = osuTK.Vector2;
namespace osu.Game.Graphics.UserInterfaceV2
{
public partial class SliderWithTextBoxInput<T> : CompositeDrawable, IHasCurrentValue<T>
where T : struct, INumber<T>, IMinMaxValue<T>
{
/// <summary>
/// A custom step value for each key press which actuates a change on this control.
/// </summary>
public float KeyboardStep
{
get => slider.KeyboardStep;
set => slider.KeyboardStep = value;
}
public Bindable<T> Current
{
get => slider.Current;
set => slider.Current = value;
}
public CompositeDrawable TabbableContentContainer
{
set => textBox.TabbableContentContainer = value;
}
private bool instantaneous;
/// <summary>
/// Whether changes to the slider should instantaneously transfer to the text box (and vice versa).
/// If <see langword="false"/>, the transfer will happen on text box commit (explicit, or implicit via focus loss), or on slider drag end.
/// </summary>
public bool Instantaneous
{
get => instantaneous;
set
{
instantaneous = value;
slider.TransferValueOnCommit = !instantaneous;
}
}
private readonly SettingsSlider<T> slider;
private readonly LabelledTextBox textBox;
public SliderWithTextBoxInput(LocalisableString labelText)
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
InternalChildren = new Drawable[]
{
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(20),
Children = new Drawable[]
{
textBox = new LabelledTextBox
{
Label = labelText,
SelectAllOnFocus = true,
},
slider = new SettingsSlider<T>
{
TransferValueOnCommit = true,
RelativeSizeAxes = Axes.X,
}
}
},
};
textBox.OnCommit += textCommitted;
textBox.Current.BindValueChanged(textChanged);
Current.BindValueChanged(updateTextBoxFromSlider, true);
}
public bool TakeFocus() => GetContainingFocusManager()?.ChangeFocus(textBox) == true;
private bool updatingFromTextBox;
private void textChanged(ValueChangedEvent<string> change)
{
if (!instantaneous) return;
tryUpdateSliderFromTextBox();
}
private void textCommitted(TextBox t, bool isNew)
{
tryUpdateSliderFromTextBox();
// If the attempted update above failed, restore text box to match the slider.
Current.TriggerChange();
}
private void tryUpdateSliderFromTextBox()
{
updatingFromTextBox = true;
try
{
switch (slider.Current)
{
case Bindable<int> bindableInt:
bindableInt.Value = int.Parse(textBox.Current.Value);
break;
case Bindable<double> bindableDouble:
bindableDouble.Value = double.Parse(textBox.Current.Value);
break;
default:
slider.Current.Parse(textBox.Current.Value, CultureInfo.CurrentCulture);
break;
}
}
catch
{
// ignore parsing failures.
// sane state will eventually be restored by a commit (either explicit, or implicit via focus loss).
}
updatingFromTextBox = false;
}
private void updateTextBoxFromSlider(ValueChangedEvent<T> _)
{
if (updatingFromTextBox) return;
decimal decimalValue = decimal.CreateTruncating(slider.Current.Value);
textBox.Text = decimalValue.ToString($@"N{FormatUtils.FindPrecision(decimalValue)}");
}
}
}
@@ -19,6 +19,8 @@ namespace osu.Game.Graphics.UserInterfaceV2
{
public partial class SwitchButton : Checkbox
{
public const float WIDTH = 45;
private const float border_thickness = 4.5f;
private const float padding = 1.25f;
@@ -35,7 +37,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
public SwitchButton()
{
Size = new Vector2(45, 20);
Size = new Vector2(WIDTH, 20);
InternalChild = content = new CircularContainer
{
@@ -135,6 +135,81 @@ Click to see what's new!", version);
/// </summary>
public static LocalisableString Mention => new TranslatableString(getKey(@"mention"), @"Mention");
/// <summary>
/// "Online: {0}"
/// </summary>
public static LocalisableString FriendOnline(string info) => new TranslatableString(getKey(@"friend_online"), @"Online: {0}", info);
/// <summary>
/// "Offline: {0}"
/// </summary>
public static LocalisableString FriendOffline(string info) => new TranslatableString(getKey(@"friend_offline"), @"Offline: {0}", info);
/// <summary>
/// "Connection to online services was interrupted. osu! will be operating with limited functionality."
/// </summary>
public static LocalisableString APIConnectionInterrupted => new TranslatableString(getKey(@"api_connection_interrupted"), @"Connection to online services was interrupted. osu! will be operating with limited functionality.");
/// <summary>
/// "You have been logged out on this device due to a login to your account on another device."
/// </summary>
public static LocalisableString AnotherDeviceDisconnect => new TranslatableString(getKey(@"another_device_disconnect"), @"You have been logged out on this device due to a login to your account on another device.");
/// <summary>
/// "You have been logged out due to a change to your account. Please log in again."
/// </summary>
public static LocalisableString AccountChangeDisconnect => new TranslatableString(getKey(@"account_change_disconnect"), @"You have been logged out due to a change to your account. Please log in again.");
/// <summary>
/// "Downloading {0}"
/// </summary>
public static LocalisableString Downloading(string info) => new TranslatableString(getKey(@"downloading"), @"Downloading {0}", info);
/// <summary>
/// "Collections import is initialising..."
/// </summary>
public static LocalisableString CollectionsImportInitialising => new TranslatableString(getKey(@"collections_import_initialising"), @"Collections import is initialising...");
/// <summary>
/// "Reading collections..."
/// </summary>
public static LocalisableString ReadingCollections => new TranslatableString(getKey(@"reading_collections"), @"Reading collections...");
/// <summary>
/// "Imported {0} collections"
/// </summary>
public static LocalisableString CollectionsImportProgress(int count) => new TranslatableString(getKey(@"collections_import_progress"), @"Imported {0} collections", count);
/// <summary>
/// "Imported {0} of {1} collections"
/// </summary>
public static LocalisableString CollectionsImportProgressTotal(int count, int totalCount) => new TranslatableString(getKey(@"collections_import_progress_total"), @"Imported {0} of {1} collections", count, totalCount);
/// <summary>
/// "This error has been automatically reported to the dev team."
/// </summary>
public static LocalisableString ErrorAutomaticallyReported => new TranslatableString(getKey(@"error_automatically_reported"), @"This error has been automatically reported to the dev team.");
/// <summary>
/// "A newer release of osu! has been found ({0} → {1})."
/// </summary>
public static LocalisableString UpdateAvailable(string oldVersion, string newVersion) => new TranslatableString(getKey(@"update_available"), @"A newer release of osu! has been found ({0} → {1}).", oldVersion, newVersion);
/// <summary>
/// "Click here to download the new version, which can be installed over the top of your existing installation."
/// </summary>
public static LocalisableString UpdateAvailableManualInstall => new TranslatableString(getKey(@"update_available_manual_install"), @"Click here to download the new version, which can be installed over the top of your existing installation.");
/// <summary>
/// "Check with your package manager / provider to bring osu! up-to-date!"
/// </summary>
public static LocalisableString UpdateAvailablePackageManaged => new TranslatableString(getKey(@"update_available_package_managed"), @"Check with your package manager / provider to bring osu! up-to-date!");
/// <summary>
/// "An action was interrupted due to a dialog being displayed."
/// </summary>
public static LocalisableString ActionInterruptedByDialog => new TranslatableString(getKey(@"action_interrupted_by_dialog"), @"An action was interrupted due to a dialog being displayed.");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}
@@ -165,9 +165,9 @@ namespace osu.Game.Localisation
public static LocalisableString NeverRepeat => new TranslatableString(getKey(@"never_repeat_random"), @"Never repeat");
/// <summary>
/// "True Random"
/// "True random"
/// </summary>
public static LocalisableString TrueRandom => new TranslatableString(getKey(@"true_random"), @"True Random");
public static LocalisableString TrueRandom => new TranslatableString(getKey(@"true_random"), @"True random");
/// <summary>
/// "Selected Mods"
@@ -10,21 +10,26 @@ namespace osu.Game.Online.API.Requests
/// Looks up users with the given <see cref="UserIds"/>.
/// In comparison to <see cref="GetUsersRequest"/>, the response here does not contain <see cref="APIUser.RulesetsStatistics"/>,
/// but in exchange is subject to less stringent rate limiting, making it suitable for mass user listings.
///
/// Providing a ruleset ID will give `global_rank`s in the response.
/// </summary>
public class LookupUsersRequest : APIRequest<GetUsersResponse>
{
public readonly int[] UserIds;
public readonly int? RulesetId;
private const int max_ids_per_request = 50;
public LookupUsersRequest(int[] userIds)
public LookupUsersRequest(int[] userIds, int? rulesetId = null)
{
if (userIds.Length > max_ids_per_request)
throw new ArgumentException($"{nameof(LookupUsersRequest)} calls only support up to {max_ids_per_request} IDs at once");
UserIds = userIds;
RulesetId = rulesetId;
}
protected override string Target => @"users/lookup/?ids[]=" + string.Join(@"&ids[]=", UserIds);
protected override string Target => @"users/lookup/?ids[]=" + string.Join(@"&ids[]=", UserIds) + (RulesetId != null ? "&ruleset_id=" + RulesetId : "");
}
}
@@ -2,6 +2,8 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using osu.Game.Beatmaps;
using osu.Game.Extensions;
@@ -115,6 +117,23 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty(@"owners")]
public BeatmapOwner[] BeatmapOwners { get; set; } = Array.Empty<BeatmapOwner>();
public (APITag Tag, int VoteCount)[] GetTopUserTags()
{
if (TopTags == null || TopTags.Length == 0 || BeatmapSet?.RelatedTags == null)
return [];
var tagsById = BeatmapSet.RelatedTags.ToDictionary(t => t.Id);
return TopTags
.Select(t => (topTag: t, relatedTag: tagsById.GetValueOrDefault(t.TagId)))
.Where(t => t.relatedTag != null)
// see https://github.com/ppy/osu-web/blob/bb3bd2e7c6f84f26066df5ea20a81c77ec9bb60a/resources/js/beatmapsets-show/controller.ts#L103-L106 for sort criteria
.OrderByDescending(t => t.topTag.VoteCount)
.ThenBy(t => t.relatedTag!.Name)
.Select(t => (t.relatedTag!, t.topTag.VoteCount))
.ToArray();
}
#region Implementation of IBeatmapInfo
public IBeatmapMetadataInfo Metadata => (BeatmapSet as IBeatmapSetInfo)?.Metadata ?? new BeatmapMetadata();
@@ -247,6 +247,20 @@ namespace osu.Game.Online.API.Requests.Responses
}
}
// Only provided via /users/ batch lookups. Usually implicitly comes inside `UserStatistics`.
[JsonProperty(@"global_rank")]
[CanBeNull]
public GlobalRank Rank { get; set; }
public class GlobalRank
{
[JsonProperty(@"rank")]
public int? Rank;
[JsonProperty(@"ruleset_id")]
public int RulesetId;
}
[JsonProperty(@"rank_history")]
private APIRankHistory rankHistory
{
+84 -62
View File
@@ -11,6 +11,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Localisation;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Chat;
@@ -44,14 +45,24 @@ namespace osu.Game.Online
private readonly HashSet<APIUser> onlineAlertQueue = new HashSet<APIUser>();
private readonly HashSet<APIUser> offlineAlertQueue = new HashSet<APIUser>();
private double? lastOnlineAlertTime;
private double? lastOfflineAlertTime;
private double? nextOnlineAlertTime;
private double? nextOfflineAlertTime;
private const double debounce_time_before_notification = 1000;
protected override void LoadComplete()
{
base.LoadComplete();
config.BindWith(OsuSetting.NotifyOnFriendPresenceChange, notifyOnFriendPresenceChange);
notifyOnFriendPresenceChange.BindValueChanged(_ =>
{
onlineAlertQueue.Clear();
offlineAlertQueue.Clear();
nextOfflineAlertTime = null;
nextOnlineAlertTime = null;
});
friends.BindTo(api.LocalUserState.Friends);
friends.BindCollectionChanged(onFriendsChanged, true);
@@ -64,8 +75,11 @@ namespace osu.Game.Online
{
base.Update();
alertOnlineUsers();
alertOfflineUsers();
if (notifyOnFriendPresenceChange.Value)
{
alertOnlineUsers();
alertOfflineUsers();
}
}
private void onFriendsChanged(object? sender, NotifyCollectionChangedEventArgs e)
@@ -131,7 +145,7 @@ namespace osu.Game.Online
if (!offlineAlertQueue.Remove(user))
{
onlineAlertQueue.Add(user);
lastOnlineAlertTime ??= Time.Current;
nextOnlineAlertTime ??= Time.Current + debounce_time_before_notification;
}
}
@@ -140,110 +154,118 @@ namespace osu.Game.Online
if (!onlineAlertQueue.Remove(user))
{
offlineAlertQueue.Add(user);
lastOfflineAlertTime ??= Time.Current;
nextOfflineAlertTime ??= Time.Current + debounce_time_before_notification;
}
}
private void alertOnlineUsers()
{
if (onlineAlertQueue.Count == 0)
if (nextOnlineAlertTime == null || Time.Current < nextOnlineAlertTime)
return;
if (lastOnlineAlertTime == null || Time.Current - lastOnlineAlertTime < 1000)
return;
// If a user quickly switches online-offline, we might reach here without actually having a notification
// to fire. Importantly, we should still reset the next alert time in such a scenario.
if (!notifyOnFriendPresenceChange.Value)
{
lastOnlineAlertTime = null;
return;
}
notifications.Post(new FriendOnlineNotification(onlineAlertQueue.ToArray()));
if (onlineAlertQueue.Count == 1)
notifications.Post(new SingleFriendOnlineNotification(onlineAlertQueue.Single()));
else if (onlineAlertQueue.Count > 1)
notifications.Post(new MultipleFriendsOnlineNotification(onlineAlertQueue.ToArray()));
onlineAlertQueue.Clear();
lastOnlineAlertTime = null;
nextOnlineAlertTime = null;
}
private void alertOfflineUsers()
{
if (offlineAlertQueue.Count == 0)
if (nextOfflineAlertTime == null || Time.Current < nextOfflineAlertTime)
return;
if (lastOfflineAlertTime == null || Time.Current - lastOfflineAlertTime < 1000)
return;
// If a user quickly switches offline-online, we might reach here without actually having a notification
// to fire. Importantly, we should still reset the next alert time in such a scenario.
if (!notifyOnFriendPresenceChange.Value)
{
lastOfflineAlertTime = null;
return;
}
notifications.Post(new FriendOfflineNotification(offlineAlertQueue.ToArray()));
if (offlineAlertQueue.Count == 1)
notifications.Post(new SingleFriendOfflineNotification(offlineAlertQueue.Single()));
else if (offlineAlertQueue.Count > 1)
notifications.Post(new MultipleFriendsOfflineNotification(offlineAlertQueue.ToArray()));
offlineAlertQueue.Clear();
lastOfflineAlertTime = null;
nextOfflineAlertTime = null;
}
public partial class FriendOnlineNotification : UserAvatarNotification
private partial class SingleFriendOnlineNotification : UserAvatarNotification
{
private readonly ICollection<APIUser> users;
public FriendOnlineNotification(ICollection<APIUser> users)
: base(users.Count == 1 ? users.Single() : null)
public SingleFriendOnlineNotification(APIUser user)
: base(user)
{
this.users = users;
Transient = true;
IsImportant = false;
Text = $"Online: {string.Join(@", ", users.Select(u => u.Username))}";
Text = NotificationsStrings.FriendOnline(User.Username);
}
[BackgroundDependencyLoader]
private void load(OsuColour colours, ChannelManager channelManager, ChatOverlay chatOverlay)
private void load(ChannelManager channelManager, ChatOverlay chatOverlay)
{
if (users.Count > 1)
Activated = () =>
{
Icon = FontAwesome.Solid.User;
IconColour = colours.GrayD;
}
else
{
Activated = () =>
{
channelManager.OpenPrivateChannel(users.Single());
chatOverlay.Show();
channelManager.OpenPrivateChannel(User);
chatOverlay.Show();
return true;
};
}
return true;
};
}
public override string PopInSampleName => "UI/notification-friend-online";
}
public partial class FriendOfflineNotification : UserAvatarNotification
private partial class MultipleFriendsOnlineNotification : SimpleNotification
{
private readonly ICollection<APIUser> users;
public FriendOfflineNotification(ICollection<APIUser> users)
: base(users.Count == 1 ? users.Single() : null)
public MultipleFriendsOnlineNotification(ICollection<APIUser> users)
{
this.users = users;
Text = NotificationsStrings.FriendOnline(string.Join(@", ", users.Select(u => u.Username)));
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Icon = FontAwesome.Solid.User;
IconColour = colours.Green;
}
public override string PopInSampleName => "UI/notification-friend-online";
}
private partial class SingleFriendOfflineNotification : UserAvatarNotification
{
public SingleFriendOfflineNotification(APIUser user)
: base(user)
{
Transient = true;
IsImportant = false;
Text = $"Offline: {string.Join(@", ", users.Select(u => u.Username))}";
Text = NotificationsStrings.FriendOffline(User.Username);
}
[BackgroundDependencyLoader]
private void load()
{
Icon = FontAwesome.Solid.UserSlash;
Avatar.Colour = Color4.White.Opacity(0.25f);
}
public override string PopInSampleName => "UI/notification-friend-offline";
}
private partial class MultipleFriendsOfflineNotification : SimpleNotification
{
public MultipleFriendsOfflineNotification(ICollection<APIUser> users)
{
Text = NotificationsStrings.FriendOffline(string.Join(@", ", users.Select(u => u.Username)));
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Icon = FontAwesome.Solid.UserSlash;
if (users.Count == 1)
Avatar.Colour = Color4.White.Opacity(0.25f);
else
IconColour = colours.Gray3;
IconColour = colours.Red;
}
public override string PopInSampleName => "UI/notification-friend-offline";
+3 -11
View File
@@ -2,7 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR.Client;
@@ -57,17 +57,9 @@ namespace osu.Game.Online
{
// Configuring proxies is not supported on iOS, see https://github.com/xamarin/xamarin-macios/issues/14632.
if (RuntimeInfo.OS != RuntimeInfo.Platform.iOS)
{
// Use HttpClient.DefaultProxy once on net6 everywhere.
// The credential setter can also be removed at this point.
options.Proxy = WebRequest.DefaultWebProxy;
if (options.Proxy != null)
options.Proxy.Credentials = CredentialCache.DefaultCredentials;
}
options.Proxy = HttpClient.DefaultProxy;
options.Headers.Add(@"Authorization", @$"Bearer {API.AccessToken}");
// non-standard header name kept for backwards compatibility, can be removed after server side has migrated to `VERSION_HASH_HEADER`
options.Headers.Add(@"OsuVersionHash", versionHash);
options.AccessTokenProvider = () => Task.FromResult<string?>(API.AccessToken);
options.Headers.Add(VERSION_HASH_HEADER, versionHash);
options.Headers.Add(CLIENT_SESSION_ID_HEADER, API.SessionIdentifier.ToString());
});
@@ -3,6 +3,7 @@
using System;
using System.Diagnostics;
using System.Net.WebSockets;
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;
using osu.Framework.Extensions.ExceptionExtensions;
@@ -20,13 +21,23 @@ namespace osu.Game.Online.Multiplayer
Debug.Assert(t.Exception != null);
Exception exception = t.Exception.AsSingular();
onError?.Invoke(exception);
if (exception is WebSocketException wse && wse.Message == @"The remote party closed the WebSocket connection without completing the close handshake.")
{
// OnlineStatusNotifier is already letting users know about interruptions to connections.
// Silence these because it gets very spammy otherwise.
return;
}
if (exception.GetHubExceptionMessage() is string message)
{
// Hub exceptions generally contain something we can show the user directly.
Logger.Log(message, level: LogLevel.Important);
else
Logger.Error(exception, $"Unobserved exception occurred via {nameof(FireAndForget)} call: {exception.Message}");
return;
}
onError?.Invoke(exception);
Logger.Error(exception, $"Unobserved exception occurred via {nameof(FireAndForget)} call: {exception.Message}");
}
else
{
+39 -28
View File
@@ -8,6 +8,7 @@ using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Screens;
using osu.Game.Localisation;
using osu.Game.Online.API;
using osu.Game.Online.Metadata;
using osu.Game.Online.Multiplayer;
@@ -16,6 +17,7 @@ using osu.Game.Online.Spectator;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
using osu.Game.Screens.OnlinePlay;
using osu.Game.Screens.Play;
namespace osu.Game.Online
{
@@ -74,22 +76,16 @@ namespace osu.Game.Online
apiState.BindValueChanged(state =>
{
if (state.NewValue == APIState.Online)
switch (state.NewValue)
{
userNotified = false;
return;
}
case APIState.Online:
userNotified = false;
return;
if (userNotified) return;
if (state.NewValue == APIState.Offline && getCurrentScreen() is OnlinePlayScreen)
{
userNotified = true;
notificationOverlay?.Post(new SimpleErrorNotification
{
Icon = FontAwesome.Solid.ExclamationCircle,
Text = "Connection to API was lost. Can't continue with online play."
});
case APIState.Offline:
if (getCurrentScreen() is OnlinePlayScreen)
notifyApiDisconnection();
break;
}
});
@@ -101,22 +97,37 @@ namespace osu.Game.Online
return;
}
if (userNotified) return;
if (multiplayerClient.Room != null)
{
userNotified = true;
notificationOverlay?.Post(new SimpleErrorNotification
{
Icon = FontAwesome.Solid.ExclamationCircle,
Text = "Connection to the multiplayer server was lost. Exiting multiplayer."
});
}
notifyApiDisconnection();
}));
spectatorState.BindValueChanged(_ =>
spectatorState.BindValueChanged(connected => Schedule(() =>
{
// TODO: handle spectator server failure somehow?
if (connected.NewValue)
{
userNotified = false;
return;
}
switch (getCurrentScreen())
{
case SpectatorPlayer: // obvious issues
case SubmittingPlayer: // replay sending issues
notifyApiDisconnection();
break;
}
}));
}
private void notifyApiDisconnection()
{
if (userNotified) return;
userNotified = true;
notificationOverlay?.Post(new SimpleErrorNotification
{
Icon = FontAwesome.Solid.ExclamationCircle,
Text = NotificationsStrings.APIConnectionInterrupted,
});
}
@@ -128,7 +139,7 @@ namespace osu.Game.Online
notificationOverlay?.Post(new SimpleErrorNotification
{
Icon = FontAwesome.Solid.ExclamationCircle,
Text = "You have been logged out on this device due to a login to your account on another device."
Text = NotificationsStrings.AnotherDeviceDisconnect,
});
}
@@ -142,7 +153,7 @@ namespace osu.Game.Online
notificationOverlay?.Post(new SimpleErrorNotification
{
Icon = FontAwesome.Solid.ExclamationCircle,
Text = "You have been logged out due to a change to your account. Please log in again."
Text = NotificationsStrings.AccountChangeDisconnect,
});
}
+8 -1
View File
@@ -1380,10 +1380,17 @@ namespace osu.Game
if (generalLogRecentCount < short_term_display_limit)
{
LocalisableString message;
if (entry.Exception != null && IsDeployedBuild)
message = LocalisableString.Interpolate($"{entry.Message.Truncate(256)}\n\n{NotificationsStrings.ErrorAutomaticallyReported}");
else
message = entry.Message.Truncate(256);
Schedule(() => Notifications.Post(new SimpleErrorNotification
{
Icon = entry.Level == LogLevel.Important ? FontAwesome.Solid.ExclamationCircle : FontAwesome.Solid.Bomb,
Text = entry.Message.Truncate(256) + (entry.Exception != null && IsDeployedBuild ? "\n\nThis error has been automatically reported to the devs." : string.Empty),
Text = message
}));
}
else if (generalLogRecentCount == short_term_display_limit)
@@ -71,7 +71,7 @@ namespace osu.Game.Overlays.AccountCreation
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Padding = new MarginPadding(20),
Spacing = new Vector2(0, 5),
Spacing = new Vector2(0, 7),
Children = new Drawable[]
{
new Container
+1 -16
View File
@@ -2,7 +2,6 @@
// 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;
@@ -130,21 +129,7 @@ namespace osu.Game.Overlays.BeatmapSet
private void updateUserTags()
{
if (Beatmap.Value?.TopTags == null || Beatmap.Value.TopTags.Length == 0 || BeatmapSet.Value?.RelatedTags == null)
{
userTags.Metadata = null;
return;
}
var tagsById = BeatmapSet.Value.RelatedTags.ToDictionary(t => t.Id);
userTags.Metadata = Beatmap.Value.TopTags
.Select(t => (topTag: t, relatedTag: tagsById.GetValueOrDefault(t.TagId)))
.Where(t => t.relatedTag != null)
// see https://github.com/ppy/osu-web/blob/bb3bd2e7c6f84f26066df5ea20a81c77ec9bb60a/resources/js/beatmapsets-show/controller.ts#L103-L106 for sort criteria
.OrderByDescending(t => t.topTag.VoteCount)
.ThenBy(t => t.relatedTag!.Name)
.Select(t => t.relatedTag!.Name)
.ToArray();
userTags.Metadata = Beatmap.Value?.GetTopUserTags().Select(t => t.Tag.Name).ToArray();
}
[BackgroundDependencyLoader]
@@ -3,7 +3,6 @@
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
@@ -86,7 +85,7 @@ namespace osu.Game.Overlays.Music
private readonly GlobalAction action;
public MusicActionToast(LocalisableString value, GlobalAction action)
: base(ToastStrings.MusicPlayback, value, string.Empty)
: base(ToastStrings.MusicPlayback, value)
{
this.action = action;
}
@@ -94,7 +93,7 @@ namespace osu.Game.Overlays.Music
[BackgroundDependencyLoader]
private void load(RealmKeyBindingStore keyBindingStore)
{
ShortcutText.Text = keyBindingStore.GetBindingsStringFor(action).ToUpper();
ExtraText = keyBindingStore.GetBindingsStringFor(action);
}
}
}
+2 -1
View File
@@ -16,6 +16,7 @@ using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Utils;
namespace osu.Game.Overlays.News
{
@@ -143,7 +144,7 @@ namespace osu.Game.Overlays.News
},
new OsuSpriteText
{
Text = date.ToLocalisableString(@"d MMM yyyy").ToUpper(),
Text = date.ToLocalisedMediumDate().ToUpper(),
Font = OsuFont.GetFont(size: 10, weight: FontWeight.SemiBold),
Margin = new MarginPadding
{
@@ -12,14 +12,13 @@ namespace osu.Game.Overlays.Notifications
{
public abstract partial class UserAvatarNotification : SimpleNotification
{
private readonly APIUser? user;
protected readonly APIUser User;
protected DrawableAvatar Avatar { get; private set; } = null!;
protected UserAvatarNotification(APIUser? user, LocalisableString text = default)
protected UserAvatarNotification(APIUser user, LocalisableString text = default)
{
this.user = user;
User = user;
Icon = default;
Text = text;
}
@@ -31,7 +30,7 @@ namespace osu.Game.Overlays.Notifications
IconContent.CornerRadius = CORNER_RADIUS;
IconContent.ChangeChildDepth(IconDrawable, float.MinValue);
LoadComponentAsync(Avatar = new DrawableAvatar(user)
LoadComponentAsync(Avatar = new DrawableAvatar(User)
{
FillMode = FillMode.Fill,
}, IconContent.Add);
@@ -8,7 +8,7 @@ namespace osu.Game.Overlays.OSD
public partial class CopiedToClipboardToast : Toast
{
public CopiedToClipboardToast()
: base(CommonStrings.General, ToastStrings.CopiedToClipboard, "")
: base(CommonStrings.General, ToastStrings.CopiedToClipboard)
{
}
}
+9 -2
View File
@@ -1,6 +1,7 @@
// 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.Game.Input;
using osu.Game.Input.Bindings;
using osu.Game.Localisation;
@@ -9,9 +10,15 @@ namespace osu.Game.Overlays.OSD
{
public partial class SpeedChangeToast : Toast
{
public SpeedChangeToast(RealmKeyBindingStore keyBindingStore, double newSpeed)
: base(ModSelectOverlayStrings.ModCustomisation, ToastStrings.SpeedChangedTo(newSpeed), keyBindingStore.GetBindingsStringFor(GlobalAction.IncreaseModSpeed) + " / " + keyBindingStore.GetBindingsStringFor(GlobalAction.DecreaseModSpeed))
public SpeedChangeToast(double newSpeed)
: base(ModSelectOverlayStrings.ModCustomisation, ToastStrings.SpeedChangedTo(newSpeed))
{
}
[BackgroundDependencyLoader]
private void load(RealmKeyBindingStore keyBindingStore)
{
ExtraText = keyBindingStore.GetBindingsStringFor(GlobalAction.IncreaseModSpeed) + " / " + keyBindingStore.GetBindingsStringFor(GlobalAction.DecreaseModSpeed);
}
}
}
+15 -9
View File
@@ -10,23 +10,30 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osuTK;
using osuTK.Graphics;
using osu.Game.Localisation;
namespace osu.Game.Overlays.OSD
{
public abstract partial class Toast : Container
{
/// <summary>
/// Extra text to be shown at the bottom of the toast. Usually a key binding if available.
/// </summary>
public LocalisableString ExtraText
{
get => extraText.Text;
set => extraText.Text = value.ToUpper();
}
private const int toast_minimum_width = 240;
private readonly Container content;
protected override Container<Drawable> Content => content;
protected readonly OsuSpriteText ValueText;
protected readonly OsuSpriteText ValueSpriteText;
private readonly OsuSpriteText extraText;
protected readonly OsuSpriteText ShortcutText;
protected Toast(LocalisableString description, LocalisableString value, LocalisableString shortcut)
protected Toast(LocalisableString description, LocalisableString value)
{
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
@@ -65,7 +72,7 @@ namespace osu.Game.Overlays.OSD
Origin = Anchor.TopCentre,
Text = description.ToUpper()
},
ValueText = new OsuSpriteText
ValueSpriteText = new OsuSpriteText
{
Font = OsuFont.GetFont(size: 24, weight: FontWeight.Light),
Padding = new MarginPadding { Horizontal = 10 },
@@ -74,15 +81,14 @@ namespace osu.Game.Overlays.OSD
Origin = Anchor.Centre,
Text = value
},
ShortcutText = new OsuSpriteText
extraText = new OsuSpriteText
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Name = "Shortcut",
Name = "Extra Text",
Alpha = 0.3f,
Margin = new MarginPadding { Bottom = 15, Horizontal = 10 },
Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold),
Text = string.IsNullOrEmpty(shortcut.ToString()) ? ToastStrings.NoKeyBound.ToUpper() : shortcut.ToUpper()
},
};
}
+4 -2
View File
@@ -35,8 +35,10 @@ namespace osu.Game.Overlays.OSD
private Bindable<double?> lastPlaybackTime;
public TrackedSettingToast(SettingDescription description)
: base(description.Name, description.Value, description.Shortcut)
: base(description.Name, description.Value)
{
ExtraText = description.Shortcut;
FillFlowContainer<OptionLight> optionLights;
Children = new Drawable[]
@@ -75,7 +77,7 @@ namespace osu.Game.Overlays.OSD
break;
}
ValueText.Origin = optionCount > 0 ? Anchor.BottomCentre : Anchor.Centre;
ValueSpriteText.Origin = optionCount > 0 ? Anchor.BottomCentre : Anchor.Centre;
for (int i = 0; i < optionCount; i++)
optionLights.Add(new OptionLight { Glowing = i == selectedOption });
@@ -13,6 +13,7 @@ using osu.Game.Online.API.Requests.Responses;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Scoring;
using osu.Game.Users;
using osu.Game.Utils;
namespace osu.Game.Overlays.Profile.Header.Components
{
@@ -123,7 +124,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
{
var rankHighestText = UsersStrings.ShowRankHighest(
rankHighest.Rank.ToLocalisableString("\\##,##0"),
rankHighest.UpdatedAt.ToLocalisableString(@"d MMM yyyy"));
rankHighest.UpdatedAt.ToLocalisedMediumDate());
if (result == null)
result = rankHighestText;
@@ -42,6 +42,8 @@ namespace osu.Game.Overlays.Settings.Sections
Icon = OsuIcon.SkinB
};
public override IEnumerable<LocalisableString> FilterTerms => base.FilterTerms.Concat(new LocalisableString[] { "skins" });
private static readonly Live<SkinInfo> random_skin_info = new SkinInfo
{
ID = SkinInfo.RANDOM_SKIN,
@@ -69,7 +71,6 @@ namespace osu.Game.Overlays.Settings.Sections
AllowNonContiguousMatching = true,
LabelText = SkinSettingsStrings.CurrentSkin,
Current = skins.CurrentSkinInfo,
Keywords = new[] { @"skins" },
},
new FillFlowContainer
{
+2 -1
View File
@@ -769,8 +769,9 @@ namespace osu.Game.Overlays.SkinEditor
private partial class SkinEditorToast : Toast
{
public SkinEditorToast(LocalisableString value, string skinDisplayName)
: base(SkinSettingsStrings.SkinLayoutEditor, value, skinDisplayName)
: base(SkinSettingsStrings.SkinLayoutEditor, value)
{
ExtraText = skinDisplayName;
}
}
+6 -1
View File
@@ -10,6 +10,7 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Screens;
using osu.Framework.Threading;
using osu.Game.Localisation;
using osu.Game.Overlays;
using osu.Game.Overlays.Dialog;
using osu.Game.Overlays.Notifications;
@@ -165,7 +166,11 @@ namespace osu.Game
// the last dialog encountered has been dismissed but the screen has not changed, abort.
Cancel();
notifications.Post(new SimpleNotification { Text = @"An action was interrupted due to a dialog being displayed." });
notifications.Post(new SimpleNotification
{
Text = NotificationsStrings.ActionInterruptedByDialog
});
return true;
}
@@ -308,7 +308,7 @@ namespace osu.Game.Rulesets.Edit
private readonly ValueChangedEvent<double> change;
public DistanceSpacingToast(LocalisableString value, ValueChangedEvent<double> change)
: base(getAction(change).GetLocalisableDescription(), value, string.Empty)
: base(getAction(change).GetLocalisableDescription(), value)
{
this.change = change;
}
@@ -316,7 +316,7 @@ namespace osu.Game.Rulesets.Edit
[BackgroundDependencyLoader]
private void load(RealmKeyBindingStore keyBindingStore)
{
ShortcutText.Text = keyBindingStore.GetBindingsStringFor(getAction(change)).ToUpper();
ExtraText = keyBindingStore.GetBindingsStringFor(getAction(change));
}
private static GlobalAction getAction(ValueChangedEvent<double> change) => change.NewValue - change.OldValue > 0
@@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Edit
private HitObject? getPreviousHitObject() => beatmap.HitObjects.TakeWhile(h => h.StartTime <= startTimeBindable.Value).LastOrDefault();
protected override bool IsValidForPlacement => HitObject.StartTime >= beatmap.ControlPointInfo.TimingPoints[0].Time;
protected override bool IsValidForPlacement => HitObject.StartTime >= beatmap.ControlPointInfo.TimingPoints.FirstOrDefault()?.Time;
[Resolved]
private IPlacementHandler placementHandler { get; set; } = null!;
@@ -63,16 +63,24 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
/// <param name="instant">Whether the seek should be instant (drag end, mouse button press) or debounced (drag in progress).</param>
private void seekToPosition(Vector2 screenPosition, bool instant)
{
float markerPos = Math.Clamp(ToLocalSpace(screenPosition).X, 0, DrawWidth);
double seekDestination = markerPos / DrawWidth * editorClock.TrackLength;
marker.X = (float)seekDestination;
// Debounce seeks to ensure we only run one per update frame at most.
//
// Without this, we could end up seeking 1000+ times per second, leading to
// unexpected performance overheads as the editor tries to prepare for displaying
// each of the destinations.
Scheduler.AddOnce(data =>
{
float markerPos = Math.Clamp(ToLocalSpace(data.screenPosition).X, 0, DrawWidth);
double seekDestination = markerPos / DrawWidth * editorClock.TrackLength;
marker.X = (float)seekDestination;
if (editorClock.IsRunning && !instant && lastSeekTime != null && Time.Current - lastSeekTime < NowPlayingOverlay.TRACK_DRAG_SEEK_DEBOUNCE)
return;
if (editorClock.IsRunning && !data.instant && lastSeekTime != null && Time.Current - lastSeekTime < NowPlayingOverlay.TRACK_DRAG_SEEK_DEBOUNCE)
return;
editorClock.Seek(seekDestination);
editorClock.Seek(seekDestination);
lastSeekTime = instant ? null : Time.Current;
lastSeekTime = data.instant ? null : Time.Current;
}, (screenPosition, instant));
}
protected override void Update()
+2 -1
View File
@@ -1620,8 +1620,9 @@ namespace osu.Game.Screens.Edit
private partial class BeatmapEditorToast : Toast
{
public BeatmapEditorToast(LocalisableString value, string beatmapDisplayName)
: base(InputSettingsStrings.EditorSection, value, beatmapDisplayName)
: base(InputSettingsStrings.EditorSection, value)
{
ExtraText = beatmapDisplayName;
}
}
+21 -2
View File
@@ -48,7 +48,10 @@ namespace osu.Game.Screens.Edit
private Task? fileMountOperation;
public ExternalEditOperation<BeatmapSetInfo>? EditOperation;
public ExternalEditOperation<BeatmapSetInfo>? EditOperation { get; private set; }
private bool operationFinishStarted;
private bool operationFinished;
private FillFlowContainer flow = null!;
@@ -98,9 +101,13 @@ namespace osu.Game.Screens.Edit
if (fileMountOperation?.IsCompleted == false)
return true;
// Similarly do not allow interrupting an ongoing finish.
if (operationFinishStarted && !operationFinished)
return true;
// If the operation completed successfully, ensure that we finish the operation before exiting.
// The finish() call will subsequently call Exit() when done.
if (EditOperation != null)
if (EditOperation != null && !operationFinishStarted)
{
finish().FireAndForget();
return true;
@@ -185,6 +192,12 @@ namespace osu.Game.Screens.Edit
private async Task finish()
{
if (operationFinishStarted)
return;
operationFinishStarted = true;
BackButtonVisibility.Value = false;
string originalDifficulty = editor.Beatmap.Value.Beatmap.BeatmapInfo.DifficultyName;
showSpinner("Cleaning up...");
@@ -206,7 +219,11 @@ namespace osu.Game.Screens.Edit
EditOperation = null;
if (beatmap == null)
{
// has to be set before `Exit()` call to ensure the exit isn't blocked in `OnExiting()`
operationFinished = true;
this.Exit();
}
else
{
// the `ImportAsUpdate()` flow will yield beatmap(sets) with online status of `None` if online lookup fails.
@@ -223,6 +240,8 @@ namespace osu.Game.Screens.Edit
beatmap.Value.Beatmaps.FirstOrDefault(b => b.DifficultyName == originalDifficulty)
?? beatmap.Value.Beatmaps.First();
// has to be set before `SwitchToDifficulty()` call to ensure the exit isn't blocked in `OnExiting()`
operationFinished = true;
editor.SwitchToDifficulty(closestMatchingBeatmap);
}
}
@@ -11,21 +11,24 @@ namespace osu.Game.Screens.Edit.Timing
{
internal partial class EffectSection : Section<EffectControlPoint>
{
private LabelledSwitchButton kiai = null!;
private FormCheckBox kiai = null!;
private SliderWithTextBoxInput<double> scrollSpeedSlider = null!;
private FormSliderBar<double> scrollSpeedSlider { get; set; } = null!;
[BackgroundDependencyLoader]
private void load()
{
Flow.AddRange(new Drawable[]
{
kiai = new LabelledSwitchButton { Label = "Kiai Time" },
scrollSpeedSlider = new SliderWithTextBoxInput<double>("Scroll Speed")
kiai = new FormCheckBox { Caption = "Kiai Time" },
scrollSpeedSlider = new FormSliderBar<double>
{
Caption = "Scroll Speed",
Current = new EffectControlPoint().ScrollSpeedBindable,
KeyboardStep = 0.1f
}
KeyboardStep = 0.1f,
TransferValueOnCommit = true,
TabbableContentContainer = this
},
});
}
@@ -17,7 +17,7 @@ using Vector2 = osuTK.Vector2;
namespace osu.Game.Screens.Edit.Timing
{
/// <summary>
/// Analogous to <see cref="SliderWithTextBoxInput{T}"/>, but supports scenarios
/// Analogous to SliderWithTextBoxInput, but supports scenarios
/// where multiple objects with multiple different property values are selected
/// by providing an "indeterminate state".
/// </summary>
+6 -2
View File
@@ -291,7 +291,9 @@ namespace osu.Game.Screens.Footer
return;
Debug.Assert(activeOverlayContent != null);
activeOverlayContent.Hide();
activeOverlayContent.Expire();
double timeUntilRun = activeOverlayContent.LatestTransformEndTime - Time.Current;
@@ -299,6 +301,7 @@ namespace osu.Game.Screens.Footer
{
var button = temporarilyHiddenButtons[i];
hiddenButtonsContainer.Remove(button, false);
// temporarily bypass autosize on the X axis to prevent the buttons taking space
// immediately upon being moved back to the flow.
// this prevents the overlay content jumping to the right during its fade-out.
@@ -312,12 +315,13 @@ namespace osu.Game.Screens.Footer
updateColourScheme(OverlayColourScheme.Aquamarine.GetHue());
activeOverlayContent.Delay(timeUntilRun).Schedule(() =>
Scheduler.AddDelayed(() =>
{
// overlay content is done displaying, re-enable autosize on all active buttons
foreach (var button in buttonsFlow)
button.BypassAutoSizeAxes = Axes.None;
}).Expire();
}, timeUntilRun);
activeOverlayContent = null;
ActiveOverlay = null;
}
@@ -0,0 +1,190 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Localisation;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Footer;
using osu.Game.Screens.Play.HUD;
using osuTK;
namespace osu.Game.Screens.OnlinePlay
{
public partial class FooterButtonFreeModsV2 : ScreenFooterButton
{
private const float bar_height = 30f;
public readonly Bindable<IReadOnlyList<Mod>> FreeMods = new Bindable<IReadOnlyList<Mod>>([]);
public readonly Bindable<bool> Freestyle = new Bindable<bool>();
public new Action Action
{
set => throw new NotSupportedException("The click action is handled by the button itself.");
}
[Resolved]
private OsuColour colours { get; set; } = null!;
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
private Drawable modsWedge = null!;
private ModDisplay modDisplay = null!;
private Container modContainer = null!;
private ModCountText overflowModCountDisplay = null!;
public FooterButtonFreeModsV2(ModSelectOverlay overlay)
: base(overlay)
{
}
[BackgroundDependencyLoader]
private void load()
{
Text = "Freemods";
Icon = FontAwesome.Solid.ExchangeAlt;
AccentColour = colours.Lime1;
Add(modsWedge = new Container
{
Y = -5f,
Depth = float.MaxValue,
Origin = Anchor.BottomLeft,
Shear = OsuGame.SHEAR,
CornerRadius = CORNER_RADIUS,
Size = new Vector2(BUTTON_WIDTH, bar_height),
Masking = true,
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Shadow,
Radius = 4,
// Figma says 50% opacity, but it does not match up visually if taken at face value, and looks bad.
Colour = Colour4.Black.Opacity(0.25f),
Offset = new Vector2(0, 2),
},
Alpha = 0,
Children = new Drawable[]
{
new Box
{
Colour = colourProvider.Background4,
RelativeSizeAxes = Axes.Both,
},
modContainer = new Container
{
CornerRadius = CORNER_RADIUS,
RelativeSizeAxes = Axes.Both,
Masking = true,
Children = new Drawable[]
{
new Box
{
Colour = colourProvider.Background3,
RelativeSizeAxes = Axes.Both,
},
modDisplay = new ModDisplay(showExtendedInformation: true)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Shear = -OsuGame.SHEAR,
Scale = new Vector2(0.5f),
Current = { BindTarget = FreeMods },
ExpansionMode = ExpansionMode.AlwaysContracted,
},
overflowModCountDisplay = new ModCountText
{
Mods = { BindTarget = FreeMods },
Freestyle = { BindTarget = Freestyle }
},
}
},
}
});
}
protected override void LoadComplete()
{
base.LoadComplete();
Freestyle.BindValueChanged(f => Enabled.Value = !f.NewValue, true);
FreeMods.BindValueChanged(m =>
{
if (m.NewValue.Count == 0)
modsWedge.FadeOut(200);
else
modsWedge.FadeIn(200);
}, true);
}
protected override void Update()
{
base.Update();
if (modDisplay.DrawWidth * modDisplay.Scale.X > modContainer.DrawWidth)
overflowModCountDisplay.Show();
else
overflowModCountDisplay.Hide();
}
private partial class ModCountText : CompositeDrawable
{
public readonly Bindable<IReadOnlyList<Mod>> Mods = new Bindable<IReadOnlyList<Mod>>();
public readonly Bindable<bool> Freestyle = new Bindable<bool>();
private OsuSpriteText text = null!;
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
protected override void LoadComplete()
{
base.LoadComplete();
RelativeSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
new Box
{
Colour = colourProvider.Background3,
Alpha = 0.8f,
RelativeSizeAxes = Axes.Both,
},
text = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = OsuFont.Torus.With(size: 14f, weight: FontWeight.Bold),
Shear = -OsuGame.SHEAR,
}
};
Mods.BindValueChanged(_ => updateText());
Freestyle.BindValueChanged(_ => updateText());
updateText();
}
private void updateText()
{
if (Freestyle.Value)
text.Text = "ALL MODS";
else
text.Text = ModSelectOverlayStrings.Mods(Mods.Value.Count).ToUpper();
}
}
}
}
@@ -0,0 +1,50 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Screens.Footer;
namespace osu.Game.Screens.OnlinePlay
{
public partial class FooterButtonFreestyleV2 : ScreenFooterButton
{
public readonly Bindable<bool> Freestyle = new Bindable<bool>();
public new Action Action
{
set => throw new NotSupportedException("The click action is handled by the button itself.");
}
[Resolved]
private OsuColour colours { get; set; } = null!;
public FooterButtonFreestyleV2()
{
// Overwrite any external behaviour as we delegate the main toggle action to a sub-button.
base.Action = () => Freestyle.Value = !Freestyle.Value;
}
[BackgroundDependencyLoader]
private void load()
{
Text = "Freestyle";
Icon = FontAwesome.Solid.ExchangeAlt;
AccentColour = colours.Lime1;
}
protected override void LoadComplete()
{
base.LoadComplete();
Freestyle.BindValueChanged(active =>
{
OverlayState.Value = active.NewValue ? Visibility.Visible : Visibility.Hidden;
}, true);
}
}
}
@@ -94,6 +94,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Action = i => ItemSelected?.Invoke(i),
Depth = -(float)item.PlaylistItem.StarRating
};
panelGridContainer.Add(panel);
@@ -2,6 +2,7 @@
// 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.Bindables;
using osu.Framework.Extensions.Color4Extensions;
@@ -10,6 +11,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
@@ -56,6 +58,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
private FillFlowContainer idleBottomContent = null!;
private BeatmapCardDownloadProgressBar downloadProgressBar = null!;
private AvatarOverlay selectionOverlay = null!;
private OsuTextFlowContainer beatmapAttributesText = null!;
public CardContentBeatmap(APIBeatmap beatmap, Mod[] mods)
{
@@ -70,135 +73,66 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
private void load(OsuColour colours)
{
FillFlowContainer leftIconArea;
FillFlowContainer titleBadgeArea;
GridContainer artistContainer;
Container explicitBadgeArea;
InternalChildren = new Drawable[]
InternalChildren = new[]
{
new BeatmapDownloadTracker(beatmap.BeatmapSet!)
new Container
{
State = { BindTarget = downloadState },
Progress = { BindTarget = downloadProgress },
},
thumbnail = new BeatmapCardThumbnail(beatmapSet, beatmapSet, keepLoaded: true)
{
Name = @"Left (icon) area",
Size = new Vector2(MatchmakingSelectPanel.HEIGHT),
Padding = new MarginPadding { Right = BeatmapCard.CORNER_RADIUS },
Child = leftIconArea = new FillFlowContainer
{
Margin = new MarginPadding(4),
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(1)
}
},
buttonContainer = new CollapsibleButtonContainer(beatmapSet, allowNavigationToBeatmap: false, keepBackgroundLoaded: true)
{
X = MatchmakingSelectPanel.HEIGHT - BeatmapCard.CORNER_RADIUS,
Width = BeatmapCard.WIDTH - MatchmakingSelectPanel.HEIGHT + BeatmapCard.CORNER_RADIUS,
FavouriteState = { BindTarget = favouriteState },
ButtonsCollapsedWidth = 0,
ButtonsExpandedWidth = 24,
RelativeSizeAxes = Axes.Both,
Masking = true,
CornerRadius = BeatmapCard.CORNER_RADIUS,
Children = new Drawable[]
{
new FillFlowContainer
new BeatmapDownloadTracker(beatmap.BeatmapSet!)
{
RelativeSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
State = { BindTarget = downloadState },
Progress = { BindTarget = downloadProgress },
},
thumbnail = new BeatmapCardThumbnail(beatmapSet, beatmapSet, keepLoaded: true)
{
Name = @"Left (icon) area",
Size = new Vector2(MatchmakingSelectPanel.HEIGHT),
Padding = new MarginPadding { Right = BeatmapCard.CORNER_RADIUS },
Children = new Drawable[]
{
new GridContainer
leftIconArea = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
ColumnDimensions = new[]
{
new Dimension(),
new Dimension(GridSizeMode.AutoSize),
},
RowDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize)
},
Content = new[]
{
new Drawable[]
{
new TruncatingSpriteText
{
Text = new RomanisableString(beatmapSet.TitleUnicode, beatmapSet.Title),
Font = OsuFont.Default.With(size: 18f, weight: FontWeight.SemiBold),
RelativeSizeAxes = Axes.X,
},
titleBadgeArea = new FillFlowContainer
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
}
}
}
AutoSizeAxes = Axes.Both,
Margin = new MarginPadding(4),
Direction = FillDirection.Horizontal,
Spacing = new Vector2(1)
},
artistContainer = new GridContainer
explicitBadgeArea = new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
ColumnDimensions = new[]
{
new Dimension(),
new Dimension(GridSizeMode.AutoSize)
},
RowDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize)
},
Content = new[]
{
new[]
{
new TruncatingSpriteText
{
Text = BeatmapsetsStrings.ShowDetailsByArtist(new RomanisableString(beatmapSet.ArtistUnicode, beatmapSet.Artist)),
Font = OsuFont.Default.With(size: 14f, weight: FontWeight.SemiBold),
RelativeSizeAxes = Axes.X,
},
Empty()
},
}
},
new LinkFlowContainer(s =>
{
s.Shadow = false;
s.Font = OsuFont.GetFont(size: 11f, weight: FontWeight.SemiBold);
}).With(d =>
{
d.AutoSizeAxes = Axes.Both;
d.Margin = new MarginPadding { Top = 1 };
d.AddText("mapped by ", t => t.Colour = colourProvider.Content2);
d.AddUserLink(beatmapSet.Author);
}),
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
AutoSizeAxes = Axes.Both,
Margin = new MarginPadding(4),
}
}
},
new Container
buttonContainer = new CollapsibleButtonContainer(beatmapSet, allowNavigationToBeatmap: false, keepBackgroundLoaded: true)
{
Name = @"Bottom content",
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
X = MatchmakingSelectPanel.HEIGHT - BeatmapCard.CORNER_RADIUS,
Width = BeatmapCard.WIDTH - MatchmakingSelectPanel.HEIGHT + BeatmapCard.CORNER_RADIUS,
FavouriteState = { BindTarget = favouriteState },
ButtonsCollapsedWidth = 0,
ButtonsExpandedWidth = 24,
Children = new Drawable[]
{
idleBottomContent = new FillFlowContainer
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 2),
AlwaysPresent = true,
Children = new Drawable[]
{
new TruncatingSpriteText
{
Text = new RomanisableString(beatmapSet.TitleUnicode, beatmapSet.Title),
Font = OsuFont.Default.With(size: 18f, weight: FontWeight.SemiBold),
RelativeSizeAxes = Axes.X,
},
new GridContainer
{
RelativeSizeAxes = Axes.X,
@@ -216,75 +150,160 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
{
new Drawable[]
{
new Container
new TruncatingSpriteText
{
Masking = true,
CornerRadius = BeatmapCard.CORNER_RADIUS,
Text = BeatmapsetsStrings.ShowDetailsByArtist(new RomanisableString(beatmapSet.ArtistUnicode, beatmapSet.Artist)),
Font = OsuFont.Default.With(size: 14f, weight: FontWeight.SemiBold),
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Children = new Drawable[]
{
new Box
{
Colour = colours.ForStarDifficulty(beatmap.StarRating).Darken(0.8f),
RelativeSizeAxes = Axes.Both,
},
new FillFlowContainer
{
Padding = new MarginPadding(4),
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(6, 0),
Children = new Drawable[]
{
new StarRatingDisplay(new StarDifficulty(beatmap.StarRating, 0), StarRatingDisplaySize.Small, animated: true)
{
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
Scale = new Vector2(0.9f),
},
new TruncatingSpriteText
{
Text = beatmap.DifficultyName,
Font = OsuFont.Style.Caption1.With(weight: FontWeight.Bold),
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
}
},
}
},
new ModFlowDisplay
new TopTagPill(beatmap)
{
AutoSizeAxes = Axes.Both,
Scale = new Vector2(0.5f),
Margin = new MarginPadding { Left = 5 },
Current = { Value = mods }
},
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
}
},
}
},
new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Children = new Drawable[]
{
new LinkFlowContainer(s =>
{
s.Shadow = false;
s.Font = OsuFont.Style.Caption2.With(weight: FontWeight.SemiBold);
}).With(d =>
{
d.AutoSizeAxes = Axes.Both;
d.Margin = new MarginPadding { Top = 1 };
d.AddText("mapped by ", t => t.Colour = colourProvider.Content2);
d.AddUserLink(beatmapSet.Author);
}),
beatmapAttributesText = new OsuTextFlowContainer
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
AutoSizeAxes = Axes.Both,
}
}
}
}
},
downloadProgressBar = new BeatmapCardDownloadProgressBar
new Container
{
Name = @"Bottom content",
RelativeSizeAxes = Axes.X,
Height = 5,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
State = { BindTarget = downloadState },
Progress = { BindTarget = downloadProgress }
AutoSizeAxes = Axes.Y,
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Children = new Drawable[]
{
idleBottomContent = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 2),
AlwaysPresent = true,
Children = new Drawable[]
{
new GridContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
ColumnDimensions = new[]
{
new Dimension(),
new Dimension(GridSizeMode.AutoSize)
},
RowDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize)
},
Content = new[]
{
new Drawable[]
{
new Container
{
Masking = true,
CornerRadius = BeatmapCard.CORNER_RADIUS,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Children = new Drawable[]
{
new Box
{
Colour = colours.ForStarDifficulty(beatmap.StarRating).Darken(0.8f),
RelativeSizeAxes = Axes.Both,
},
new FillFlowContainer
{
Padding = new MarginPadding(4),
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(6, 0),
Children = new Drawable[]
{
new StarRatingDisplay(new StarDifficulty(beatmap.StarRating, 0), StarRatingDisplaySize.Small, animated: true)
{
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
Scale = new Vector2(0.9f),
},
new TruncatingSpriteText
{
Text = beatmap.DifficultyName,
Font = OsuFont.Style.Caption1.With(weight: FontWeight.Bold),
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
}
},
}
},
new Container
{
AutoSizeAxes = Axes.Both,
Alpha = mods.Length > 0 ? 1 : 0,
Child = new ModFlowDisplay
{
AutoSizeAxes = Axes.Both,
Scale = new Vector2(0.5f),
Margin = new MarginPadding { Left = 5 },
Current = { Value = mods },
}
}
},
}
},
}
},
downloadProgressBar = new BeatmapCardDownloadProgressBar
{
RelativeSizeAxes = Axes.X,
Height = 5,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
State = { BindTarget = downloadState },
Progress = { BindTarget = downloadProgress }
}
}
},
selectionOverlay = new AvatarOverlay
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Margin = new MarginPadding { Top = -20 }
}
}
},
selectionOverlay = new AvatarOverlay
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
}
}
}
},
selectionOverlay.CreateProxy()
};
if (beatmapSet.HasVideo)
@@ -293,34 +312,68 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
if (beatmapSet.HasStoryboard)
leftIconArea.Add(new StoryboardIconPill { IconSize = new Vector2(16) });
if (beatmapSet.FeaturedInSpotlight)
{
titleBadgeArea.Add(new SpotlightBeatmapBadge
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
Margin = new MarginPadding { Left = 4 }
});
}
if (beatmapSet.HasExplicitContent)
{
titleBadgeArea.Add(new ExplicitContentBeatmapBadge
explicitBadgeArea.Add(new ExplicitContentBeatmapBadge
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
Margin = new MarginPadding { Left = 4 }
});
}
if (beatmapSet.TrackId != null)
bool firstAttribute = true;
foreach (var attribute in getBeatmapAttributes())
{
artistContainer.Content[0][1] = new FeaturedArtistBeatmapBadge
if (!firstAttribute)
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
Margin = new MarginPadding { Left = 4 }
};
beatmapAttributesText.AddText(@" / ", s =>
{
font(s, false);
s.Spacing = new Vector2(-2, 0);
});
}
beatmapAttributesText.AddText(attribute.heading, s => font(s, false));
beatmapAttributesText.AddText(@" ", s => font(s, false));
beatmapAttributesText.AddText(attribute.content, s => font(s, true));
firstAttribute = false;
static void font(SpriteText s, bool bold)
=> s.Font = OsuFont.Style.Caption2.With(weight: bold ? FontWeight.Bold : FontWeight.Regular);
}
}
private (string heading, string content)[] getBeatmapAttributes()
{
BeatmapDifficulty adjustedDifficulty = new BeatmapDifficulty(beatmap.Difficulty);
foreach (var mod in mods.OfType<IApplicableToDifficulty>())
mod.ApplyToDifficulty(adjustedDifficulty);
switch (beatmap.Ruleset.OnlineID)
{
default:
return new (string heading, string content)[]
{
("CS", $"{adjustedDifficulty.CircleSize:0.#}"),
("AR", $"{adjustedDifficulty.ApproachRate:0.#}"),
("OD", $"{adjustedDifficulty.OverallDifficulty:0.#}"),
};
case 1:
case 3:
return new (string heading, string content)[]
{
("OD", $"{adjustedDifficulty.OverallDifficulty:0.#}"),
("HP", $"{adjustedDifficulty.DrainRate:0.#}")
};
case 2:
return new (string heading, string content)[]
{
("CS", $"{adjustedDifficulty.CircleSize:0.#}"),
("AR", $"{adjustedDifficulty.ApproachRate:0.#}"),
};
}
}
@@ -376,6 +429,47 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
return items.ToArray();
}
}
private partial class TopTagPill : CompositeDrawable, IHasTooltip
{
private readonly APIBeatmap beatmap;
public TopTagPill(APIBeatmap beatmap)
{
this.beatmap = beatmap;
AutoSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
InternalChild = new CircularContainer
{
AutoSizeAxes = Axes.Both,
Masking = true,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background1
},
new OsuSpriteText
{
Padding = new MarginPadding { Vertical = 3, Horizontal = 8 },
Text = beatmap.GetTopUserTags().FirstOrDefault().Tag?.Name ?? string.Empty,
AlwaysPresent = true,
Colour = colourProvider.Content2,
Font = OsuFont.Style.Caption2,
UseFullGlyphHeight = false,
}
}
};
}
public LocalisableString TooltipText => string.Join('\n', beatmap.GetTopUserTags().Select(t => $"{t.Tag.Name} ({t.VoteCount})"));
}
}
}
}
@@ -3,9 +3,11 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Utils;
using osu.Game.Beatmaps.Drawables.Cards;
using osu.Game.Graphics.Backgrounds;
using osu.Game.Graphics.Sprites;
using osu.Game.Overlays;
@@ -29,37 +31,44 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
[BackgroundDependencyLoader]
private void load()
{
InternalChildren = new Drawable[]
InternalChild = new Container
{
new Box
RelativeSizeAxes = Axes.Both,
Masking = true,
CornerRadius = BeatmapCard.CORNER_RADIUS,
Children = new Drawable[]
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Dark5,
},
new TrianglesV2
{
RelativeSizeAxes = Axes.Both,
Alpha = 0.1f,
},
Label = new OsuSpriteText
{
Y = 20,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = "Random"
},
Dice = new SpriteIcon
{
Y = -10,
Size = new Vector2(28),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Icon = randomDiceIcon(),
},
selectionOverlay = new AvatarOverlay
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Dark5,
},
new TrianglesV2
{
RelativeSizeAxes = Axes.Both,
Alpha = 0.1f,
},
Label = new OsuSpriteText
{
Y = 20,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = "Random"
},
Dice = new SpriteIcon
{
Y = -10,
Size = new Vector2(28),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Icon = randomDiceIcon(),
},
selectionOverlay = new AvatarOverlay
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Margin = new MarginPadding { Right = 5 }
}
}
};
@@ -61,21 +61,18 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
{
new Container
{
RelativeSizeAxes = Axes.Both,
Masking = true,
CornerRadius = BeatmapCard.CORNER_RADIUS,
CornerExponent = 10,
RelativeSizeAxes = Axes.Both,
Children = new[]
Child = lighting = new Box
{
Content,
lighting = new Box
{
Blending = BlendingParameters.Additive,
RelativeSizeAxes = Axes.Both,
Alpha = 0,
},
Blending = BlendingParameters.Additive,
RelativeSizeAxes = Axes.Both,
Alpha = 0,
}
},
Content,
border = new Container
{
Alpha = 0,
@@ -6,9 +6,11 @@ using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Transforms;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps.Drawables.Cards;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Rooms;
using osuTK;
@@ -69,7 +71,13 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
AddRange(new Drawable[]
{
new CardContentBeatmap(playlistItem.Beatmap, playlistItem.Mods),
flashLayer,
new Container
{
RelativeSizeAxes = Axes.Both,
Masking = true,
CornerRadius = BeatmapCard.CORNER_RADIUS,
Child = flashLayer
}
});
foreach (var user in users)
@@ -44,7 +44,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Horizontal = 200 },
Padding = new MarginPadding { Horizontal = 250 },
Children = new Drawable[]
{
beatmapSelectGrid = new BeatmapSelectGrid
@@ -51,7 +51,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
AddInternal(new Container
{
Padding = new MarginPadding(2),
Padding = new MarginPadding(isOwnUser ? 2 : 0),
RelativeSizeAxes = Axes.Both,
Child = new CircularContainer
{
@@ -47,7 +47,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
/// </summary>
public partial class PlayerPanel : OsuClickableContainer, IHasContextMenu
{
private static readonly Vector2 size_horizontal = new Vector2(250, 100);
private static readonly Vector2 size_horizontal = new Vector2(300, 100);
private static readonly Vector2 size_vertical = new Vector2(150, 200);
private static readonly Vector2 avatar_size = new Vector2(80);
@@ -236,13 +236,14 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
Text = "-",
Font = OsuFont.Style.Title.With(size: 55),
},
username = new OsuSpriteText
username = new TruncatingSpriteText
{
Alpha = 0,
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Text = User.Username,
Font = OsuFont.Style.Heading1,
MaxWidth = 120
},
scoreText = new OsuSpriteText
{

Some files were not shown because too many files have changed in this diff Show More