1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-14 15:17:27 +08:00

Merge pull request #25743 from peppy/merge-everything-at-once

Merge everything at once
This commit is contained in:
Dean Herbert 2023-12-13 16:17:06 +09:00 committed by GitHub
commit ec6200b4ad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 354 additions and 122 deletions

View File

@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.1201.1" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.1213.0" />
</ItemGroup>
<PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged.

View File

@ -310,9 +310,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddStep("release left button", () => InputManager.ReleaseButton(MouseButton.Left));
assertPlaced(true);
assertLength(760, tolerance: 10);
assertLength(808, tolerance: 10);
assertControlPointCount(5);
assertControlPointType(0, PathType.BSpline(3));
assertControlPointType(0, PathType.BSpline(4));
assertControlPointType(1, null);
assertControlPointType(2, null);
assertControlPointType(3, null);
@ -337,9 +337,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true);
assertLength(600, tolerance: 10);
assertControlPointCount(4);
assertControlPointType(0, PathType.LINEAR);
assertControlPointType(1, null);
assertControlPointType(2, null);
assertControlPointType(0, PathType.BSpline(4));
assertControlPointType(1, PathType.BSpline(4));
assertControlPointType(2, PathType.BSpline(4));
assertControlPointType(3, null);
}

View File

@ -373,7 +373,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
curveTypeItems.Add(createMenuItemForPathType(PathType.LINEAR));
curveTypeItems.Add(createMenuItemForPathType(PathType.PERFECT_CURVE));
curveTypeItems.Add(createMenuItemForPathType(PathType.BEZIER));
curveTypeItems.Add(createMenuItemForPathType(PathType.BSpline(3)));
curveTypeItems.Add(createMenuItemForPathType(PathType.BSpline(4)));
if (selectedPieces.Any(piece => piece.ControlPoint.Type?.Type == SplineType.Catmull))
curveTypeItems.Add(createMenuItemForPathType(PathType.CATMULL));

View File

