mirror of
https://github.com/ppy/osu.git
synced 2026-06-09 04:13:39 +08:00
Compare commits
165 Commits
+1
-3
@@ -55,9 +55,7 @@ When in doubt, it's probably best to start with a discussion first. We will esca
|
||||
|
||||
While pull requests from unaffiliated contributors are welcome, please note that due to significant community interest and limited review throughput, the core team's primary focus is on the issues which are currently [on the roadmap](https://github.com/orgs/ppy/projects/7/views/6). Reviewing PRs that fall outside of the scope of the roadmap is done on a best-effort basis, so please be aware that it may take a while before a core maintainer gets around to review your change.
|
||||
|
||||
The [issue tracker](https://github.com/ppy/osu/issues) should provide plenty of issues to start with. We also have a [`good first issue`](https://github.com/ppy/osu/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) label, although from experience it is not used very often, as it is relatively rare that we can spot an issue that will definitively be a good first issue for a new contributor regardless of their programming experience.
|
||||
|
||||
In the case of simple issues, a direct PR is okay. However, if you decide to work on an existing issue which doesn't seem trivial, **please ask us first**. This way we can try to estimate if it is a good fit for you and provide the correct direction on how to address it. In addition, note that while we do not rule out external contributors from working on roadmapped issues, we will generally prefer to handle them ourselves unless they're not very time sensitive.
|
||||
The [issue tracker](https://github.com/ppy/osu/issues) should provide plenty of issues to start with. In the case of simple issues, a direct PR is okay. However, if you decide to work on an existing issue which doesn't seem trivial, **please ask us first**. This way we can try to estimate if it is a good fit for you and provide the correct direction on how to address it. In addition, note that while we do not rule out external contributors from working on roadmapped issues, we will generally prefer to handle them ourselves unless they're not very time sensitive.
|
||||
|
||||
If you'd like to propose a subjective change to one of the visual aspects of the game, or there is a bigger task you'd like to work on, but there is no corresponding issue or discussion thread yet for it, **please open a discussion or issue first** to avoid wasted effort. This in particular applies if you want to work on [one of the available designs from the osu! Figma master library](https://www.figma.com/file/VIkXMYNPMtQem2RJg9k2iQ/Master-Library).
|
||||
|
||||
|
||||
+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.
|
||||
|
||||
@@ -7,6 +7,7 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
|
||||
@@ -32,7 +33,7 @@ namespace osu.Desktop.Security
|
||||
{
|
||||
public ElevatedPrivilegesNotification()
|
||||
{
|
||||
Text = $"Running osu! as {(RuntimeInfo.IsUnix ? "root" : "administrator")} does not improve performance, may break integrations and poses a security risk. Please run the game as a normal user.";
|
||||
Text = NotificationsStrings.ElevatedPrivileges(RuntimeInfo.IsUnix ? "root" : "Administrator");
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -398,6 +398,30 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -95,7 +95,7 @@ namespace osu.Game.Beatmaps
|
||||
protected virtual WorkingBeatmapCache CreateWorkingBeatmapCache(AudioManager audioManager, IResourceStore<byte[]> resources, IResourceStore<byte[]> storage, WorkingBeatmap? defaultBeatmap,
|
||||
GameHost? host)
|
||||
{
|
||||
return new WorkingBeatmapCache(BeatmapTrackStore, audioManager, resources, storage, defaultBeatmap, host);
|
||||
return new WorkingBeatmapCache(BeatmapTrackStore, audioManager, resources, storage, defaultBeatmap, host, Realm);
|
||||
}
|
||||
|
||||
protected virtual BeatmapImporter CreateBeatmapImporter(Storage storage, RealmAccess realm) => new BeatmapImporter(storage, realm);
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,12 +47,13 @@ namespace osu.Game.Beatmaps
|
||||
private readonly LargeTextureStore beatmapPanelTextureStore;
|
||||
private readonly ITrackStore trackStore;
|
||||
private readonly IResourceStore<byte[]> files;
|
||||
private readonly RealmAccess realm;
|
||||
|
||||
[CanBeNull]
|
||||
private readonly GameHost host;
|
||||
|
||||
public WorkingBeatmapCache(ITrackStore trackStore, AudioManager audioManager, IResourceStore<byte[]> resources, IResourceStore<byte[]> files, WorkingBeatmap defaultBeatmap = null,
|
||||
GameHost host = null)
|
||||
GameHost host = null, RealmAccess realm = null)
|
||||
{
|
||||
DefaultBeatmap = defaultBeatmap;
|
||||
|
||||
@@ -63,6 +64,7 @@ namespace osu.Game.Beatmaps
|
||||
largeTextureStore = new LargeTextureStore(host?.Renderer ?? new DummyRenderer(), host?.CreateTextureLoaderStore(files));
|
||||
beatmapPanelTextureStore = new LargeTextureStore(host?.Renderer ?? new DummyRenderer(), new BeatmapPanelBackgroundTextureLoaderStore(host?.CreateTextureLoaderStore(files)));
|
||||
this.trackStore = trackStore;
|
||||
this.realm = realm;
|
||||
}
|
||||
|
||||
public void Invalidate(BeatmapSetInfo info)
|
||||
@@ -118,7 +120,7 @@ namespace osu.Game.Beatmaps
|
||||
ITrackStore IBeatmapResourceProvider.Tracks => trackStore;
|
||||
IRenderer IStorageResourceProvider.Renderer => host?.Renderer ?? new DummyRenderer();
|
||||
AudioManager IStorageResourceProvider.AudioManager => audioManager;
|
||||
RealmAccess IStorageResourceProvider.RealmAccess => null!;
|
||||
RealmAccess IStorageResourceProvider.RealmAccess => realm;
|
||||
IResourceStore<byte[]> IStorageResourceProvider.Files => files;
|
||||
IResourceStore<byte[]> IStorageResourceProvider.Resources => resources;
|
||||
IResourceStore<TextureUpload> IStorageResourceProvider.CreateTextureLoaderStore(IResourceStore<byte[]> underlyingStore) => host?.CreateTextureLoaderStore(underlyingStore);
|
||||
|
||||
@@ -13,6 +13,7 @@ using osu.Game.Beatmaps.Formats;
|
||||
using osu.Game.Beatmaps.Timing;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.IO;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
@@ -198,7 +199,7 @@ namespace osu.Game.Database
|
||||
ProgressNotification notification = new ProgressNotification
|
||||
{
|
||||
State = ProgressNotificationState.Active,
|
||||
Text = $"Exporting {itemFilename}...",
|
||||
Text = NotificationsStrings.FileExportOngoing(itemFilename),
|
||||
};
|
||||
|
||||
PostNotification?.Invoke(notification);
|
||||
@@ -225,7 +226,7 @@ namespace osu.Game.Database
|
||||
throw;
|
||||
}
|
||||
|
||||
notification.CompletionText = $"Exported {itemFilename}! Click to view.";
|
||||
notification.CompletionText = NotificationsStrings.FileExportFinished(itemFilename);
|
||||
notification.CompletionClickAction = () => ExportStorage.PresentFileExternally(filename);
|
||||
notification.State = ProgressNotificationState.Completed;
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ using System.Threading.Tasks;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.IO;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
using osu.Game.Utils;
|
||||
using Realms;
|
||||
@@ -83,7 +84,7 @@ namespace osu.Game.Database
|
||||
ProgressNotification notification = new ProgressNotification
|
||||
{
|
||||
State = ProgressNotificationState.Active,
|
||||
Text = $"Exporting {itemFilename}...",
|
||||
Text = NotificationsStrings.FileExportOngoing(itemFilename),
|
||||
};
|
||||
|
||||
PostNotification?.Invoke(notification);
|
||||
@@ -106,7 +107,7 @@ namespace osu.Game.Database
|
||||
throw;
|
||||
}
|
||||
|
||||
notification.CompletionText = $"Exported {itemFilename}! Click to view.";
|
||||
notification.CompletionText = NotificationsStrings.FileExportFinished(itemFilename);
|
||||
notification.CompletionClickAction = () => ExportStorage.PresentFileExternally(filename);
|
||||
notification.State = ProgressNotificationState.Completed;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -17,6 +17,7 @@ using osu.Framework.Platform;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
@@ -169,7 +170,7 @@ namespace osu.Game.Graphics
|
||||
|
||||
notificationOverlay.Post(new SimpleNotification
|
||||
{
|
||||
Text = $"Screenshot {filename} saved!",
|
||||
Text = NotificationsStrings.ScreenshotSaved(filename),
|
||||
Activated = () =>
|
||||
{
|
||||
storage.PresentFileExternally(filename);
|
||||
|
||||
@@ -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,12 +92,11 @@ namespace osu.Game.Graphics.UserInterface
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Colour = Enabled.Value ? Colour4.White : DimColour;
|
||||
Enabled.BindValueChanged(_ => this.FadeColour(DimColour, 200, Easing.OutQuint), true);
|
||||
Enabled.BindValueChanged(_ => content.FadeColour(DimColour, 200, Easing.OutQuint), true);
|
||||
FinishTransforms(true);
|
||||
}
|
||||
|
||||
protected virtual Colour4 DimColour => 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
|
||||
@@ -38,6 +40,8 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
||||
|
||||
private FormDropdownHeader header = null!;
|
||||
|
||||
private const float header_menu_spacing = 5;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
@@ -45,6 +49,10 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
||||
|
||||
header.Caption = Caption;
|
||||
header.HintText = HintText;
|
||||
|
||||
// there's bottom margin applied inside the header to give spacing between the header and the menu.
|
||||
// however when the menu is closed the extra spacing remains present. to remove it, apply negative bottom padding here.
|
||||
Margin = new MarginPadding { Bottom = -header_menu_spacing };
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
@@ -140,30 +148,34 @@ 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;
|
||||
Margin = new MarginPadding { Bottom = header_menu_spacing };
|
||||
|
||||
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 +224,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 +231,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 +265,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 +276,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 };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,11 +286,26 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
||||
private void load(OverlayColourProvider colourProvider)
|
||||
{
|
||||
ItemsContainer.Padding = new MarginPadding(9);
|
||||
Margin = new MarginPadding { Top = 5 };
|
||||
|
||||
MaskingContainer.BorderThickness = 2;
|
||||
MaskingContainer.BorderColour = colourProvider.Highlight1;
|
||||
}
|
||||
|
||||
protected override void AnimateOpen()
|
||||
{
|
||||
base.AnimateOpen();
|
||||
|
||||
// there's negative bottom margin applied on the whole dropdown control to remove extra spacing when the menu is closed.
|
||||
// however, when the menu is open, we want spacing between the menu and the next control below it. therefore apply bottom margin here.
|
||||
// we use a transform to keep the open animation smooth while margin is adjusted.
|
||||
this.TransformTo(nameof(Margin), new MarginPadding { Bottom = header_menu_spacing }, 300, Easing.OutQuint);
|
||||
}
|
||||
|
||||
protected override void AnimateClose()
|
||||
{
|
||||
base.AnimateClose();
|
||||
this.TransformTo(nameof(Margin), new MarginPadding { Bottom = 0 }, 300, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -305,7 +330,11 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
||||
break;
|
||||
|
||||
case Bindable<double> bindableDouble:
|
||||
bindableDouble.Value = double.Parse(textBox.Current.Value);
|
||||
bindableDouble.Value = double.Parse(textBox.Current.Value) / (DisplayAsPercentage ? 100 : 1);
|
||||
break;
|
||||
|
||||
case Bindable<float> bindableFloat:
|
||||
bindableFloat.Value = float.Parse(textBox.Current.Value) / (DisplayAsPercentage ? 100 : 1);
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -372,7 +401,18 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
||||
{
|
||||
if (updatingFromTextBox) return;
|
||||
|
||||
textBox.Text = currentNumberInstantaneous.Value.ToStandardFormattedString(OsuSliderBar<T>.MAX_DECIMAL_DIGITS);
|
||||
if (DisplayAsPercentage)
|
||||
{
|
||||
double floatValue = double.CreateTruncating(currentNumberInstantaneous.Value);
|
||||
|
||||
if (currentNumberInstantaneous.Value is int)
|
||||
floatValue /= 100;
|
||||
|
||||
textBox.Text = floatValue.ToStandardFormattedString(Math.Max(0, OsuSliderBar<T>.MAX_DECIMAL_DIGITS - 2));
|
||||
}
|
||||
else
|
||||
textBox.Text = currentNumberInstantaneous.Value.ToStandardFormattedString(OsuSliderBar<T>.MAX_DECIMAL_DIGITS);
|
||||
|
||||
valueLabel.Text = LabelFormat(currentNumberInstantaneous.Value);
|
||||
}
|
||||
|
||||
@@ -382,9 +422,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 +477,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,131 @@ 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.");
|
||||
|
||||
/// <summary>
|
||||
/// "Exporting {0}..."
|
||||
/// </summary>
|
||||
public static LocalisableString FileExportOngoing(string filename) => new TranslatableString(getKey(@"file_export_ongoing"), @"Exporting {0}...", filename);
|
||||
|
||||
/// <summary>
|
||||
/// "Exported {0}! Click to view."
|
||||
/// </summary>
|
||||
public static LocalisableString FileExportFinished(string filename) => new TranslatableString(getKey(@"file_export_finished"), @"Exported {0}! Click to view.", filename);
|
||||
|
||||
/// <summary>
|
||||
/// "Exporting logs..."
|
||||
/// </summary>
|
||||
public static LocalisableString LogsExportOngoing => new TranslatableString(getKey(@"logs_export_ongoing"), @"Exporting logs...");
|
||||
|
||||
/// <summary>
|
||||
/// "Exported logs! Click to view."
|
||||
/// </summary>
|
||||
public static LocalisableString LogsExportFinished => new TranslatableString(getKey(@"logs_export_finished"), @"Exported logs! Click to view.");
|
||||
|
||||
/// <summary>
|
||||
/// "Running osu! as {0} does not improve performance, may break integrations and poses a security risk. Please run the game as a normal user."
|
||||
/// </summary>
|
||||
public static LocalisableString ElevatedPrivileges(LocalisableString user) => new TranslatableString(getKey(@"elevated_privileges"), @"Running osu! as {0} does not improve performance, may break integrations and poses a security risk. Please run the game as a normal user.", user);
|
||||
|
||||
/// <summary>
|
||||
/// "Screenshot {0} saved!"
|
||||
/// </summary>
|
||||
public static LocalisableString ScreenshotSaved(string filename) => new TranslatableString(getKey(@"screenshot_saved"), @"Screenshot {0} saved!", filename);
|
||||
|
||||
/// <summary>
|
||||
/// "The multiplayer server will be right back..."
|
||||
/// </summary>
|
||||
public static LocalisableString MultiplayerServerShuttingDownImmediately => new TranslatableString(getKey(@"multiplayer_server_shutting_down_immediately"), @"The multiplayer server will be right back...");
|
||||
|
||||
/// <summary>
|
||||
/// "The multiplayer server is restarting in {0}."
|
||||
/// </summary>
|
||||
public static LocalisableString MultiplayerServerShuttingDownRemaining(string remainingTime) => new TranslatableString(getKey(@"multiplayer_server_shutting_down_remaining"), @"The multiplayer server is restarting in {0}.", remainingTime);
|
||||
|
||||
/// <summary>
|
||||
/// "Created new collection "{0}" with {1} beatmaps."
|
||||
/// </summary>
|
||||
public static LocalisableString CollectionCreated(string name, int beatmapsCount) => new TranslatableString(getKey(@"collection_created"), @"Created new collection ""{0}"" with {1} beatmaps.", name, beatmapsCount);
|
||||
|
||||
/// <summary>
|
||||
/// "Added {0} beatmaps to collection "{1}"."
|
||||
/// </summary>
|
||||
public static LocalisableString CollectionBeatmapsAdded(string name, int beatmapsCount) => new TranslatableString(getKey(@"collection_beatmaps_added"), @"Added {0} beatmaps to collection ""{1}"".", beatmapsCount, name);
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,15 +165,25 @@ 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"
|
||||
/// </summary>
|
||||
public static LocalisableString SelectedMods => new TranslatableString(getKey(@"selected_mods"), @"Selected Mods");
|
||||
|
||||
/// <summary>
|
||||
/// "hold for menu"
|
||||
/// </summary>
|
||||
public static LocalisableString HoldForMenu => new TranslatableString(getKey(@"hold_for_menu"), @"hold for menu");
|
||||
|
||||
/// <summary>
|
||||
/// "press for menu"
|
||||
/// </summary>
|
||||
public static LocalisableString PressForMenu => new TranslatableString(getKey(@"press_for_menu"), @"press for menu");
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,64 +154,52 @@ 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 (!notifyOnFriendPresenceChange.Value)
|
||||
{
|
||||
lastOnlineAlertTime = null;
|
||||
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 (onlineAlertQueue.Count == 1)
|
||||
notifications.Post(new SingleFriendOnlineNotification(onlineAlertQueue.Single()));
|
||||
else
|
||||
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 (!notifyOnFriendPresenceChange.Value)
|
||||
{
|
||||
lastOfflineAlertTime = null;
|
||||
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 (offlineAlertQueue.Count == 1)
|
||||
notifications.Post(new SingleFriendOfflineNotification(offlineAlertQueue.Single()));
|
||||
else
|
||||
else if (offlineAlertQueue.Count > 1)
|
||||
notifications.Post(new MultipleFriendsOfflineNotification(offlineAlertQueue.ToArray()));
|
||||
|
||||
offlineAlertQueue.Clear();
|
||||
lastOfflineAlertTime = null;
|
||||
nextOfflineAlertTime = null;
|
||||
}
|
||||
|
||||
public partial class SingleFriendOnlineNotification : UserAvatarNotification
|
||||
private partial class SingleFriendOnlineNotification : UserAvatarNotification
|
||||
{
|
||||
public SingleFriendOnlineNotification(APIUser user)
|
||||
: base(user)
|
||||
{
|
||||
Transient = true;
|
||||
IsImportant = false;
|
||||
Text = $"Online: {User.Username}";
|
||||
Text = NotificationsStrings.FriendOnline(User.Username);
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@@ -215,11 +217,11 @@ namespace osu.Game.Online
|
||||
public override string PopInSampleName => "UI/notification-friend-online";
|
||||
}
|
||||
|
||||
public partial class MultipleFriendsOnlineNotification : SimpleNotification
|
||||
private partial class MultipleFriendsOnlineNotification : SimpleNotification
|
||||
{
|
||||
public MultipleFriendsOnlineNotification(ICollection<APIUser> users)
|
||||
{
|
||||
Text = $"Online: {string.Join(@", ", users.Select(u => u.Username))}";
|
||||
Text = NotificationsStrings.FriendOnline(string.Join(@", ", users.Select(u => u.Username)));
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@@ -232,14 +234,14 @@ namespace osu.Game.Online
|
||||
public override string PopInSampleName => "UI/notification-friend-online";
|
||||
}
|
||||
|
||||
public partial class SingleFriendOfflineNotification : UserAvatarNotification
|
||||
private partial class SingleFriendOfflineNotification : UserAvatarNotification
|
||||
{
|
||||
public SingleFriendOfflineNotification(APIUser user)
|
||||
: base(user)
|
||||
{
|
||||
Transient = true;
|
||||
IsImportant = false;
|
||||
Text = $"Offline: {User.Username}";
|
||||
Text = NotificationsStrings.FriendOffline(User.Username);
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@@ -252,11 +254,11 @@ namespace osu.Game.Online
|
||||
public override string PopInSampleName => "UI/notification-friend-offline";
|
||||
}
|
||||
|
||||
public partial class MultipleFriendsOfflineNotification : SimpleNotification
|
||||
private partial class MultipleFriendsOfflineNotification : SimpleNotification
|
||||
{
|
||||
public MultipleFriendsOfflineNotification(ICollection<APIUser> users)
|
||||
{
|
||||
Text = $"Offline: {string.Join(@", ", users.Select(u => u.Username))}";
|
||||
Text = NotificationsStrings.FriendOffline(string.Join(@", ", users.Select(u => u.Username)));
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
|
||||
@@ -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());
|
||||
});
|
||||
|
||||
@@ -53,5 +53,10 @@ namespace osu.Game.Online.Metadata
|
||||
/// Signals to the server that the current user would like to stop receiving updates about the state of the multiplayer room with the given <paramref name="id"/>.
|
||||
/// </summary>
|
||||
Task EndWatchingMultiplayerRoom(long id);
|
||||
|
||||
/// <summary>
|
||||
/// Refresh this user's friend listing.
|
||||
/// </summary>
|
||||
Task RefreshFriends();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Users;
|
||||
|
||||
@@ -21,6 +22,16 @@ namespace osu.Game.Online.Metadata
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; } = null!;
|
||||
|
||||
private readonly IBindableList<APIRelation> localFriends = new BindableList<APIRelation>();
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
localFriends.BindTo(api.LocalUserState.Friends);
|
||||
localFriends.BindCollectionChanged((_, _) => RefreshFriends().FireAndForget());
|
||||
}
|
||||
|
||||
#region Beatmap metadata updates
|
||||
|
||||
public abstract Task<BeatmapUpdates> GetChangesSince(int queueId);
|
||||
@@ -152,6 +163,8 @@ namespace osu.Game.Online.Metadata
|
||||
|
||||
public abstract Task EndWatchingMultiplayerRoom(long id);
|
||||
|
||||
public abstract Task RefreshFriends();
|
||||
|
||||
public event Action<MultiplayerRoomScoreSetEvent>? MultiplayerRoomScoreSet;
|
||||
|
||||
Task IMetadataClient.MultiplayerRoomScoreSet(MultiplayerRoomScoreSetEvent roomScoreSetEvent)
|
||||
|
||||
@@ -288,6 +288,15 @@ namespace osu.Game.Online.Metadata
|
||||
Logger.Log($@"{nameof(OnlineMetadataClient)} stopped watching multiplayer room with ID {id}", LoggingTarget.Network);
|
||||
}
|
||||
|
||||
public override async Task RefreshFriends()
|
||||
{
|
||||
if (connector?.IsConnected.Value != true)
|
||||
throw new OperationCanceledException();
|
||||
|
||||
Debug.Assert(connection != null);
|
||||
await connection.InvokeAsync(nameof(IMetadataServer.RefreshFriends)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public override async Task DisconnectRequested()
|
||||
{
|
||||
await base.DisconnectRequested().ConfigureAwait(false);
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -5,6 +5,7 @@ using System;
|
||||
using Humanizer.Localisation;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
using osu.Game.Utils;
|
||||
|
||||
@@ -53,10 +54,10 @@ namespace osu.Game.Online.Multiplayer
|
||||
if (remaining.TotalSeconds <= 5)
|
||||
{
|
||||
updateDelegate?.Cancel();
|
||||
Text = "The multiplayer server will be right back...";
|
||||
Text = NotificationsStrings.MultiplayerServerShuttingDownImmediately;
|
||||
}
|
||||
else
|
||||
Text = $"The multiplayer server is restarting in {HumanizerUtils.Humanize(remaining, precision: 3, minUnit: TimeUnit.Second)}.";
|
||||
Text = NotificationsStrings.MultiplayerServerShuttingDownRemaining(HumanizerUtils.Humanize(remaining, precision: 3, minUnit: TimeUnit.Second));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -84,7 +84,7 @@ namespace osu.Game.Overlays.Settings.Sections.General
|
||||
ProgressNotification notification = new ProgressNotification
|
||||
{
|
||||
State = ProgressNotificationState.Active,
|
||||
Text = "Exporting logs...",
|
||||
Text = NotificationsStrings.LogsExportOngoing,
|
||||
};
|
||||
|
||||
notifications?.Post(notification);
|
||||
@@ -116,7 +116,7 @@ namespace osu.Game.Overlays.Settings.Sections.General
|
||||
throw;
|
||||
}
|
||||
|
||||
notification.CompletionText = "Exported logs! Click to view.";
|
||||
notification.CompletionText = NotificationsStrings.LogsExportFinished;
|
||||
notification.CompletionClickAction = () => exportStorage.PresentFileExternally(archive_filename);
|
||||
|
||||
notification.State = ProgressNotificationState.Completed;
|
||||
|
||||
@@ -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!;
|
||||
|
||||
@@ -42,7 +42,7 @@ namespace osu.Game.Screens.Edit.Components
|
||||
set => current.Current = value;
|
||||
}
|
||||
|
||||
public Func<FileInfo, string>? SampleAddRequested { get; init; }
|
||||
public Func<FileInfo, string, string>? SampleAddRequested { get; init; }
|
||||
public Action<string>? SampleRemoveRequested { get; init; }
|
||||
|
||||
private readonly BindableWithCurrent<EditorBeatmapSkin.SampleSet?> current = new BindableWithCurrent<EditorBeatmapSkin.SampleSet?>();
|
||||
@@ -194,7 +194,7 @@ namespace osu.Game.Screens.Edit.Components
|
||||
/// <summary>
|
||||
/// Invoked when a new sample is selected via this button.
|
||||
/// </summary>
|
||||
public Func<FileInfo, string>? SampleAddRequested { get; init; }
|
||||
public Func<FileInfo, string, string>? SampleAddRequested { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when a sample removal is selected via this button.
|
||||
@@ -294,7 +294,18 @@ namespace osu.Game.Screens.Edit.Components
|
||||
|
||||
AddInternal(hoverSounds = (ActualFilename.Value == null ? new HoverClickSounds(HoverSampleSet.Button) : new HoverSounds(HoverSampleSet.Button)));
|
||||
|
||||
sample = ActualFilename.Value == null ? null : editorBeatmap?.BeatmapSkin?.Skin.Samples?.Get(ActualFilename.Value);
|
||||
if (ActualFilename.Value != null)
|
||||
{
|
||||
// to cover all bases, invalidate the extensionless filename (which gameplay is most likely to use)
|
||||
// as well as the filename with extension (which we are using here).
|
||||
editorBeatmap?.BeatmapSkin?.Skin.Samples?.Invalidate(ExpectedFilename.Value);
|
||||
editorBeatmap?.BeatmapSkin?.Skin.Samples?.Invalidate(ActualFilename.Value);
|
||||
sample = editorBeatmap?.BeatmapSkin?.Skin.Samples?.Get(ActualFilename.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
sample = null;
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
@@ -317,7 +328,7 @@ namespace osu.Game.Screens.Edit.Components
|
||||
return;
|
||||
|
||||
this.HidePopover();
|
||||
ActualFilename.Value = SampleAddRequested?.Invoke(selectedFile.Value) ?? selectedFile.Value.ToString();
|
||||
ActualFilename.Value = SampleAddRequested?.Invoke(selectedFile.Value, ExpectedFilename.Value) ?? selectedFile.Value.ToString();
|
||||
}
|
||||
|
||||
private void deleteSample()
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -101,11 +101,8 @@ namespace osu.Game.Screens.Edit
|
||||
|
||||
this.beatmapInfo = beatmapInfo ?? playableBeatmap.BeatmapInfo;
|
||||
|
||||
if (beatmapSkin is Skin skin)
|
||||
{
|
||||
BeatmapSkin = new EditorBeatmapSkin(skin);
|
||||
BeatmapSkin.BeatmapSkinChanged += SaveState;
|
||||
}
|
||||
if (beatmapSkin is LegacyBeatmapSkin skin)
|
||||
BeatmapSkin = new EditorBeatmapSkin(this, skin);
|
||||
|
||||
beatmapProcessor = new EditorBeatmapProcessor(this, playableBeatmap.BeatmapInfo.Ruleset.CreateInstance());
|
||||
|
||||
@@ -532,5 +529,11 @@ namespace osu.Game.Screens.Edit
|
||||
public double GetBeatLengthAtTime(double referenceTime) => ControlPointInfo.TimingPointAt(referenceTime).BeatLength / BeatDivisor;
|
||||
|
||||
public int BeatDivisor => beatDivisor?.Value ?? 1;
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
BeatmapSkin?.Dispose();
|
||||
base.Dispose(isDisposing);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,14 +18,14 @@ namespace osu.Game.Screens.Edit
|
||||
/// <summary>
|
||||
/// A beatmap skin which is being edited.
|
||||
/// </summary>
|
||||
public class EditorBeatmapSkin : ISkin
|
||||
public class EditorBeatmapSkin : ISkin, IDisposable
|
||||
{
|
||||
public event Action? BeatmapSkinChanged;
|
||||
|
||||
/// <summary>
|
||||
/// The underlying beatmap skin.
|
||||
/// </summary>
|
||||
protected internal readonly Skin Skin;
|
||||
protected internal readonly LegacyBeatmapSkin Skin;
|
||||
|
||||
/// <summary>
|
||||
/// The combo colours of this skin.
|
||||
@@ -33,10 +33,13 @@ namespace osu.Game.Screens.Edit
|
||||
/// </summary>
|
||||
public BindableList<Colour4> ComboColours { get; }
|
||||
|
||||
public EditorBeatmapSkin(Skin skin)
|
||||
{
|
||||
Skin = skin;
|
||||
private readonly EditorBeatmap editorBeatmap;
|
||||
|
||||
public EditorBeatmapSkin(EditorBeatmap editorBeatmap, LegacyBeatmapSkin skin)
|
||||
{
|
||||
this.editorBeatmap = editorBeatmap;
|
||||
|
||||
Skin = skin;
|
||||
ComboColours = new BindableList<Colour4>();
|
||||
|
||||
if (Skin.Configuration.ComboColours is IReadOnlyList<Color4> comboColours)
|
||||
@@ -50,9 +53,14 @@ namespace osu.Game.Screens.Edit
|
||||
}
|
||||
|
||||
ComboColours.BindCollectionChanged((_, _) => updateColours());
|
||||
|
||||
if (skin.BeatmapSetResources != null)
|
||||
skin.BeatmapSetResources.CacheInvalidated += InvokeSkinChanged;
|
||||
}
|
||||
|
||||
private void invokeSkinChanged() => BeatmapSkinChanged?.Invoke();
|
||||
public void InvokeSkinChanged() => BeatmapSkinChanged?.Invoke();
|
||||
|
||||
#region Combo colours
|
||||
|
||||
private void updateColours()
|
||||
{
|
||||
@@ -60,9 +68,14 @@ namespace osu.Game.Screens.Edit
|
||||
Skin.Configuration.CustomComboColours.Clear();
|
||||
for (int i = 0; i < ComboColours.Count; ++i)
|
||||
Skin.Configuration.CustomComboColours.Add(ComboColours[(ComboColours.Count + i - 1) % ComboColours.Count]);
|
||||
invokeSkinChanged();
|
||||
InvokeSkinChanged();
|
||||
editorBeatmap.SaveState();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Sample sets
|
||||
|
||||
public record SampleSet(int SampleSetIndex, string Name)
|
||||
{
|
||||
public SampleSet(int sampleSetIndex)
|
||||
@@ -88,7 +101,7 @@ namespace osu.Game.Screens.Edit
|
||||
|
||||
string[] possiblePrefixes = possibleSounds.SelectMany(sound => possibleBanks.Select(bank => $@"{bank}-{sound}")).ToArray();
|
||||
|
||||
HashSet<int> indices = new HashSet<int>();
|
||||
Dictionary<int, SampleSet> sampleSets = new Dictionary<int, SampleSet>();
|
||||
|
||||
if (Skin.Samples != null)
|
||||
{
|
||||
@@ -96,19 +109,39 @@ namespace osu.Game.Screens.Edit
|
||||
{
|
||||
foreach (string possiblePrefix in possiblePrefixes)
|
||||
{
|
||||
if (!sample.StartsWith(possiblePrefix, StringComparison.InvariantCultureIgnoreCase))
|
||||
if (!sample.StartsWith(possiblePrefix, StringComparison.Ordinal))
|
||||
continue;
|
||||
|
||||
string indexString = Path.GetFileNameWithoutExtension(sample)[possiblePrefix.Length..];
|
||||
int? index = null;
|
||||
|
||||
if (string.IsNullOrEmpty(indexString))
|
||||
indices.Add(1);
|
||||
if (int.TryParse(indexString, out int index))
|
||||
indices.Add(index);
|
||||
index = 1;
|
||||
if (int.TryParse(indexString, out int parsed))
|
||||
index = parsed;
|
||||
|
||||
if (!index.HasValue)
|
||||
continue;
|
||||
|
||||
SampleSet? sampleSet;
|
||||
if (!sampleSets.TryGetValue(index.Value, out sampleSet))
|
||||
sampleSet = sampleSets[index.Value] = new SampleSet(index.Value);
|
||||
|
||||
sampleSet.Filenames.Add(sample);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return indices.OrderBy(i => i).Select(i => new SampleSet(i));
|
||||
return sampleSets.OrderBy(i => i.Key).Select(i => i.Value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Skin.BeatmapSetResources != null)
|
||||
Skin.BeatmapSetResources.CacheInvalidated -= InvokeSkinChanged;
|
||||
Skin.Dispose();
|
||||
}
|
||||
|
||||
#region Delegated ISkin implementation
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
// 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.Globalization;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Setup
|
||||
{
|
||||
public partial class FormSampleSetChooser : FormDropdown<EditorBeatmapSkin.SampleSet?>, IHasPopover
|
||||
{
|
||||
private EditorBeatmapSkin? beatmapSkin;
|
||||
|
||||
public FormSampleSetChooser()
|
||||
{
|
||||
Caption = "Custom sample sets";
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(EditorBeatmap editorBeatmap)
|
||||
{
|
||||
beatmapSkin = editorBeatmap.BeatmapSkin;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
populateItems();
|
||||
if (beatmapSkin != null)
|
||||
beatmapSkin.BeatmapSkinChanged += populateItems;
|
||||
|
||||
Current.Value = Items.FirstOrDefault(i => i?.SampleSetIndex > 0);
|
||||
Current.BindValueChanged(val =>
|
||||
{
|
||||
if (val.NewValue?.SampleSetIndex == -1)
|
||||
this.ShowPopover();
|
||||
});
|
||||
}
|
||||
|
||||
private void populateItems()
|
||||
{
|
||||
var items = beatmapSkin?.GetAvailableSampleSets().ToList() ?? [];
|
||||
items.Add(new EditorBeatmapSkin.SampleSet(-1, "Add new..."));
|
||||
Items = items;
|
||||
}
|
||||
|
||||
protected override LocalisableString GenerateItemText(EditorBeatmapSkin.SampleSet? item)
|
||||
{
|
||||
if (item == null)
|
||||
return string.Empty;
|
||||
|
||||
return base.GenerateItemText(item);
|
||||
}
|
||||
|
||||
public Popover GetPopover() => new NewSampleSetPopover(
|
||||
Items.Any(i => i?.SampleSetIndex > 0) ? Items.Max(i => i!.SampleSetIndex) : 0,
|
||||
idx =>
|
||||
{
|
||||
if (idx == null)
|
||||
{
|
||||
Current.Value = Items.FirstOrDefault(i => i?.SampleSetIndex > 0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Items.SingleOrDefault(i => i?.SampleSetIndex == idx) is EditorBeatmapSkin.SampleSet existing)
|
||||
{
|
||||
Current.Value = existing;
|
||||
return;
|
||||
}
|
||||
|
||||
var sampleSet = new EditorBeatmapSkin.SampleSet(idx.Value, $@"Custom #{idx}");
|
||||
var newItems = Items.ToList();
|
||||
newItems.Insert(newItems.Count - 1, sampleSet);
|
||||
Items = newItems;
|
||||
Current.Value = sampleSet;
|
||||
});
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
if (beatmapSkin != null)
|
||||
beatmapSkin.BeatmapSkinChanged -= populateItems;
|
||||
|
||||
base.Dispose(isDisposing);
|
||||
}
|
||||
|
||||
private partial class NewSampleSetPopover : OsuPopover
|
||||
{
|
||||
private readonly int currentLargestIndex;
|
||||
private readonly Action<int?> onCommit;
|
||||
|
||||
private int? committedIndex;
|
||||
|
||||
private LabelledNumberBox numberBox = null!;
|
||||
|
||||
public NewSampleSetPopover(int currentLargestIndex, Action<int?> onCommit)
|
||||
{
|
||||
this.currentLargestIndex = currentLargestIndex;
|
||||
this.onCommit = onCommit;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Child = numberBox = new LabelledNumberBox
|
||||
{
|
||||
RelativeSizeAxes = Axes.None,
|
||||
Width = 250,
|
||||
Label = "Sample set index",
|
||||
Current = { Value = (currentLargestIndex + 1).ToString(CultureInfo.InvariantCulture) }
|
||||
};
|
||||
numberBox.OnCommit += (_, _) =>
|
||||
{
|
||||
committedIndex = int.Parse(numberBox.Current.Value);
|
||||
Hide();
|
||||
};
|
||||
}
|
||||
|
||||
protected override void OnFocus(FocusEvent e)
|
||||
{
|
||||
base.OnFocus(e);
|
||||
// avoids infinite refocus loop
|
||||
if (committedIndex == null)
|
||||
GetContainingFocusManager()?.ChangeFocus(numberBox);
|
||||
}
|
||||
|
||||
public override void Hide()
|
||||
{
|
||||
if (State.Value == Visibility.Visible)
|
||||
onCommit.Invoke(committedIndex > 0 ? committedIndex : null);
|
||||
base.Hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ using osu.Game.Localisation;
|
||||
using osu.Game.Models;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Screens.Backgrounds;
|
||||
using osu.Game.Screens.Edit.Components;
|
||||
using osu.Game.Utils;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Setup
|
||||
@@ -23,6 +24,8 @@ namespace osu.Game.Screens.Edit.Setup
|
||||
private FormBeatmapFileSelector audioTrackChooser = null!;
|
||||
private FormBeatmapFileSelector backgroundChooser = null!;
|
||||
|
||||
private readonly Bindable<EditorBeatmapSkin.SampleSet?> currentSampleSet = new Bindable<EditorBeatmapSkin.SampleSet?>();
|
||||
|
||||
public override LocalisableString Title => EditorSetupStrings.ResourcesHeader;
|
||||
|
||||
[Resolved]
|
||||
@@ -65,6 +68,27 @@ namespace osu.Game.Screens.Edit.Setup
|
||||
Caption = EditorSetupStrings.AudioTrack,
|
||||
PlaceholderText = EditorSetupStrings.ClickToSelectTrack,
|
||||
},
|
||||
new FormSampleSetChooser
|
||||
{
|
||||
Current = { BindTarget = currentSampleSet },
|
||||
},
|
||||
new FormSampleSet
|
||||
{
|
||||
Current = { BindTarget = currentSampleSet },
|
||||
SampleAddRequested = (file, targetName) =>
|
||||
{
|
||||
string actualFilename = string.Concat(targetName, file.Extension);
|
||||
using var stream = file.OpenRead();
|
||||
beatmaps.AddFile(working.Value.BeatmapSetInfo, stream, actualFilename);
|
||||
return actualFilename;
|
||||
},
|
||||
SampleRemoveRequested = filename =>
|
||||
{
|
||||
var file = working.Value.BeatmapSetInfo.GetFile(filename);
|
||||
if (file != null)
|
||||
beatmaps.DeleteFile(working.Value.BeatmapSetInfo, file);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
backgroundChooser.PreviewContainer.Add(headerBackground);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ using osu.Game.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Input.StateChanges;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
|
||||
@@ -258,15 +257,6 @@ namespace osu.Game.Screens.Menu
|
||||
|
||||
protected override void OnMouseUp(MouseUpEvent e)
|
||||
{
|
||||
// HORRIBLE HACK
|
||||
// This is here so that on mobile, the main menu button that progresses to song select can correctly progress to song select v2 when held.
|
||||
// Once the temporary solution of holding the button to access song select v2 is removed, this should be too.
|
||||
// Without this, the long-press-to-right-click flow intercepts the hold and converts it to a right click which would not trigger the button
|
||||
// and therefore not progress to song select.
|
||||
if (e.Button == MouseButton.Right && e.CurrentState.Mouse.LastSource is ISourcedFromTouch)
|
||||
trigger(e);
|
||||
// END OF HORRIBLE HACK
|
||||
|
||||
boxHoverLayer.FadeTo(0, 1000, Easing.OutQuint);
|
||||
base.OnMouseUp(e);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Input.StateChanges;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Graphics.Backgrounds;
|
||||
@@ -393,27 +392,12 @@ namespace osu.Game.Screens.Menu
|
||||
|
||||
protected override void OnMouseUp(MouseUpEvent e)
|
||||
{
|
||||
// HORRIBLE HACK
|
||||
// This is here so that on mobile, the logo can correctly progress from main menu to song select v2 when held.
|
||||
// Once the temporary solution of holding the logo to access song select v2 is removed, this should be too.
|
||||
// Without this, the long-press-to-right-click flow intercepts the hold and converts it to a right click which would not trigger the logo
|
||||
// and therefore not progress to song select.
|
||||
if (e.Button == MouseButton.Right && e.CurrentState.Mouse.LastSource is ISourcedFromTouch)
|
||||
triggerClick();
|
||||
// END OF HORRIBLE HACK
|
||||
|
||||
if (e.Button != MouseButton.Left) return;
|
||||
|
||||
logoBounceContainer.ScaleTo(1f, 500, Easing.OutElastic);
|
||||
}
|
||||
|
||||
protected override bool OnClick(ClickEvent e)
|
||||
{
|
||||
triggerClick();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void triggerClick()
|
||||
{
|
||||
flashLayer.ClearTransforms();
|
||||
flashLayer.Alpha = 0.4f;
|
||||
@@ -425,6 +409,8 @@ namespace osu.Game.Screens.Menu
|
||||
sampleClickChannel = sampleClick.GetChannel();
|
||||
sampleClickChannel.Play();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user