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:
+1
-1
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+22
-32
@@ -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 =
|
||||
{
|
||||
|
||||
@@ -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 } },
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 =>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
+276
-182
@@ -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})"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+38
-29
@@ -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 }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+6
-9
@@ -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,
|
||||
|
||||
+9
-1
@@ -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)
|
||||
|
||||
+1
-1
@@ -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
Reference in New Issue
Block a user