@ -3,6 +3,7 @@
#nullable disable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
@ -41,15 +42,18 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private int currentSegmentLength;
[Resolved(CanBeNull = true)]
[CanBeNull]
private IPositionSnapProvider positionSnapProvider { get; set; }
[Resolved(CanBeNull = true)]
[CanBeNull]
private IDistanceSnapProvider distanceSnapProvider { get; set; }
[Resolved(CanBeNull = true)]
[CanBeNull]
private FreehandSliderToolboxGroup freehandToolboxGroup { get; set; }
private readonly IncrementalBSplineBuilder bSplineBuilder = new IncrementalBSplineBuilder();
private readonly IncrementalBSplineBuilder bSplineBuilder = new IncrementalBSplineBuilder { Degree = 4 };
protected override bool IsValidForPlacement => HitObject.Path.HasValidLength;
@ -94,6 +98,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
bSplineBuilder.CornerThreshold = e.NewValue;
Scheduler.AddOnce(updateSliderPathFromBSplineBuilder);
}, true);
freehandToolboxGroup.CircleThreshold.BindValueChanged(e =>
{
Scheduler.AddOnce(updateSliderPathFromBSplineBuilder);
}, true);
}
}
@ -197,7 +206,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
base.OnDragEnd(e);
if (state == SliderPlacementState.Drawing)
{
bSplineBuilder.Finish();
updateSliderPathFromBSplineBuilder();
// Change the state so it will snap the expected distance in endCurve.
state = SliderPlacementState.Finishing;
endCurve();
}
}
protected override void OnMouseUp(MouseUpEvent e)
@ -232,7 +248,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{
if (state == SliderPlacementState.Drawing)
{
segmentStart.Type = PathType.BSpline(3);
segmentStart.Type = PathType.BSpline(4);
return;
}
@ -300,7 +316,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private void updateSlider()
{
HitObject.Path.ExpectedDistance.Value = distanceSnapProvider?.FindSnappedDistance(HitObject, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance;
if (state == SliderPlacementState.Drawing)
HitObject.Path.ExpectedDistance.Value = (float)HitObject.Path.CalculatedDistance;
else
HitObject.Path.ExpectedDistance.Value = distanceSnapProvider?.FindSnappedDistance(HitObject, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance;
bodyPiece.UpdateFrom(HitObject);
headCirclePiece.UpdateFrom(HitObject.HeadCircle);
@ -309,53 +328,126 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private void updateSliderPathFromBSplineBuilder()
{
IReadOnlyList<Vector2> builderPoints = bSplineBuilder.ControlPoints;
IReadOnlyList<List<Vector2>> builderPoints = bSplineBuilder.ControlPoints;
if (builderPoints.Count == 0)
if (builderPoints.Count == 0 || builderPoints[0].Count == 0)
return;
int lastSegmentStart = 0;
PathType? lastPathType = null;
HitObject.Path.ControlPoints.Clear();
// Iterate through generated points, finding each segment and adding non-inheriting path types where appropriate.
// Importantly, the B-Spline builder returns three Vector2s at the same location when a new segment is to be started.
// Iterate through generated segments and adding non-inheriting path types where appropriate.
for (int i = 0; i < builderPoints.Count; i++)
{
bool isLastPoint = i == builderPoints.Count - 1;
bool isNewSegment = i < builderPoints.Count - 2 && builderPoints[i] == builderPoints[i + 1] && builderPoints[i] == builderPoints[i + 2];
bool isLastSegment = i == builderPoints.Count - 1;
var segment = builderPoints[i];
if (isNewSegment || isLastPoint)
if (segment.Count == 0)
continue;
// Replace this segment with a circular arc if it is a reasonable substitute.
var circleArcSegment = tryCircleArc(segment);
if (circleArcSegment is not null)
{
int pointsInSegment = i - lastSegmentStart;
// Where possible, we can use the simpler LINEAR path type.
PathType? pathType = pointsInSegment == 1 ? PathType.LINEAR : PathType.BSpline(3);
// Linear segments can be combined, as two adjacent linear sections are computationally the same as one with the points combined.
if (lastPathType == pathType && lastPathType == PathType.LINEAR)
pathType = null;
HitObject.Path.ControlPoints.Add(new PathControlPoint(builderPoints[lastSegmentStart], pathType));
for (int j = lastSegmentStart + 1; j < i; j++)
HitObject.Path.ControlPoints.Add(new PathControlPoint(builderPoints[j]));
if (isLastPoint)
HitObject.Path.ControlPoints.Add(new PathControlPoint(builderPoints[i]));
// Skip the redundant duplicated points (see isNewSegment above) which have been coalesced into a path type.
lastSegmentStart = (i += 2);
if (pathType != null) lastPathType = pathType;
HitObject.Path.ControlPoints.Add(new PathControlPoint(circleArcSegment[0], PathType.PERFECT_CURVE));
HitObject.Path.ControlPoints.Add(new PathControlPoint(circleArcSegment[1]));
}
else
{
HitObject.Path.ControlPoints.Add(new PathControlPoint(segment[0], PathType.BSpline(4)));
for (int j = 1; j < segment.Count - 1; j++)
HitObject.Path.ControlPoints.Add(new PathControlPoint(segment[j]));
}
if (isLastSegment)
HitObject.Path.ControlPoints.Add(new PathControlPoint(segment[^1]));
}
}
private Vector2[] tryCircleArc(List<Vector2> segment)
{
if (segment.Count < 3 || freehandToolboxGroup?.CircleThreshold.Value == 0) return null;
// Assume the segment creates a reasonable circular arc and then check if it reasonable
var points = PathApproximator.BSplineToPiecewiseLinear(segment.ToArray(), bSplineBuilder.Degree);
var circleArcControlPoints = new[] { points[0], points[points.Count / 2], points[^1] };
var circleArc = new CircularArcProperties(circleArcControlPoints);
if (!circleArc.IsValid) return null;
double length = circleArc.ThetaRange * circleArc.Radius;
if (length > 1000) return null;
double loss = 0;
Vector2? lastPoint = null;
Vector2? lastVec = null;
Vector2? lastVec2 = null;
int? lastDir = null;
int? lastDir2 = null;
double totalWinding = 0;
// Loop through the points and check if they are not too far away from the circular arc.
// Also make sure it curves monotonically in one direction and at most one loop is done.
foreach (var point in points)
{
var vec = point - circleArc.Centre;
loss += Math.Pow((vec.Length - circleArc.Radius) / length, 2);
if (lastVec.HasValue)
{
double det = lastVec.Value.X * vec.Y - lastVec.Value.Y * vec.X;
int dir = Math.Sign(det);
if (dir == 0)
continue;
if (lastDir.HasValue && dir != lastDir)
return null; // Circle center is not inside the polygon
lastDir = dir;
}
lastVec = vec;
if (lastPoint.HasValue)
{
var vec2 = point - lastPoint.Value;
if (lastVec2.HasValue)
{
double dot = Vector2.Dot(vec2, lastVec2.Value);
double det = lastVec2.Value.X * vec2.Y - lastVec2.Value.Y * vec2.X;
double angle = Math.Atan2(det, dot);
int dir2 = Math.Sign(angle);
if (dir2 == 0)
continue;
if (lastDir2.HasValue && dir2 != lastDir2)
return null; // Curvature changed, like in an S-shape
totalWinding += Math.Abs(angle);
lastDir2 = dir2;
}
lastVec2 = vec2;
}
lastPoint = point;
}
loss /= points.Count;
return loss > freehandToolboxGroup?.CircleThreshold.Value || totalWinding > MathHelper.TwoPi ? null : circleArcControlPoints;
}
private enum SliderPlacementState
{
Initial,
ControlPoints,
Drawing
Drawing,
Finishing
}
}
}

View File

@ -17,10 +17,10 @@ namespace osu.Game.Rulesets.Osu.Edit
{
}
public BindableFloat Tolerance { get; } = new BindableFloat(1.5f)
public BindableFloat Tolerance { get; } = new BindableFloat(1.8f)
{
MinValue = 0.05f,
MaxValue = 3f,
MaxValue = 2.0f,
Precision = 0.01f
};
@ -31,8 +31,15 @@ namespace osu.Game.Rulesets.Osu.Edit
Precision = 0.01f
};
public BindableFloat CircleThreshold { get; } = new BindableFloat(0.0015f)
{
MinValue = 0f,
MaxValue = 0.005f,
Precision = 0.0001f
};
// We map internal ranges to a more standard range of values for display to the user.
private readonly BindableInt displayTolerance = new BindableInt(40)
private readonly BindableInt displayTolerance = new BindableInt(90)
{
MinValue = 5,
MaxValue = 100
@ -44,8 +51,15 @@ namespace osu.Game.Rulesets.Osu.Edit
MaxValue = 100
};
private readonly BindableInt displayCircleThreshold = new BindableInt(30)
{
MinValue = 0,
MaxValue = 100
};
private ExpandableSlider<int> toleranceSlider = null!;
private ExpandableSlider<int> cornerThresholdSlider = null!;
private ExpandableSlider<int> circleThresholdSlider = null!;
[BackgroundDependencyLoader]
private void load()
@ -59,6 +73,10 @@ namespace osu.Game.Rulesets.Osu.Edit
cornerThresholdSlider = new ExpandableSlider<int>
{
Current = displayCornerThreshold
},
circleThresholdSlider = new ExpandableSlider<int>
{
Current = displayCircleThreshold
}
};
}
@ -83,18 +101,32 @@ namespace osu.Game.Rulesets.Osu.Edit
CornerThreshold.Value = displayToInternalCornerThreshold(threshold.NewValue);
}, true);
displayCircleThreshold.BindValueChanged(threshold =>
{
circleThresholdSlider.ContractedLabelText = $"P. C. T.: {threshold.NewValue:N0}";
circleThresholdSlider.ExpandedLabelText = $"Perfect Curve Threshold: {threshold.NewValue:N0}";
CircleThreshold.Value = displayToInternalCircleThreshold(threshold.NewValue);
}, true);
Tolerance.BindValueChanged(tolerance =>
displayTolerance.Value = internalToDisplayTolerance(tolerance.NewValue)
);
CornerThreshold.BindValueChanged(threshold =>
displayCornerThreshold.Value = internalToDisplayCornerThreshold(threshold.NewValue)
);
CircleThreshold.BindValueChanged(threshold =>
displayCircleThreshold.Value = internalToDisplayCircleThreshold(threshold.NewValue)
);
float displayToInternalTolerance(float v) => v / 33f;
int internalToDisplayTolerance(float v) => (int)Math.Round(v * 33f);
float displayToInternalTolerance(float v) => v / 50f;
int internalToDisplayTolerance(float v) => (int)Math.Round(v * 50f);
float displayToInternalCornerThreshold(float v) => v / 100f;
int internalToDisplayCornerThreshold(float v) => (int)Math.Round(v * 100f);
float displayToInternalCircleThreshold(float v) => v / 20000f;
int internalToDisplayCircleThreshold(float v) => (int)Math.Round(v * 20000f);
}
}
}

View File

@ -132,7 +132,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
relativeGlowSize = Source.glowSize / Source.DrawSize.X;
}
public override void Draw(IRenderer renderer)
protected override void Draw(IRenderer renderer)
{
base.Draw(renderer);
drawGlow(renderer);

View File

@ -231,7 +231,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
points.AddRange(Source.SmokePoints.Skip(firstVisiblePointIndex).Take(futurePointIndex - firstVisiblePointIndex));
}
public sealed override void Draw(IRenderer renderer)
protected sealed override void Draw(IRenderer renderer)
{
base.Draw(renderer);

View File

@ -258,7 +258,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
private IUniformBuffer<CursorTrailParameters> cursorTrailParameters;
public override void Draw(IRenderer renderer)
protected override void Draw(IRenderer renderer)
{
base.Draw(renderer);

View File

@ -143,13 +143,13 @@ namespace osu.Game.Tests.Visual.Navigation
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
AddStep("set filter", () => songSelect.ChildrenOfType<SearchTextBox>().Single().Current.Value = "test");
AddStep("set filter", () => filterControlTextBox().Current.Value = "test");
AddStep("press back", () => InputManager.Click(MouseButton.Button1));
AddAssert("still at song select", () => Game.ScreenStack.CurrentScreen == songSelect);
AddAssert("filter cleared", () => string.IsNullOrEmpty(songSelect.ChildrenOfType<SearchTextBox>().Single().Current.Value));
AddAssert("filter cleared", () => string.IsNullOrEmpty(filterControlTextBox().Current.Value));
AddStep("set filter again", () => songSelect.ChildrenOfType<SearchTextBox>().Single().Current.Value = "test");
AddStep("set filter again", () => filterControlTextBox().Current.Value = "test");
AddStep("open collections dropdown", () =>
{
InputManager.MoveMouseTo(songSelect.ChildrenOfType<CollectionDropdown>().Single());
@ -163,10 +163,12 @@ namespace osu.Game.Tests.Visual.Navigation
.ChildrenOfType<Dropdown<CollectionFilterMenuItem>.DropdownMenu>().Single().State == MenuState.Closed);
AddStep("press back a second time", () => InputManager.Click(MouseButton.Button1));
AddAssert("filter cleared", () => string.IsNullOrEmpty(songSelect.ChildrenOfType<SearchTextBox>().Single().Current.Value));
AddAssert("filter cleared", () => string.IsNullOrEmpty(filterControlTextBox().Current.Value));
AddStep("press back a third time", () => InputManager.Click(MouseButton.Button1));
ConfirmAtMainMenu();
TextBox filterControlTextBox() => songSelect.ChildrenOfType<FilterControl.FilterControlTextBox>().Single();
}
[Test]

View File

@ -631,7 +631,7 @@ namespace osu.Game.Tests.Visual.Online
AddAssert("Nothing happened", () => this.ChildrenOfType<ReportChatPopover>().Any());
AddStep("Set report data", () =>
{
var field = this.ChildrenOfType<ReportChatPopover>().Single().ChildrenOfType<OsuTextBox>().Single();
var field = this.ChildrenOfType<ReportChatPopover>().Single().ChildrenOfType<OsuTextBox>().First();
field.Current.Value = "test other";
});

View File

@ -262,7 +262,7 @@ namespace osu.Game.Tests.Visual.Online
AddAssert("Nothing happened", () => this.ChildrenOfType<ReportCommentPopover>().Any());
AddStep("Set report data", () =>
{
var field = this.ChildrenOfType<ReportCommentPopover>().Single().ChildrenOfType<OsuTextBox>().Single();
var field = this.ChildrenOfType<ReportCommentPopover>().Single().ChildrenOfType<OsuTextBox>().First();
field.Current.Value = report_text;
var reason = this.ChildrenOfType<OsuEnumDropdown<CommentReportReason>>().Single();
reason.Current.Value = CommentReportReason.Other;

View File

@ -49,12 +49,12 @@ namespace osu.Game.Tests.Visual.Settings
AddStep("reset mouse", () => InputManager.MoveMouseTo(settings));
if (beforeLoad)
AddStep("set filter", () => settings.SectionsContainer.ChildrenOfType<SearchTextBox>().First().Current.Value = "scaling");
AddStep("set filter", () => settings.SectionsContainer.ChildrenOfType<SettingsSearchTextBox>().First().Current.Value = "scaling");
AddUntilStep("wait for items to load", () => settings.SectionsContainer.ChildrenOfType<IFilterable>().Any());
if (!beforeLoad)
AddStep("set filter", () => settings.SectionsContainer.ChildrenOfType<SearchTextBox>().First().Current.Value = "scaling");
AddStep("set filter", () => settings.SectionsContainer.ChildrenOfType<SettingsSearchTextBox>().First().Current.Value = "scaling");
AddAssert("ensure all items match filter", () => settings.SectionsContainer
.ChildrenOfType<SettingsSection>().Where(f => f.IsPresent)
@ -76,7 +76,7 @@ namespace osu.Game.Tests.Visual.Settings
AddUntilStep("wait for items to load", () => settings.SectionsContainer.ChildrenOfType<IFilterable>().Any());
AddStep("set filter", () => settings.SectionsContainer.ChildrenOfType<SearchTextBox>().First().Current.Value = "scaling");
AddStep("set filter", () => settings.SectionsContainer.ChildrenOfType<SettingsSearchTextBox>().First().Current.Value = "scaling");
}
[Test]
@ -94,7 +94,7 @@ namespace osu.Game.Tests.Visual.Settings
AddStep("reset mouse", () => InputManager.MoveMouseTo(settings));
AddUntilStep("sections loaded", () => settings.SectionsContainer.Children.Count > 0);
AddUntilStep("top-level textbox focused", () => settings.SectionsContainer.ChildrenOfType<FocusedTextBox>().FirstOrDefault()?.HasFocus == true);
AddUntilStep("top-level textbox focused", () => settings.SectionsContainer.ChildrenOfType<SettingsSearchTextBox>().FirstOrDefault()?.HasFocus == true);
AddStep("open key binding subpanel", () =>
{
@ -106,13 +106,13 @@ namespace osu.Game.Tests.Visual.Settings
AddUntilStep("binding panel textbox focused", () => settings
.ChildrenOfType<KeyBindingPanel>().FirstOrDefault()?
.ChildrenOfType<FocusedTextBox>().FirstOrDefault()?.HasFocus == true);
.ChildrenOfType<SettingsSearchTextBox>().FirstOrDefault()?.HasFocus == true);
AddStep("Press back", () => settings
.ChildrenOfType<KeyBindingPanel>().FirstOrDefault()?
.ChildrenOfType<SettingsSubPanel.BackButton>().FirstOrDefault()?.TriggerClick());
AddUntilStep("top-level textbox focused", () => settings.SectionsContainer.ChildrenOfType<FocusedTextBox>().FirstOrDefault()?.HasFocus == true);
AddUntilStep("top-level textbox focused", () => settings.SectionsContainer.ChildrenOfType<SettingsSearchTextBox>().FirstOrDefault()?.HasFocus == true);
}
[Test]
@ -121,7 +121,7 @@ namespace osu.Game.Tests.Visual.Settings
AddStep("reset mouse", () => InputManager.MoveMouseTo(settings));
AddUntilStep("sections loaded", () => settings.SectionsContainer.Children.Count > 0);
AddUntilStep("top-level textbox focused", () => settings.SectionsContainer.ChildrenOfType<FocusedTextBox>().FirstOrDefault()?.HasFocus == true);
AddUntilStep("top-level textbox focused", () => settings.SectionsContainer.ChildrenOfType<SettingsSearchTextBox>().FirstOrDefault()?.HasFocus == true);
AddStep("open key binding subpanel", () =>
{
@ -133,19 +133,19 @@ namespace osu.Game.Tests.Visual.Settings
AddUntilStep("binding panel textbox focused", () => settings
.ChildrenOfType<KeyBindingPanel>().FirstOrDefault()?
.ChildrenOfType<FocusedTextBox>().FirstOrDefault()?.HasFocus == true);
.ChildrenOfType<SettingsSearchTextBox>().FirstOrDefault()?.HasFocus == true);
AddStep("Escape", () => InputManager.Key(Key.Escape));
AddUntilStep("top-level textbox focused", () => settings.SectionsContainer.ChildrenOfType<FocusedTextBox>().FirstOrDefault()?.HasFocus == true);
AddUntilStep("top-level textbox focused", () => settings.SectionsContainer.ChildrenOfType<SettingsSearchTextBox>().FirstOrDefault()?.HasFocus == true);
}
[Test]
public void TestSearchTextBoxSelectedOnShow()
{
SearchTextBox searchTextBox = null!;
SettingsSearchTextBox searchTextBox = null!;
AddStep("set text", () => (searchTextBox = settings.SectionsContainer.ChildrenOfType<SearchTextBox>().First()).Current.Value = "some text");
AddStep("set text", () => (searchTextBox = settings.SectionsContainer.ChildrenOfType<SettingsSearchTextBox>().First()).Current.Value = "some text");
AddAssert("no text selected", () => searchTextBox.SelectedText == string.Empty);
AddRepeatStep("toggle visibility", () => settings.ToggleVisibility(), 2);
AddAssert("search text selected", () => searchTextBox.SelectedText == searchTextBox.Current.Value);

View File

@ -22,7 +22,6 @@ using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Database;
using osu.Game.Extensions;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Chat;
using osu.Game.Overlays;
@ -614,7 +613,7 @@ namespace osu.Game.Tests.Visual.SongSelect
AddUntilStep("has selection", () => songSelect!.Carousel.SelectedBeatmapInfo != null);
AddStep("set filter text", () => songSelect!.FilterControl.ChildrenOfType<SearchTextBox>().First().Text = "nonono");
AddStep("set filter text", () => songSelect!.FilterControl.ChildrenOfType<FilterControl.FilterControlTextBox>().First().Text = "nonono");
AddUntilStep("dummy selected", () => Beatmap.Value is DummyWorkingBeatmap);
@ -648,7 +647,7 @@ namespace osu.Game.Tests.Visual.SongSelect
AddUntilStep("carousel has correct", () => songSelect!.Carousel.SelectedBeatmapInfo?.MatchesOnlineID(target) == true);
AddUntilStep("game has correct", () => Beatmap.Value.BeatmapInfo.MatchesOnlineID(target));
AddStep("reset filter text", () => songSelect!.FilterControl.ChildrenOfType<SearchTextBox>().First().Text = string.Empty);
AddStep("reset filter text", () => songSelect!.FilterControl.ChildrenOfType<FilterControl.FilterControlTextBox>().First().Text = string.Empty);
AddAssert("game still correct", () => Beatmap.Value?.BeatmapInfo.MatchesOnlineID(target) == true);
AddAssert("carousel still correct", () => songSelect!.Carousel.SelectedBeatmapInfo.MatchesOnlineID(target));
@ -666,7 +665,7 @@ namespace osu.Game.Tests.Visual.SongSelect
AddUntilStep("has selection", () => songSelect!.Carousel.SelectedBeatmapInfo != null);
AddStep("set filter text", () => songSelect!.FilterControl.ChildrenOfType<SearchTextBox>().First().Text = "nonono");
AddStep("set filter text", () => songSelect!.FilterControl.ChildrenOfType<FilterControl.FilterControlTextBox>().First().Text = "nonono");
AddUntilStep("dummy selected", () => Beatmap.Value is DummyWorkingBeatmap);
@ -689,7 +688,7 @@ namespace osu.Game.Tests.Visual.SongSelect
AddUntilStep("carousel has correct", () => songSelect!.Carousel.SelectedBeatmapInfo?.MatchesOnlineID(target) == true);
AddUntilStep("game has correct", () => Beatmap.Value.BeatmapInfo.MatchesOnlineID(target));
AddStep("set filter text", () => songSelect!.FilterControl.ChildrenOfType<SearchTextBox>().First().Text = "nononoo");
AddStep("set filter text", () => songSelect!.FilterControl.ChildrenOfType<FilterControl.FilterControlTextBox>().First().Text = "nononoo");
AddUntilStep("game lost selection", () => Beatmap.Value is DummyWorkingBeatmap);
AddAssert("carousel lost selection", () => songSelect!.Carousel.SelectedBeatmapInfo == null);
@ -1135,7 +1134,7 @@ namespace osu.Game.Tests.Visual.SongSelect
{
createSongSelect();
AddStep("set filter text", () => songSelect!.FilterControl.ChildrenOfType<SearchTextBox>().First().Text = "nonono");
AddStep("set filter text", () => songSelect!.FilterControl.ChildrenOfType<FilterControl.FilterControlTextBox>().First().Text = "nonono");
AddStep("select all", () => InputManager.Keys(PlatformAction.SelectAll));
AddStep("press ctrl-x", () =>
{
@ -1144,7 +1143,7 @@ namespace osu.Game.Tests.Visual.SongSelect
InputManager.ReleaseKey(Key.ControlLeft);
});
AddAssert("filter text cleared", () => songSelect!.FilterControl.ChildrenOfType<SearchTextBox>().First().Text, () => Is.Empty);
AddAssert("filter text cleared", () => songSelect!.FilterControl.ChildrenOfType<FilterControl.FilterControlTextBox>().First().Text, () => Is.Empty);
}
private void waitForInitialSelection()

View File

@ -1,9 +1,17 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Input.States;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
namespace osu.Game.Tests.Visual.UserInterface
{
@ -13,8 +21,29 @@ namespace osu.Game.Tests.Visual.UserInterface
new OsuEnumDropdown<BeatmapOnlineStatus>
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Origin = Anchor.TopCentre,
Width = 150
};
[Test]
// todo: this can be written much better if ThemeComparisonTestScene has a manual input manager
public void TestBackAction()
{
AddStep("open", () => dropdown().ChildrenOfType<Menu>().Single().Open());
AddStep("press back", () => dropdown().OnPressed(new KeyBindingPressEvent<GlobalAction>(new InputState(), GlobalAction.Back)));
AddAssert("closed", () => dropdown().ChildrenOfType<Menu>().Single().State == MenuState.Closed);
AddStep("open", () => dropdown().ChildrenOfType<Menu>().Single().Open());
AddStep("type something", () => dropdown().ChildrenOfType<DropdownSearchBar>().Single().SearchTerm.Value = "something");
AddAssert("search bar visible", () => dropdown().ChildrenOfType<DropdownSearchBar>().Single().State.Value == Visibility.Visible);
AddStep("press back", () => dropdown().OnPressed(new KeyBindingPressEvent<GlobalAction>(new InputState(), GlobalAction.Back)));
AddAssert("text clear", () => dropdown().ChildrenOfType<DropdownSearchBar>().Single().SearchTerm.Value == string.Empty);
AddAssert("search bar hidden", () => dropdown().ChildrenOfType<DropdownSearchBar>().Single().State.Value == Visibility.Hidden);
AddAssert("still open", () => dropdown().ChildrenOfType<Menu>().Single().State == MenuState.Open);
AddStep("press back", () => dropdown().OnPressed(new KeyBindingPressEvent<GlobalAction>(new InputState(), GlobalAction.Back)));
AddAssert("closed", () => dropdown().ChildrenOfType<Menu>().Single().State == MenuState.Closed);
OsuEnumDropdown<BeatmapOnlineStatus> dropdown() => this.ChildrenOfType<OsuEnumDropdown<BeatmapOnlineStatus>>().First();
}
}
}

View File

@ -286,7 +286,7 @@ namespace osu.Game.Graphics.Backgrounds
private IUniformBuffer<TriangleBorderData> borderDataBuffer;
public override void Draw(IRenderer renderer)
protected override void Draw(IRenderer renderer)
{
base.Draw(renderer);

View File

@ -228,7 +228,7 @@ namespace osu.Game.Graphics.Backgrounds
private IUniformBuffer<TriangleBorderData>? borderDataBuffer;
public override void Draw(IRenderer renderer)
protected override void Draw(IRenderer renderer)
{
base.Draw(renderer);

View File

@ -134,7 +134,7 @@ namespace osu.Game.Graphics.UserInterface
lengths.AddRange(Source.bars.InstantaneousLengths);
}
public override void Draw(IRenderer renderer)
protected override void Draw(IRenderer renderer)
{
base.Draw(renderer);

View File

@ -22,7 +22,7 @@ using osuTK.Graphics;
namespace osu.Game.Graphics.UserInterface
{
public partial class OsuDropdown<T> : Dropdown<T>
public partial class OsuDropdown<T> : Dropdown<T>, IKeyBindingHandler<GlobalAction>
{
private const float corner_radius = 5;
@ -30,9 +30,23 @@ namespace osu.Game.Graphics.UserInterface
protected override DropdownMenu CreateMenu() => new OsuDropdownMenu();
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
if (e.Repeat) return false;
if (e.Action == GlobalAction.Back)
return Back();
return false;
}
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
{
}
#region OsuDropdownMenu
protected partial class OsuDropdownMenu : DropdownMenu, IKeyBindingHandler<GlobalAction>
protected partial class OsuDropdownMenu : DropdownMenu
{
public override bool HandleNonPositionalInput => State == MenuState.Open;
@ -276,23 +290,6 @@ namespace osu.Game.Graphics.UserInterface
}
#endregion
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
if (e.Repeat) return false;
if (e.Action == GlobalAction.Back)
{
State = MenuState.Closed;
return true;
}
return false;
}
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
{
}
}
#endregion
@ -355,11 +352,77 @@ namespace osu.Game.Graphics.UserInterface
AddInternal(new HoverClickSounds());
}
[BackgroundDependencyLoader(true)]
private void load(OverlayColourProvider? colourProvider, OsuColour colours)
[Resolved]
private OverlayColourProvider? colourProvider { get; set; }
[Resolved]
private OsuColour colours { get; set; } = null!;
protected override void LoadComplete()
{
BackgroundColour = colourProvider?.Background5 ?? Color4.Black.Opacity(0.5f);
BackgroundColourHover = colourProvider?.Light4 ?? colours.PinkDarker;
base.LoadComplete();
SearchBar.State.ValueChanged += _ => updateColour();
updateColour();
}
protected override bool OnHover(HoverEvent e)
{
updateColour();
return false;
}
protected override void OnHoverLost(HoverLostEvent e)
{
updateColour();
}
private void updateColour()
{
bool hovered = Enabled.Value && IsHovered;
var hoveredColour = colourProvider?.Light4 ?? colours.PinkDarker;
var unhoveredColour = colourProvider?.Background5 ?? Color4.Black.Opacity(0.5f);
if (SearchBar.State.Value == Visibility.Visible)
{
Icon.Colour = hovered ? hoveredColour.Lighten(0.5f) : Colour4.White;
Background.Colour = unhoveredColour;
}
else
{
Icon.Colour = Color4.White;
Background.Colour = hovered ? hoveredColour : unhoveredColour;
}
}
protected override DropdownSearchBar CreateSearchBar() => new OsuDropdownSearchBar
{
Padding = new MarginPadding { Right = 36 },
};
private partial class OsuDropdownSearchBar : DropdownSearchBar
{
protected override void PopIn() => this.FadeIn();
protected override void PopOut() => this.FadeOut();
protected override TextBox CreateTextBox() => new DropdownSearchTextBox
{
FontSize = OsuFont.Default.Size,
};
private partial class DropdownSearchTextBox : SearchTextBox
{
public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
if (e.Action == GlobalAction.Back)
// this method is blocking Dropdown from receiving the back action, despite this text box residing in a separate input manager.
// to fix this properly, a local global action container needs to be added as well, but for simplicity, just don't handle the back action here.
return false;
return base.OnPressed(e);
}
}
}
}
}

View File

@ -23,7 +23,7 @@ namespace osu.Game.Graphics.UserInterface
protected override Drawable GetDrawableCharacter(char c) => new FallingDownContainer
{
AutoSizeAxes = Axes.Both,
Child = new PasswordMaskChar(CalculatedTextSize),
Child = new PasswordMaskChar(FontSize),
};
protected override bool AllowUniqueCharacterSamples => false;

View File

@ -268,7 +268,7 @@ namespace osu.Game.Graphics.UserInterface
protected override Drawable GetDrawableCharacter(char c) => new FallingDownContainer
{
AutoSizeAxes = Axes.Both,
Child = new OsuSpriteText { Text = c.ToString(), Font = OsuFont.GetFont(size: CalculatedTextSize) },
Child = new OsuSpriteText { Text = c.ToString(), Font = OsuFont.GetFont(size: FontSize) },
};
protected override Caret CreateCaret() => caret = new OsuCaret
@ -314,18 +314,16 @@ namespace osu.Game.Graphics.UserInterface
public OsuCaret()
{
RelativeSizeAxes = Axes.Y;
Size = new Vector2(1, 0.9f);
Colour = Color4.Transparent;
Anchor = Anchor.CentreLeft;
Origin = Anchor.CentreLeft;
Masking = true;
CornerRadius = 1;
InternalChild = beatSync = new CaretBeatSyncedContainer
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Masking = true,
CornerRadius = 1f,
RelativeSizeAxes = Axes.Both,
Height = 0.9f,
};
}

View File

@ -221,7 +221,7 @@ namespace osu.Game.Graphics.UserInterface
tierColours.AddRange(Source.tierColours);
}
public override void Draw(IRenderer renderer)
protected override void Draw(IRenderer renderer)
{
base.Draw(renderer);

View File

@ -110,7 +110,7 @@ namespace osu.Game.Graphics.UserInterface
BackgroundFocused = colourProvider.Background4;
BackgroundUnfocused = colourProvider.Background4;
Placeholder.Font = OsuFont.GetFont(size: CalculatedTextSize, weight: FontWeight.SemiBold);
Placeholder.Font = OsuFont.GetFont(size: FontSize, weight: FontWeight.SemiBold);
PlaceholderText = CommonStrings.InputSearch;
CornerRadius = corner_radius;

View File

@ -121,7 +121,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
break;
default:
slider.Current.Parse(textBox.Current.Value);
slider.Current.Parse(textBox.Current.Value, CultureInfo.CurrentCulture);
break;
}
}

View File

@ -57,9 +57,11 @@ namespace osu.Game.Overlays.Settings.Sections
{
skinDropdown = new SkinSettingsDropdown
{
AlwaysShowSearchBar = true,
AllowNonContiguousMatching = true,
LabelText = SkinSettingsStrings.CurrentSkin,
Current = skins.CurrentSkinInfo,
Keywords = new[] { @"skins" }
Keywords = new[] { @"skins" },
},
new SettingsButton
{

View File

@ -16,6 +16,18 @@ namespace osu.Game.Overlays.Settings
{
protected new OsuDropdown<T> Control => (OsuDropdown<T>)base.Control;
public bool AlwaysShowSearchBar
{
get => Control.AlwaysShowSearchBar;
set => Control.AlwaysShowSearchBar = value;
}
public bool AllowNonContiguousMatching
{
get => Control.AllowNonContiguousMatching;
set => Control.AllowNonContiguousMatching = value;
}
public IEnumerable<T> Items
{
get => Control.Items;

View File

@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Configuration
if (setting != null)
{
bindable.Parse(setting.Value);
bindable.Parse(setting.Value, CultureInfo.InvariantCulture);
}
else
{

View File

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using Newtonsoft.Json;
@ -284,7 +285,7 @@ namespace osu.Game.Rulesets.Mods
if (!(target is IParseable parseable))
throw new InvalidOperationException($"Bindable type {target.GetType().ReadableName()} is not {nameof(IParseable)}.");
parseable.Parse(source);
parseable.Parse(source, CultureInfo.InvariantCulture);
}
}

View File

@ -252,7 +252,7 @@ namespace osu.Game.Rulesets.Mods
private IUniformBuffer<FlashlightParameters>? flashlightParametersBuffer;
public override void Draw(IRenderer renderer)
protected override void Draw(IRenderer renderer)
{
base.Draw(renderer);

View File

@ -103,7 +103,7 @@ namespace osu.Game.Screens.Edit.Timing
break;
default:
slider.Current.Parse(t.Text);
slider.Current.Parse(t.Text, CultureInfo.CurrentCulture);
break;
}
}

View File

@ -189,7 +189,7 @@ namespace osu.Game.Screens.Menu
Source.frequencyAmplitudes.AsSpan().CopyTo(audioData);
}
public override void Draw(IRenderer renderer)
protected override void Draw(IRenderer renderer)
{
base.Draw(renderer);

View File

@ -250,7 +250,7 @@ namespace osu.Game.Screens.Select
protected override bool OnHover(HoverEvent e) => true;
private partial class FilterControlTextBox : SeekLimitedSearchTextBox
internal partial class FilterControlTextBox : SeekLimitedSearchTextBox
{
private const float filter_text_size = 12;

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Globalization;
using osu.Framework.Bindables;
using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Graphics;
@ -46,7 +47,7 @@ namespace osu.Game.Skinning
if (!(target is IParseable parseable))
throw new InvalidOperationException($"Bindable type {target.GetType().ReadableName()} is not {nameof(IParseable)}.");
parseable.Parse(source);
parseable.Parse(source, CultureInfo.InvariantCulture);
}
}
}

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
using JetBrains.Annotations;
@ -330,7 +331,7 @@ namespace osu.Game.Skinning
var bindable = new Bindable<TValue>();
if (val != null)
bindable.Parse(val);
bindable.Parse(val, CultureInfo.InvariantCulture);
return bindable;
}
}

View File

@ -36,7 +36,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Realm" Version="11.5.0" />
<PackageReference Include="ppy.osu.Framework" Version="2023.1201.1" />
<PackageReference Include="ppy.osu.Framework" Version="2023.1213.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2023.1127.0" />
<PackageReference Include="Sentry" Version="3.40.0" />
<!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. -->

View File

@ -23,6 +23,6 @@
<RuntimeIdentifier>iossimulator-x64</RuntimeIdentifier>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.iOS" Version="2023.1201.1" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2023.1213.0" />
</ItemGroup>
</Project>