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

Merge branch 'master' into selection-center

This commit is contained in:
Bartłomiej Dach 2024-09-27 09:30:36 +02:00
commit fa90b43233
No known key found for this signature in database
93 changed files with 1729 additions and 520 deletions

View File

@ -66,7 +66,7 @@ namespace osu.Desktop.Updater
{ {
Activated = () => Activated = () =>
{ {
restartToApplyUpdate(); Task.Run(restartToApplyUpdate);
return true; return true;
} }
}); });
@ -88,7 +88,11 @@ namespace osu.Desktop.Updater
{ {
notification = new UpdateProgressNotification notification = new UpdateProgressNotification
{ {
CompletionClickAction = restartToApplyUpdate, CompletionClickAction = () =>
{
Task.Run(restartToApplyUpdate);
return true;
},
}; };
Schedule(() => notificationOverlay.Post(notification)); Schedule(() => notificationOverlay.Post(notification));
@ -127,13 +131,10 @@ namespace osu.Desktop.Updater
return true; return true;
} }
private bool restartToApplyUpdate() private async Task restartToApplyUpdate()
{ {
// TODO: Migrate this to async flow whenever available (see https://github.com/ppy/osu/pull/28743#discussion_r1740505665). await updateManager.WaitExitThenApplyUpdatesAsync(pendingUpdate?.TargetFullRelease).ConfigureAwait(false);
// Currently there's an internal Thread.Sleep(300) which will cause a stutter when the user clicks to restart.
updateManager.WaitExitThenApplyUpdates(pendingUpdate?.TargetFullRelease);
Schedule(() => game.AttemptExit()); Schedule(() => game.AttemptExit());
return true;
} }
} }
} }

View File

@ -26,7 +26,7 @@
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="System.IO.Packaging" Version="8.0.0" /> <PackageReference Include="System.IO.Packaging" Version="8.0.0" />
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" /> <PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
<PackageReference Include="Velopack" Version="0.0.598-g933b2ab" /> <PackageReference Include="Velopack" Version="0.0.630-g9c52e40" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Resources"> <ItemGroup Label="Resources">
<EmbeddedResource Include="lazer.ico" /> <EmbeddedResource Include="lazer.ico" />

View File

@ -71,7 +71,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
contentContainer.Playfield.HitObjectContainer.Add(hitObject); contentContainer.Playfield.HitObjectContainer.Add(hitObject);
} }
protected override SnapResult SnapForBlueprint(PlacementBlueprint blueprint) protected override SnapResult SnapForBlueprint(HitObjectPlacementBlueprint blueprint)
{ {
var result = base.SnapForBlueprint(blueprint); var result = base.SnapForBlueprint(blueprint);
result.Time = Math.Round(HitObjectContainer.TimeAtScreenSpacePosition(result.ScreenSpacePosition) / TIME_SNAP) * TIME_SNAP; result.Time = Math.Round(HitObjectContainer.TimeAtScreenSpacePosition(result.ScreenSpacePosition) / TIME_SNAP) * TIME_SNAP;

View File

@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
{ {
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableBananaShower((BananaShower)hitObject); protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableBananaShower((BananaShower)hitObject);
protected override PlacementBlueprint CreateBlueprint() => new BananaShowerPlacementBlueprint(); protected override HitObjectPlacementBlueprint CreateBlueprint() => new BananaShowerPlacementBlueprint();
protected override void AddHitObject(DrawableHitObject hitObject) protected override void AddHitObject(DrawableHitObject hitObject)
{ {

View File

@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
{ {
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableFruit((Fruit)hitObject); protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableFruit((Fruit)hitObject);
protected override PlacementBlueprint CreateBlueprint() => new FruitPlacementBlueprint(); protected override HitObjectPlacementBlueprint CreateBlueprint() => new FruitPlacementBlueprint();
[Test] [Test]
public void TestFruitPlacementPosition() public void TestFruitPlacementPosition()

View File

@ -114,7 +114,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableJuiceStream((JuiceStream)hitObject); protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableJuiceStream((JuiceStream)hitObject);
protected override PlacementBlueprint CreateBlueprint() => new JuiceStreamPlacementBlueprint(); protected override HitObjectPlacementBlueprint CreateBlueprint() => new JuiceStreamPlacementBlueprint();
private void addMoveAndClickSteps(double time, float position, bool end = false) private void addMoveAndClickSteps(double time, float position, bool end = false)
{ {

View File

@ -3,7 +3,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Beatmaps; using osu.Game.Rulesets.Catch.Beatmaps;
using osu.Game.Rulesets.Catch.Difficulty.Preprocessing; using osu.Game.Rulesets.Catch.Difficulty.Preprocessing;
@ -44,7 +43,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
StarRating = Math.Sqrt(skills[0].DifficultyValue()) * difficulty_multiplier, StarRating = Math.Sqrt(skills[0].DifficultyValue()) * difficulty_multiplier,
Mods = mods, Mods = mods,
ApproachRate = preempt > 1200.0 ? -(preempt - 1800.0) / 120.0 : -(preempt - 1200.0) / 150.0 + 5.0, ApproachRate = preempt > 1200.0 ? -(preempt - 1800.0) / 120.0 : -(preempt - 1200.0) / 150.0 + 5.0,
MaxCombo = beatmap.HitObjects.Count(h => h is Fruit) + beatmap.HitObjects.OfType<JuiceStream>().SelectMany(j => j.NestedHitObjects).Count(h => !(h is TinyDroplet)), MaxCombo = beatmap.GetMaxCombo(),
}; };
return attributes; return attributes;

View File

@ -10,7 +10,7 @@ using osu.Game.Rulesets.Edit.Tools;
namespace osu.Game.Rulesets.Catch.Edit namespace osu.Game.Rulesets.Catch.Edit
{ {
public class BananaShowerCompositionTool : HitObjectCompositionTool public class BananaShowerCompositionTool : CompositionTool
{ {
public BananaShowerCompositionTool() public BananaShowerCompositionTool()
: base(nameof(BananaShower)) : base(nameof(BananaShower))
@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Catch.Edit
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners); public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners);
public override PlacementBlueprint CreatePlacementBlueprint() => new BananaShowerPlacementBlueprint(); public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new BananaShowerPlacementBlueprint();
} }
} }

View File

@ -9,7 +9,7 @@ using osu.Game.Rulesets.UI.Scrolling;
namespace osu.Game.Rulesets.Catch.Edit.Blueprints namespace osu.Game.Rulesets.Catch.Edit.Blueprints
{ {
public partial class CatchPlacementBlueprint<THitObject> : PlacementBlueprint public partial class CatchPlacementBlueprint<THitObject> : HitObjectPlacementBlueprint
where THitObject : CatchHitObject, new() where THitObject : CatchHitObject, new()
{ {
protected new THitObject HitObject => (THitObject)base.HitObject; protected new THitObject HitObject => (THitObject)base.HitObject;

View File

@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Catch.Edit
protected override BeatSnapGrid CreateBeatSnapGrid() => new CatchBeatSnapGrid(); protected override BeatSnapGrid CreateBeatSnapGrid() => new CatchBeatSnapGrid();
protected override IReadOnlyList<HitObjectCompositionTool> CompositionTools => new HitObjectCompositionTool[] protected override IReadOnlyList<CompositionTool> CompositionTools => new CompositionTool[]
{ {
new FruitCompositionTool(), new FruitCompositionTool(),
new JuiceStreamCompositionTool(), new JuiceStreamCompositionTool(),

View File

@ -10,7 +10,7 @@ using osu.Game.Rulesets.Edit.Tools;
namespace osu.Game.Rulesets.Catch.Edit namespace osu.Game.Rulesets.Catch.Edit
{ {
public class FruitCompositionTool : HitObjectCompositionTool public class FruitCompositionTool : CompositionTool
{ {
public FruitCompositionTool() public FruitCompositionTool()
: base(nameof(Fruit)) : base(nameof(Fruit))
@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Catch.Edit
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles); public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles);
public override PlacementBlueprint CreatePlacementBlueprint() => new FruitPlacementBlueprint(); public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new FruitPlacementBlueprint();
} }
} }

View File

@ -10,7 +10,7 @@ using osu.Game.Rulesets.Edit.Tools;
namespace osu.Game.Rulesets.Catch.Edit namespace osu.Game.Rulesets.Catch.Edit
{ {
public class JuiceStreamCompositionTool : HitObjectCompositionTool public class JuiceStreamCompositionTool : CompositionTool
{ {
public JuiceStreamCompositionTool() public JuiceStreamCompositionTool()
: base(nameof(JuiceStream)) : base(nameof(JuiceStream))
@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Catch.Edit
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders); public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders);
public override PlacementBlueprint CreatePlacementBlueprint() => new JuiceStreamPlacementBlueprint(); public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new JuiceStreamPlacementBlueprint();
} }
} }

View File

@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
}); });
} }
protected override SnapResult SnapForBlueprint(PlacementBlueprint blueprint) protected override SnapResult SnapForBlueprint(HitObjectPlacementBlueprint blueprint)
{ {
double time = column.TimeAtScreenSpacePosition(InputManager.CurrentState.Mouse.Position); double time = column.TimeAtScreenSpacePosition(InputManager.CurrentState.Mouse.Position);
var pos = column.ScreenSpacePositionAtTime(time); var pos = column.ScreenSpacePositionAtTime(time);

View File

@ -13,6 +13,6 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
public partial class TestSceneHoldNotePlacementBlueprint : ManiaPlacementBlueprintTestScene public partial class TestSceneHoldNotePlacementBlueprint : ManiaPlacementBlueprintTestScene
{ {
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableHoldNote((HoldNote)hitObject); protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableHoldNote((HoldNote)hitObject);
protected override PlacementBlueprint CreateBlueprint() => new HoldNotePlacementBlueprint(); protected override HitObjectPlacementBlueprint CreateBlueprint() => new HoldNotePlacementBlueprint();
} }
} }

View File

@ -64,6 +64,6 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
private Note getNote() => this.ChildrenOfType<DrawableNote>().FirstOrDefault()?.HitObject; private Note getNote() => this.ChildrenOfType<DrawableNote>().FirstOrDefault()?.HitObject;
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableNote((Note)hitObject); protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableNote((Note)hitObject);
protected override PlacementBlueprint CreateBlueprint() => new NotePlacementBlueprint(); protected override HitObjectPlacementBlueprint CreateBlueprint() => new NotePlacementBlueprint();
} }
} }

View File

@ -15,7 +15,7 @@ using osuTK.Input;
namespace osu.Game.Rulesets.Mania.Edit.Blueprints namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{ {
public abstract partial class ManiaPlacementBlueprint<T> : PlacementBlueprint public abstract partial class ManiaPlacementBlueprint<T> : HitObjectPlacementBlueprint
where T : ManiaHitObject where T : ManiaHitObject
{ {
protected new T HitObject => (T)base.HitObject; protected new T HitObject => (T)base.HitObject;

View File

@ -9,7 +9,7 @@ using osu.Game.Rulesets.Mania.Edit.Blueprints;
namespace osu.Game.Rulesets.Mania.Edit namespace osu.Game.Rulesets.Mania.Edit
{ {
public class HoldNoteCompositionTool : HitObjectCompositionTool public class HoldNoteCompositionTool : CompositionTool
{ {
public HoldNoteCompositionTool() public HoldNoteCompositionTool()
: base("Hold") : base("Hold")
@ -18,6 +18,6 @@ namespace osu.Game.Rulesets.Mania.Edit
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders); public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders);
public override PlacementBlueprint CreatePlacementBlueprint() => new HoldNotePlacementBlueprint(); public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new HoldNotePlacementBlueprint();
} }
} }

View File

@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Mania.Edit
protected override BeatSnapGrid CreateBeatSnapGrid() => new ManiaBeatSnapGrid(); protected override BeatSnapGrid CreateBeatSnapGrid() => new ManiaBeatSnapGrid();
protected override IReadOnlyList<HitObjectCompositionTool> CompositionTools => new HitObjectCompositionTool[] protected override IReadOnlyList<CompositionTool> CompositionTools => new CompositionTool[]
{ {
new NoteCompositionTool(), new NoteCompositionTool(),
new HoldNoteCompositionTool() new HoldNoteCompositionTool()

View File

@ -10,7 +10,7 @@ using osu.Game.Rulesets.Mania.Objects;
namespace osu.Game.Rulesets.Mania.Edit namespace osu.Game.Rulesets.Mania.Edit
{ {
public class NoteCompositionTool : HitObjectCompositionTool public class NoteCompositionTool : CompositionTool
{ {
public NoteCompositionTool() public NoteCompositionTool()
: base(nameof(Note)) : base(nameof(Note))
@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Mania.Edit
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles); public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles);
public override PlacementBlueprint CreatePlacementBlueprint() => new NotePlacementBlueprint(); public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new NotePlacementBlueprint();
} }
} }

View File

@ -14,6 +14,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
public partial class TestSceneHitCirclePlacementBlueprint : PlacementBlueprintTestScene public partial class TestSceneHitCirclePlacementBlueprint : PlacementBlueprintTestScene
{ {
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableHitCircle((HitCircle)hitObject); protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableHitCircle((HitCircle)hitObject);
protected override PlacementBlueprint CreateBlueprint() => new HitCirclePlacementBlueprint(); protected override HitObjectPlacementBlueprint CreateBlueprint() => new HitCirclePlacementBlueprint();
} }
} }

View File

@ -514,6 +514,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
private Slider? getSlider() => HitObjectContainer.Count > 0 ? ((DrawableSlider)HitObjectContainer[0]).HitObject : null; private Slider? getSlider() => HitObjectContainer.Count > 0 ? ((DrawableSlider)HitObjectContainer[0]).HitObject : null;
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableSlider((Slider)hitObject); protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableSlider((Slider)hitObject);
protected override PlacementBlueprint CreateBlueprint() => new SliderPlacementBlueprint(); protected override HitObjectPlacementBlueprint CreateBlueprint() => new SliderPlacementBlueprint();
} }
} }

View File

@ -15,6 +15,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
{ {
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableSpinner((Spinner)hitObject); protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableSpinner((Spinner)hitObject);
protected override PlacementBlueprint CreateBlueprint() => new SpinnerPlacementBlueprint(); protected override HitObjectPlacementBlueprint CreateBlueprint() => new SpinnerPlacementBlueprint();
} }
} }

View File

@ -81,7 +81,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate; double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
double drainRate = beatmap.Difficulty.DrainRate; double drainRate = beatmap.Difficulty.DrainRate;
int maxCombo = beatmap.GetMaxCombo();
int hitCirclesCount = beatmap.HitObjects.Count(h => h is HitCircle); int hitCirclesCount = beatmap.HitObjects.Count(h => h is HitCircle);
int sliderCount = beatmap.HitObjects.Count(h => h is Slider); int sliderCount = beatmap.HitObjects.Count(h => h is Slider);
@ -104,7 +103,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5, ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5,
OverallDifficulty = (80 - hitWindowGreat) / 6, OverallDifficulty = (80 - hitWindowGreat) / 6,
DrainRate = drainRate, DrainRate = drainRate,
MaxCombo = maxCombo, MaxCombo = beatmap.GetMaxCombo(),
HitCircleCount = hitCirclesCount, HitCircleCount = hitCirclesCount,
SliderCount = sliderCount, SliderCount = sliderCount,
SpinnerCount = spinnerCount, SpinnerCount = spinnerCount,

View File

@ -9,7 +9,7 @@ using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
{ {
public partial class HitCirclePlacementBlueprint : PlacementBlueprint public partial class HitCirclePlacementBlueprint : HitObjectPlacementBlueprint
{ {
public new HitCircle HitObject => (HitCircle)base.HitObject; public new HitCircle HitObject => (HitCircle)base.HitObject;

View File

@ -21,7 +21,7 @@ using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{ {
public partial class SliderPlacementBlueprint : PlacementBlueprint public partial class SliderPlacementBlueprint : HitObjectPlacementBlueprint
{ {
public new Slider HitObject => (Slider)base.HitObject; public new Slider HitObject => (Slider)base.HitObject;

View File

@ -13,7 +13,7 @@ using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners
{ {
public partial class SpinnerPlacementBlueprint : PlacementBlueprint public partial class SpinnerPlacementBlueprint : HitObjectPlacementBlueprint
{ {
public new Spinner HitObject => (Spinner)base.HitObject; public new Spinner HitObject => (Spinner)base.HitObject;

View File

@ -10,7 +10,7 @@ using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Rulesets.Osu.Edit namespace osu.Game.Rulesets.Osu.Edit
{ {
public class HitCircleCompositionTool : HitObjectCompositionTool public class HitCircleCompositionTool : CompositionTool
{ {
public HitCircleCompositionTool() public HitCircleCompositionTool()
: base(nameof(HitCircle)) : base(nameof(HitCircle))
@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Osu.Edit
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles); public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles);
public override PlacementBlueprint CreatePlacementBlueprint() => new HitCirclePlacementBlueprint(); public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new HitCirclePlacementBlueprint();
} }
} }

View File

@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.Edit
protected override DrawableRuleset<OsuHitObject> CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods) protected override DrawableRuleset<OsuHitObject> CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods)
=> new DrawableOsuEditorRuleset(ruleset, beatmap, mods); => new DrawableOsuEditorRuleset(ruleset, beatmap, mods);
protected override IReadOnlyList<HitObjectCompositionTool> CompositionTools => new HitObjectCompositionTool[] protected override IReadOnlyList<CompositionTool> CompositionTools => new CompositionTool[]
{ {
new HitCircleCompositionTool(), new HitCircleCompositionTool(),
new SliderCompositionTool(), new SliderCompositionTool(),

View File

@ -105,9 +105,7 @@ namespace osu.Game.Rulesets.Osu.Edit
// is not looking to change the duration of the slider but expand the whole pattern. // is not looking to change the duration of the slider but expand the whole pattern.
if (objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider) if (objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider)
{ {
var originalInfo = objectsInScale[slider]; scaleSlider(slider, scale, actualOrigin, objectsInScale[slider], axisRotation);
Debug.Assert(originalInfo.PathControlPointPositions != null && originalInfo.PathControlPointTypes != null);
scaleSlider(slider, scale, originalInfo.PathControlPointPositions, originalInfo.PathControlPointTypes, axisRotation);
} }
else else
{ {
@ -159,21 +157,25 @@ namespace osu.Game.Rulesets.Osu.Edit
return scale; return scale;
} }
private void scaleSlider(Slider slider, Vector2 scale, Vector2[] originalPathPositions, PathType?[] originalPathTypes, float axisRotation = 0) private void scaleSlider(Slider slider, Vector2 scale, Vector2 origin, OriginalHitObjectState originalInfo, float axisRotation = 0)
{ {
Debug.Assert(originalInfo.PathControlPointPositions != null && originalInfo.PathControlPointTypes != null);
scale = Vector2.ComponentMax(scale, new Vector2(Precision.FLOAT_EPSILON)); scale = Vector2.ComponentMax(scale, new Vector2(Precision.FLOAT_EPSILON));
// Maintain the path types in case they were defaulted to bezier at some point during scaling // Maintain the path types in case they were defaulted to bezier at some point during scaling
for (int i = 0; i < slider.Path.ControlPoints.Count; i++) for (int i = 0; i < slider.Path.ControlPoints.Count; i++)
{ {
slider.Path.ControlPoints[i].Position = GeometryUtils.GetScaledPosition(scale, Vector2.Zero, originalPathPositions[i], axisRotation); slider.Path.ControlPoints[i].Position = GeometryUtils.GetScaledPosition(scale, Vector2.Zero, originalInfo.PathControlPointPositions[i], axisRotation);
slider.Path.ControlPoints[i].Type = originalPathTypes[i]; slider.Path.ControlPoints[i].Type = originalInfo.PathControlPointTypes[i];
} }
// Snap the slider's length to the current beat divisor // Snap the slider's length to the current beat divisor
// to calculate the final resulting duration / bounding box before the final checks. // to calculate the final resulting duration / bounding box before the final checks.
slider.SnapTo(snapProvider); slider.SnapTo(snapProvider);
slider.Position = GeometryUtils.GetScaledPosition(scale, origin, originalInfo.Position, axisRotation);
//if sliderhead or sliderend end up outside playfield, revert scaling. //if sliderhead or sliderend end up outside playfield, revert scaling.
Quad scaledQuad = GeometryUtils.GetSurroundingQuad(new OsuHitObject[] { slider }); Quad scaledQuad = GeometryUtils.GetSurroundingQuad(new OsuHitObject[] { slider });
(bool xInBounds, bool yInBounds) = isQuadInBounds(scaledQuad); (bool xInBounds, bool yInBounds) = isQuadInBounds(scaledQuad);
@ -182,7 +184,9 @@ namespace osu.Game.Rulesets.Osu.Edit
return; return;
for (int i = 0; i < slider.Path.ControlPoints.Count; i++) for (int i = 0; i < slider.Path.ControlPoints.Count; i++)
slider.Path.ControlPoints[i].Position = originalPathPositions[i]; slider.Path.ControlPoints[i].Position = originalInfo.PathControlPointPositions[i];
slider.Position = originalInfo.Position;
// Snap the slider's length again to undo the potentially-invalid length applied by the previous snap. // Snap the slider's length again to undo the potentially-invalid length applied by the previous snap.
slider.SnapTo(snapProvider); slider.SnapTo(snapProvider);

View File

@ -10,7 +10,10 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Osu.UI;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Components.RadioButtons; using osu.Game.Screens.Edit.Components.RadioButtons;
using osuTK; using osuTK;
@ -35,6 +38,8 @@ namespace osu.Game.Rulesets.Osu.Edit
private OsuCheckbox xCheckBox = null!; private OsuCheckbox xCheckBox = null!;
private OsuCheckbox yCheckBox = null!; private OsuCheckbox yCheckBox = null!;
private BindableList<HitObject> selectedItems { get; } = new BindableList<HitObject>();
public PreciseScalePopover(OsuSelectionScaleHandler scaleHandler, OsuGridToolboxGroup gridToolbox) public PreciseScalePopover(OsuSelectionScaleHandler scaleHandler, OsuGridToolboxGroup gridToolbox)
{ {
this.scaleHandler = scaleHandler; this.scaleHandler = scaleHandler;
@ -44,8 +49,10 @@ namespace osu.Game.Rulesets.Osu.Edit
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load(EditorBeatmap editorBeatmap)
{ {
selectedItems.BindTo(editorBeatmap.SelectedHitObjects);
Child = new FillFlowContainer Child = new FillFlowContainer
{ {
Width = 220, Width = 220,
@ -191,14 +198,26 @@ namespace osu.Game.Rulesets.Osu.Edit
updateAxisCheckBoxesEnabled(); updateAxisCheckBoxesEnabled();
} }
private Vector2? getOriginPosition(PreciseScaleInfo scale) => private Vector2? getOriginPosition(PreciseScaleInfo scale)
scale.Origin switch {
switch (scale.Origin)
{ {
ScaleOrigin.GridCentre => gridToolbox.StartPosition.Value, case ScaleOrigin.GridCentre:
ScaleOrigin.PlayfieldCentre => OsuPlayfield.BASE_SIZE / 2, return gridToolbox.StartPosition.Value;
ScaleOrigin.SelectionCentre => null,
_ => throw new ArgumentOutOfRangeException(nameof(scale)) case ScaleOrigin.PlayfieldCentre:
}; return OsuPlayfield.BASE_SIZE / 2;
case ScaleOrigin.SelectionCentre:
if (selectedItems.Count == 1 && selectedItems.First() is Slider slider)
return slider.Position;
return null;
default:
throw new ArgumentOutOfRangeException(nameof(scale));
}
}
private Axes getAdjustAxis(PreciseScaleInfo scale) => scale.XAxis ? scale.YAxis ? Axes.Both : Axes.X : Axes.Y; private Axes getAdjustAxis(PreciseScaleInfo scale) => scale.XAxis ? scale.YAxis ? Axes.Both : Axes.X : Axes.Y;

View File

@ -10,7 +10,7 @@ using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Rulesets.Osu.Edit namespace osu.Game.Rulesets.Osu.Edit
{ {
public class SliderCompositionTool : HitObjectCompositionTool public class SliderCompositionTool : CompositionTool
{ {
public SliderCompositionTool() public SliderCompositionTool()
: base(nameof(Slider)) : base(nameof(Slider))
@ -26,6 +26,6 @@ namespace osu.Game.Rulesets.Osu.Edit
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders); public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders);
public override PlacementBlueprint CreatePlacementBlueprint() => new SliderPlacementBlueprint(); public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new SliderPlacementBlueprint();
} }
} }

View File

@ -10,7 +10,7 @@ using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Rulesets.Osu.Edit namespace osu.Game.Rulesets.Osu.Edit
{ {
public class SpinnerCompositionTool : HitObjectCompositionTool public class SpinnerCompositionTool : CompositionTool
{ {
public SpinnerCompositionTool() public SpinnerCompositionTool()
: base(nameof(Spinner)) : base(nameof(Spinner))
@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Osu.Edit
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners); public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners);
public override PlacementBlueprint CreatePlacementBlueprint() => new SpinnerPlacementBlueprint(); public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new SpinnerPlacementBlueprint();
} }
} }

View File

@ -91,20 +91,35 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
drawableObjectPiece.ApplyCustomUpdateState -= applyDimToDrawableHitObject; drawableObjectPiece.ApplyCustomUpdateState -= applyDimToDrawableHitObject;
drawableObjectPiece.ApplyCustomUpdateState += applyDimToDrawableHitObject; drawableObjectPiece.ApplyCustomUpdateState += applyDimToDrawableHitObject;
} }
else
applyDim(piece);
}
void applyDim(Drawable piece) // but at the end apply the transforms now regardless of whether this is a DHO or not.
{ // the above is just to ensure they don't get overwritten later.
piece.FadeColour(new Color4(195, 195, 195, 255)); applyDim(piece);
using (piece.BeginDelayedSequence(InitialLifetimeOffset - OsuHitWindows.MISS_WINDOW))
piece.FadeColour(Color4.White, 100);
} }
void applyDimToDrawableHitObject(DrawableHitObject dho, ArmedState _) => applyDim(dho);
} }
protected override void ClearNestedHitObjects()
{
base.ClearNestedHitObjects();
// any dimmable pieces that are DHOs will be pooled separately.
// `applyDimToDrawableHitObject` is a closure that implicitly captures `this`,
// and because of separate pooling of parent and child objects, there is no guarantee that the pieces will be associated with `this` again on re-use.
// therefore, clean up the subscription here to avoid crosstalk.
// not doing so can result in the callback attempting to read things from `this` when it is in a completely bogus state (not in use or similar).
foreach (var piece in DimmablePieces.OfType<DrawableHitObject>())
piece.ApplyCustomUpdateState -= applyDimToDrawableHitObject;
}
private void applyDim(Drawable piece)
{
piece.FadeColour(new Color4(195, 195, 195, 255));
using (piece.BeginDelayedSequence(InitialLifetimeOffset - OsuHitWindows.MISS_WINDOW))
piece.FadeColour(Color4.White, 100);
}
private void applyDimToDrawableHitObject(DrawableHitObject dho, ArmedState _) => applyDim(dho);
protected sealed override double InitialLifetimeOffset => HitObject.TimePreempt; protected sealed override double InitialLifetimeOffset => HitObject.TimePreempt;
private OsuInputManager osuActionInputManager; private OsuInputManager osuActionInputManager;

View File

@ -14,7 +14,6 @@ using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour;
using osu.Game.Rulesets.Taiko.Difficulty.Skills; using osu.Game.Rulesets.Taiko.Difficulty.Skills;
using osu.Game.Rulesets.Taiko.Mods; using osu.Game.Rulesets.Taiko.Mods;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Scoring; using osu.Game.Rulesets.Taiko.Scoring;
namespace osu.Game.Rulesets.Taiko.Difficulty namespace osu.Game.Rulesets.Taiko.Difficulty
@ -100,7 +99,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
ColourDifficulty = colourRating, ColourDifficulty = colourRating,
PeakDifficulty = combinedRating, PeakDifficulty = combinedRating,
GreatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate, GreatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate,
MaxCombo = beatmap.HitObjects.Count(h => h is Hit), MaxCombo = beatmap.GetMaxCombo(),
}; };
return attributes; return attributes;

View File

@ -10,7 +10,7 @@ using osuTK.Input;
namespace osu.Game.Rulesets.Taiko.Edit.Blueprints namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
{ {
public partial class HitPlacementBlueprint : PlacementBlueprint public partial class HitPlacementBlueprint : HitObjectPlacementBlueprint
{ {
private readonly HitPiece piece; private readonly HitPiece piece;

View File

@ -17,7 +17,7 @@ using osuTK.Input;
namespace osu.Game.Rulesets.Taiko.Edit.Blueprints namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
{ {
public partial class TaikoSpanPlacementBlueprint : PlacementBlueprint public partial class TaikoSpanPlacementBlueprint : HitObjectPlacementBlueprint
{ {
private readonly HitPiece headPiece; private readonly HitPiece headPiece;
private readonly HitPiece tailPiece; private readonly HitPiece tailPiece;

View File

@ -10,7 +10,7 @@ using osu.Game.Rulesets.Taiko.Objects;
namespace osu.Game.Rulesets.Taiko.Edit namespace osu.Game.Rulesets.Taiko.Edit
{ {
public class DrumRollCompositionTool : HitObjectCompositionTool public class DrumRollCompositionTool : CompositionTool
{ {
public DrumRollCompositionTool() public DrumRollCompositionTool()
: base(nameof(DrumRoll)) : base(nameof(DrumRoll))
@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Taiko.Edit
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders); public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders);
public override PlacementBlueprint CreatePlacementBlueprint() => new DrumRollPlacementBlueprint(); public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new DrumRollPlacementBlueprint();
} }
} }

View File

@ -10,7 +10,7 @@ using osu.Game.Rulesets.Taiko.Objects;
namespace osu.Game.Rulesets.Taiko.Edit namespace osu.Game.Rulesets.Taiko.Edit
{ {
public class HitCompositionTool : HitObjectCompositionTool public class HitCompositionTool : CompositionTool
{ {
public HitCompositionTool() public HitCompositionTool()
: base(nameof(Hit)) : base(nameof(Hit))
@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Taiko.Edit
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles); public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles);
public override PlacementBlueprint CreatePlacementBlueprint() => new HitPlacementBlueprint(); public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new HitPlacementBlueprint();
} }
} }

View File

@ -10,7 +10,7 @@ using osu.Game.Rulesets.Taiko.Objects;
namespace osu.Game.Rulesets.Taiko.Edit namespace osu.Game.Rulesets.Taiko.Edit
{ {
public class SwellCompositionTool : HitObjectCompositionTool public class SwellCompositionTool : CompositionTool
{ {
public SwellCompositionTool() public SwellCompositionTool()
: base(nameof(Swell)) : base(nameof(Swell))
@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Taiko.Edit
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners); public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners);
public override PlacementBlueprint CreatePlacementBlueprint() => new SwellPlacementBlueprint(); public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new SwellPlacementBlueprint();
} }
} }

View File

@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Taiko.Edit
{ {
} }
protected override IReadOnlyList<HitObjectCompositionTool> CompositionTools => new HitObjectCompositionTool[] protected override IReadOnlyList<CompositionTool> CompositionTools => new CompositionTool[]
{ {
new HitCompositionTool(), new HitCompositionTool(),
new DrumRollCompositionTool(), new DrumRollCompositionTool(),

View File

@ -0,0 +1,52 @@
// 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.Bindables;
using osu.Game.Utils;
namespace osu.Game.Tests.Utils
{
[TestFixture]
public class BindableValueAccessorTest
{
[Test]
public void GetValue()
{
const int value = 1337;
BindableInt bindable = new BindableInt(value);
Assert.That(BindableValueAccessor.GetValue(bindable), Is.EqualTo(value));
}
[Test]
public void SetValue()
{
const int value = 1337;
BindableInt bindable = new BindableInt();
BindableValueAccessor.SetValue(bindable, value);
Assert.That(bindable.Value, Is.EqualTo(value));
}
[Test]
public void GetInvalidBindable()
{
BindableList<object> list = new BindableList<object>();
Assert.That(BindableValueAccessor.GetValue(list), Is.EqualTo(list));
}
[Test]
public void SetInvalidBindable()
{
const int value = 1337;
BindableList<int> list = new BindableList<int> { value };
BindableValueAccessor.SetValue(list, 2);
Assert.That(list, Has.Exactly(1).Items);
Assert.That(list[0], Is.EqualTo(value));
}
}
}

View File

@ -5,14 +5,18 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Threading; using System.Threading;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online; using osu.Game.Online;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty;
@ -23,6 +27,7 @@ using osu.Game.Screens.Ranking.Statistics;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Screens.Ranking.Statistics.User;
using osu.Game.Tests.Resources; using osu.Game.Tests.Resources;
using osu.Game.Users; using osu.Game.Users;
using osuTK; using osuTK;
@ -80,6 +85,69 @@ namespace osu.Game.Tests.Visual.Ranking
loadPanel(null); loadPanel(null);
} }
[Test]
public void TestStatisticsShownCorrectlyIfUpdateDeliveredBeforeLoad()
{
UserStatisticsWatcher userStatisticsWatcher = null!;
ScoreInfo score = null!;
AddStep("create user statistics watcher", () => Add(userStatisticsWatcher = new UserStatisticsWatcher()));
AddStep("set user statistics update", () =>
{
score = TestResources.CreateTestScoreInfo();
score.OnlineID = 1234;
((Bindable<UserStatisticsUpdate>)userStatisticsWatcher.LatestUpdate).Value = new UserStatisticsUpdate(score,
new UserStatistics
{
Level = new UserStatistics.LevelInfo
{
Current = 5,
Progress = 20,
},
GlobalRank = 38000,
CountryRank = 12006,
PP = 2134,
RankedScore = 21123849,
Accuracy = 0.985,
PlayCount = 13375,
PlayTime = 354490,
TotalScore = 128749597,
TotalHits = 0,
MaxCombo = 1233,
}, new UserStatistics
{
Level = new UserStatistics.LevelInfo
{
Current = 5,
Progress = 30,
},
GlobalRank = 36000,
CountryRank = 12000,
PP = (decimal)2134.5,
RankedScore = 23897015,
Accuracy = 0.984,
PlayCount = 13376,
PlayTime = 35789,
TotalScore = 132218497,
TotalHits = 0,
MaxCombo = 1233,
});
});
AddStep("load user statistics panel", () => Child = new DependencyProvidingContainer
{
CachedDependencies = [(typeof(UserStatisticsWatcher), userStatisticsWatcher)],
RelativeSizeAxes = Axes.Both,
Child = new UserStatisticsPanel(score)
{
RelativeSizeAxes = Axes.Both,
State = { Value = Visibility.Visible },
Score = { Value = score, }
}
});
AddUntilStep("overall ranking present", () => this.ChildrenOfType<OverallRanking>().Any());
AddUntilStep("loading spinner not visible", () => this.ChildrenOfType<LoadingLayer>().All(l => l.State.Value == Visibility.Hidden));
}
private void loadPanel(ScoreInfo score) => AddStep("load panel", () => private void loadPanel(ScoreInfo score) => AddStep("load panel", () =>
{ {
Child = new UserStatisticsPanel(score) Child = new UserStatisticsPanel(score)

View File

@ -9,6 +9,11 @@ namespace osu.Game.Tests.Visual.Settings
{ {
public partial class TestSceneDirectorySelector : ThemeComparisonTestScene public partial class TestSceneDirectorySelector : ThemeComparisonTestScene
{ {
public TestSceneDirectorySelector()
: base(false)
{
}
protected override Drawable CreateContent() => new OsuDirectorySelector protected override Drawable CreateContent() => new OsuDirectorySelector
{ {
RelativeSizeAxes = Axes.Both RelativeSizeAxes = Axes.Both

View File

@ -1,37 +1,49 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays;
using osu.Game.Tests.Visual.UserInterface; using osu.Game.Tests.Visual.UserInterface;
namespace osu.Game.Tests.Visual.Settings namespace osu.Game.Tests.Visual.Settings
{ {
public partial class TestSceneFileSelector : ThemeComparisonTestScene public partial class TestSceneFileSelector : ThemeComparisonTestScene
{ {
[Resolved] public TestSceneFileSelector()
private OsuColour colours { get; set; } = null!; : base(false)
{
}
[Test] [Test]
public void TestJpgFilesOnly() public void TestJpgFilesOnly()
{ {
AddStep("create", () => AddStep("create", () =>
{ {
ContentContainer.Children = new Drawable[] var colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
ContentContainer.Child = new DependencyProvidingContainer
{ {
new Box RelativeSizeAxes = Axes.Both,
CachedDependencies = new (Type, object)[]
{ {
RelativeSizeAxes = Axes.Both, (typeof(OverlayColourProvider), colourProvider)
Colour = colours.GreySeaFoam
}, },
new OsuFileSelector(validFileExtensions: new[] { ".jpg" }) Children = new Drawable[]
{ {
RelativeSizeAxes = Axes.Both, new Box
}, {
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background3
},
new OsuFileSelector(validFileExtensions: new[] { ".jpg" })
{
RelativeSizeAxes = Axes.Both,
},
}
}; };
}); });
} }

View File

@ -5,6 +5,7 @@ using NUnit.Framework;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings;
@ -19,16 +20,20 @@ namespace osu.Game.Tests.Visual.Settings
{ {
Children = new Drawable[] Children = new Drawable[]
{ {
new FillFlowContainer new PopoverContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Direction = FillDirection.Vertical, Child = new FillFlowContainer
Spacing = new Vector2(20), {
Width = 0.5f, RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre, Direction = FillDirection.Vertical,
Origin = Anchor.Centre, Spacing = new Vector2(20),
Padding = new MarginPadding(50), Width = 0.5f,
ChildrenEnumerable = new TestTargetClass().CreateSettingsControls() Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Padding = new MarginPadding(50),
ChildrenEnumerable = new TestTargetClass().CreateSettingsControls()
},
}, },
}; };
} }
@ -66,6 +71,13 @@ namespace osu.Game.Tests.Visual.Settings
[SettingSource("Sample number textbox", "Textbox number entry", SettingControlType = typeof(SettingsNumberBox))] [SettingSource("Sample number textbox", "Textbox number entry", SettingControlType = typeof(SettingsNumberBox))]
public Bindable<int?> IntTextBoxBindable { get; } = new Bindable<int?>(); public Bindable<int?> IntTextBoxBindable { get; } = new Bindable<int?>();
[SettingSource("Sample colour", "Change the colour", SettingControlType = typeof(SettingsColour))]
public BindableColour4 ColourBindable { get; } = new BindableColour4
{
Default = Colour4.White,
Value = Colour4.Red
};
} }
private enum TestEnum private enum TestEnum

View File

@ -5,6 +5,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Cursor;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Cursor; using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Localisation; using osu.Game.Localisation;
@ -94,6 +95,16 @@ namespace osu.Game.Tests.Visual.UserInterface
Instantaneous = false, Instantaneous = false,
TabbableContentContainer = this, TabbableContentContainer = this,
}, },
new FormEnumDropdown<CountdownType>
{
Caption = EditorSetupStrings.EnableCountdown,
HintText = EditorSetupStrings.CountdownDescription,
},
new FormFileSelector
{
Caption = "Audio file",
PlaceholderText = "Select an audio file",
},
}, },
}, },
} }

View File

@ -0,0 +1,75 @@
// 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.Cursor;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays.Settings;
using osuTK.Graphics;
using osuTK.Input;
namespace osu.Game.Tests.Visual.UserInterface
{
public partial class TestSceneSettingsColour : OsuManualInputManagerTestScene
{
private SettingsColour? component;
[Test]
public void TestColour()
{
createContent();
AddRepeatStep("set random colour", () => component!.Current.Value = randomColour(), 4);
}
[Test]
public void TestUserInteractions()
{
createContent();
AddStep("click colour", () =>
{
InputManager.MoveMouseTo(component!);
InputManager.Click(MouseButton.Left);
});
AddAssert("colour picker spawned", () => this.ChildrenOfType<OsuColourPicker>().Any());
}
private void createContent()
{
AddStep("create component", () =>
{
Child = new PopoverContainer
{
RelativeSizeAxes = Axes.Both,
Child = new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 500,
AutoSizeAxes = Axes.Y,
Children = new Drawable[]
{
component = new SettingsColour
{
LabelText = "a sample component",
},
},
},
};
});
}
private Colour4 randomColour() => new Color4(
RNG.NextSingle(),
RNG.NextSingle(),
RNG.NextSingle(),
1);
}
}

View File

@ -27,6 +27,9 @@ namespace osu.Game.Tournament.Screens.Setup
[Resolved] [Resolved]
private MatchIPCInfo ipc { get; set; } = null!; private MatchIPCInfo ipc { get; set; } = null!;
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
private OsuDirectorySelector directorySelector = null!; private OsuDirectorySelector directorySelector = null!;
private DialogOverlay? overlay; private DialogOverlay? overlay;

View File

@ -198,8 +198,11 @@ namespace osu.Game.Beatmaps
if (beatmapSet.OnlineID > 0) if (beatmapSet.OnlineID > 0)
{ {
// Required local for iOS. Will cause runtime crash if inlined.
int onlineId = beatmapSet.OnlineID;
// OnlineID should really be unique, but to avoid catastrophic failure let's iterate just to be sure. // OnlineID should really be unique, but to avoid catastrophic failure let's iterate just to be sure.
foreach (var existingSetWithSameOnlineID in realm.All<BeatmapSetInfo>().Where(b => b.OnlineID == beatmapSet.OnlineID)) foreach (var existingSetWithSameOnlineID in realm.All<BeatmapSetInfo>().Where(b => b.OnlineID == onlineId))
{ {
existingSetWithSameOnlineID.DeletePending = true; existingSetWithSameOnlineID.DeletePending = true;
existingSetWithSameOnlineID.OnlineID = -1; existingSetWithSameOnlineID.OnlineID = -1;

View File

@ -4,7 +4,6 @@
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using JetBrains.Annotations; using JetBrains.Annotations;
@ -15,6 +14,7 @@ using osu.Framework.Graphics;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings;
using osu.Game.Utils;
namespace osu.Game.Configuration namespace osu.Game.Configuration
{ {
@ -186,6 +186,16 @@ namespace osu.Game.Configuration
break; break;
case BindableColour4 bColour:
yield return new SettingsColour
{
LabelText = attr.Label,
TooltipText = attr.Description,
Current = bColour
};
break;
case IBindable bindable: case IBindable bindable:
var dropdownType = typeof(ModSettingsEnumDropdown<>).MakeGenericType(bindable.GetType().GetGenericArguments()[0]); var dropdownType = typeof(ModSettingsEnumDropdown<>).MakeGenericType(bindable.GetType().GetGenericArguments()[0]);
var dropdown = (Drawable)Activator.CreateInstance(dropdownType)!; var dropdown = (Drawable)Activator.CreateInstance(dropdownType)!;
@ -227,11 +237,11 @@ namespace osu.Game.Configuration
case Bindable<bool> b: case Bindable<bool> b:
return b.Value; return b.Value;
case BindableColour4 c:
return c.Value.ToHex();
case IBindable u: case IBindable u:
// An unknown (e.g. enum) generic type. return BindableValueAccessor.GetValue(u);
var valueMethod = u.GetType().GetProperty(nameof(IBindable<int>.Value));
Debug.Assert(valueMethod != null);
return valueMethod.GetValue(u)!;
default: default:
// fall back for non-bindable cases. // fall back for non-bindable cases.

View File

@ -0,0 +1,59 @@
// 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.Shapes;
using osu.Framework.Input.Events;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
namespace osu.Game.Graphics.UserInterfaceV2.FileSelection
{
internal partial class BackgroundLayer : CompositeDrawable
{
private Box background = null!;
private readonly float defaultAlpha;
public BackgroundLayer(float defaultAlpha = 0f)
{
Depth = float.MaxValue;
this.defaultAlpha = defaultAlpha;
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider overlayColourProvider)
{
RelativeSizeAxes = Axes.Both;
Masking = true;
CornerRadius = 5;
InternalChildren = new Drawable[]
{
new HoverClickSounds(),
background = new Box
{
Alpha = defaultAlpha,
Colour = overlayColourProvider.Background3,
RelativeSizeAxes = Axes.Both,
},
};
}
protected override bool OnHover(HoverEvent e)
{
background.FadeTo(1, 200, Easing.OutQuint);
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
base.OnHoverLost(e);
background.FadeTo(defaultAlpha, 500, Easing.OutQuint);
}
}
}

View File

@ -8,20 +8,23 @@ using osu.Game.Overlays;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Graphics.UserInterfaceV2 namespace osu.Game.Graphics.UserInterfaceV2.FileSelection
{ {
internal partial class OsuDirectorySelectorHiddenToggle : OsuCheckbox internal partial class HiddenFilesToggleCheckbox : OsuCheckbox
{ {
public OsuDirectorySelectorHiddenToggle() public HiddenFilesToggleCheckbox()
{ {
RelativeSizeAxes = Axes.None; RelativeSizeAxes = Axes.None;
AutoSizeAxes = Axes.None; AutoSizeAxes = Axes.None;
Size = new Vector2(100, 50); Size = new Vector2(140, OsuDirectorySelectorBreadcrumbDisplay.HEIGHT);
Margin = new MarginPadding { Right = OsuDirectorySelectorBreadcrumbDisplay.HORIZONTAL_PADDING, };
Anchor = Anchor.CentreLeft; Anchor = Anchor.CentreLeft;
Origin = Anchor.CentreLeft; Origin = Anchor.CentreLeft;
LabelTextFlowContainer.Anchor = Anchor.CentreLeft; LabelTextFlowContainer.Anchor = Anchor.CentreLeft;
LabelTextFlowContainer.Origin = Anchor.CentreLeft; LabelTextFlowContainer.Origin = Anchor.CentreLeft;
LabelText = @"Show hidden"; LabelText = @"Show hidden";
Scale = new Vector2(0.8f);
} }
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader(true)]

View File

@ -0,0 +1,106 @@
// 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.IO;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osuTK;
namespace osu.Game.Graphics.UserInterfaceV2.FileSelection
{
internal partial class OsuDirectorySelectorBreadcrumbDisplay : DirectorySelectorBreadcrumbDisplay
{
public const float HEIGHT = 45;
public const float HORIZONTAL_PADDING = 20;
protected override Drawable CreateCaption() => Empty().With(d =>
{
d.Origin = Anchor.CentreLeft;
d.Anchor = Anchor.CentreLeft;
d.Alpha = 0;
});
protected override DirectorySelectorDirectory CreateRootDirectoryItem() => new OsuBreadcrumbDisplayComputer();
protected override DirectorySelectorDirectory CreateDirectoryItem(DirectoryInfo directory, string? displayName = null) => new OsuBreadcrumbDisplayDirectory(directory, displayName);
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
((FillFlowContainer)InternalChild).Padding = new MarginPadding
{
Horizontal = HORIZONTAL_PADDING,
Vertical = 10,
};
AddInternal(new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background4,
Depth = 1,
});
}
private partial class OsuBreadcrumbDisplayComputer : OsuBreadcrumbDisplayDirectory
{
protected override IconUsage? Icon => null;
public OsuBreadcrumbDisplayComputer()
: base(null, "Computer")
{
}
}
private partial class OsuBreadcrumbDisplayDirectory : DirectorySelectorDirectory
{
public OsuBreadcrumbDisplayDirectory(DirectoryInfo? directory, string? displayName = null)
: base(directory, displayName)
{
}
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
[BackgroundDependencyLoader]
private void load()
{
Anchor = Anchor.CentreLeft;
Origin = Anchor.CentreLeft;
Flow.AutoSizeAxes = Axes.X;
Flow.Height = 25;
Flow.Margin = new MarginPadding { Horizontal = 10, };
AddRangeInternal(new Drawable[]
{
new BackgroundLayer(0.5f)
{
Depth = 1
},
new HoverClickSounds(),
});
Flow.Add(new SpriteIcon
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Icon = FontAwesome.Solid.ChevronRight,
Size = new Vector2(FONT_SIZE / 2),
Margin = new MarginPadding { Left = 5, },
});
Flow.Colour = colourProvider.Light3;
}
protected override SpriteText CreateSpriteText() => new OsuSpriteText().With(t => t.Font = OsuFont.Default.With(weight: FontWeight.SemiBold));
protected override IconUsage? Icon => Directory.Name.Contains(Path.DirectorySeparatorChar) ? FontAwesome.Solid.Database : null;
}
}
}

View File

@ -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.IO;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics.Sprites;
namespace osu.Game.Graphics.UserInterfaceV2.FileSelection
{
internal partial class OsuDirectorySelectorDirectory : DirectorySelectorDirectory
{
public OsuDirectorySelectorDirectory(DirectoryInfo directory, string? displayName = null)
: base(directory, displayName)
{
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Flow.AutoSizeAxes = Axes.X;
Flow.Height = OsuDirectorySelector.ITEM_HEIGHT;
AddInternal(new BackgroundLayer());
Colour = colours.Orange1;
}
protected override SpriteText CreateSpriteText() => new OsuSpriteText().With(t => t.Font = OsuFont.Default.With(weight: FontWeight.Bold));
protected override IconUsage? Icon => Directory.Name.Contains(Path.DirectorySeparatorChar)
? FontAwesome.Solid.Database
: FontAwesome.Regular.Folder;
}
}

View File

@ -2,9 +2,11 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.IO; using System.IO;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Game.Overlays;
namespace osu.Game.Graphics.UserInterfaceV2 namespace osu.Game.Graphics.UserInterfaceV2.FileSelection
{ {
internal partial class OsuDirectorySelectorParentDirectory : OsuDirectorySelectorDirectory internal partial class OsuDirectorySelectorParentDirectory : OsuDirectorySelectorDirectory
{ {
@ -14,5 +16,11 @@ namespace osu.Game.Graphics.UserInterfaceV2
: base(directory, "..") : base(directory, "..")
{ {
} }
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
Colour = colourProvider.Content1;
}
} }
} }

View File

@ -0,0 +1,251 @@
// 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.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osuTK;
namespace osu.Game.Graphics.UserInterfaceV2
{
public partial class FormDropdown<T> : OsuDropdown<T>
{
/// <summary>
/// Caption describing this slider bar, displayed on top of the controls.
/// </summary>
public LocalisableString Caption { get; init; }
/// <summary>
/// Hint text containing an extended description of this slider bar, displayed in a tooltip when hovering the caption.
/// </summary>
public LocalisableString HintText { get; init; }
private FormDropdownHeader header = null!;
[BackgroundDependencyLoader]
private void load()
{
RelativeSizeAxes = Axes.X;
header.Caption = Caption;
header.HintText = HintText;
}
protected override DropdownHeader CreateHeader() => header = new FormDropdownHeader
{
Dropdown = this,
};
protected override DropdownMenu CreateMenu() => new FormDropdownMenu();
private partial class FormDropdownHeader : DropdownHeader
{
public FormDropdown<T> Dropdown { get; set; } = null!;
protected override DropdownSearchBar CreateSearchBar() => SearchBar = new FormDropdownSearchBar();
private LocalisableString captionText;
private LocalisableString hintText;
private LocalisableString labelText;
public LocalisableString Caption
{
get => captionText;
set
{
captionText = value;
if (caption.IsNotNull())
caption.Caption = value;
}
}
public LocalisableString HintText
{
get => hintText;
set
{
hintText = value;
if (caption.IsNotNull())
caption.TooltipText = value;
}
}
protected override LocalisableString Label
{
get => labelText;
set
{
labelText = value;
if (label.IsNotNull())
label.Text = labelText;
}
}
protected new FormDropdownSearchBar SearchBar { get; set; } = null!;
private FormFieldCaption caption = null!;
private OsuSpriteText label = null!;
private SpriteIcon chevron = null!;
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
[BackgroundDependencyLoader]
private void load()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.None;
Height = 50;
Masking = true;
CornerRadius = 5;
Foreground.AutoSizeAxes = Axes.None;
Foreground.RelativeSizeAxes = Axes.Both;
Foreground.Padding = new MarginPadding(9);
Foreground.Children = new Drawable[]
{
caption = new FormFieldCaption
{
Anchor = Anchor.TopLeft,
Origin = Anchor.TopLeft,
Caption = Caption,
TooltipText = HintText,
},
label = new OsuSpriteText
{
RelativeSizeAxes = Axes.X,
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
},
chevron = new SpriteIcon
{
Icon = FontAwesome.Solid.ChevronDown,
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Size = new Vector2(16),
},
};
AddInternal(new HoverClickSounds());
}
protected override void LoadComplete()
{
base.LoadComplete();
Dropdown.Current.BindDisabledChanged(_ => updateState());
SearchBar.SearchTerm.BindValueChanged(_ => updateState(), true);
Dropdown.Menu.StateChanged += _ =>
{
updateState();
updateChevron();
};
SearchBar.TextBox.OnCommit += (_, _) =>
{
Background.FlashColour(ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark2), 800, Easing.OutQuint);
};
}
protected override bool OnHover(HoverEvent e)
{
updateState();
return true;
}
protected override void OnHoverLost(HoverLostEvent e)
{
base.OnHoverLost(e);
updateState();
}
private void updateState()
{
label.Alpha = string.IsNullOrEmpty(SearchBar.SearchTerm.Value) ? 1 : 0;
caption.Colour = Dropdown.Current.Disabled ? colourProvider.Foreground1 : colourProvider.Content2;
label.Colour = Dropdown.Current.Disabled ? colourProvider.Foreground1 : colourProvider.Content1;
chevron.Colour = Dropdown.Current.Disabled ? colourProvider.Foreground1 : colourProvider.Content1;
DisabledColour = Colour4.White;
bool dropdownOpen = Dropdown.Menu.State == MenuState.Open;
if (!Dropdown.Current.Disabled)
{
BorderThickness = IsHovered || dropdownOpen ? 2 : 0;
BorderColour = dropdownOpen ? colourProvider.Highlight1 : colourProvider.Light4;
if (dropdownOpen)
Background.Colour = ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark3);
else if (IsHovered)
Background.Colour = ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark4);
else
Background.Colour = colourProvider.Background5;
}
else
{
Background.Colour = colourProvider.Background4;
}
}
private void updateChevron()
{
bool open = Dropdown.Menu.State == MenuState.Open;
chevron.ScaleTo(open ? new Vector2(1f, -1f) : Vector2.One, 300, Easing.OutQuint);
}
}
private partial class FormDropdownSearchBar : DropdownSearchBar
{
public FormTextBox.InnerTextBox TextBox { get; private set; } = null!;
protected override void PopIn() => this.FadeIn();
protected override void PopOut() => this.FadeOut();
protected override TextBox CreateTextBox() => TextBox = new FormTextBox.InnerTextBox();
[BackgroundDependencyLoader]
private void load()
{
TextBox.Anchor = Anchor.BottomLeft;
TextBox.Origin = Anchor.BottomLeft;
TextBox.RelativeSizeAxes = Axes.X;
TextBox.Margin = new MarginPadding(9);
}
}
private partial class FormDropdownMenu : OsuDropdownMenu
{
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
ItemsContainer.Padding = new MarginPadding(9);
Margin = new MarginPadding { Top = 5 };
MaskingContainer.BorderThickness = 2;
MaskingContainer.BorderColour = colourProvider.Highlight1;
}
}
}
public partial class FormEnumDropdown<T> : FormDropdown<T>
where T : struct, Enum
{
public FormEnumDropdown()
{
Items = Enum.GetValues<T>();
}
}
}

View File

@ -0,0 +1,277 @@
// 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 System.IO;
using System.Linq;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Framework.Platform;
using osu.Game.Database;
using osu.Game.Graphics.Sprites;
using osu.Game.Overlays;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Graphics.UserInterfaceV2
{
public partial class FormFileSelector : CompositeDrawable, IHasCurrentValue<FileInfo?>, ICanAcceptFiles, IHasPopover
{
public Bindable<FileInfo?> Current
{
get => current.Current;
set => current.Current = value;
}
private readonly BindableWithCurrent<FileInfo?> current = new BindableWithCurrent<FileInfo?>();
public IEnumerable<string> HandledExtensions => handledExtensions;
private readonly string[] handledExtensions;
/// <summary>
/// The initial path to use when displaying the <see cref="FileChooserPopover"/>.
/// </summary>
/// <remarks>
/// Uses a <see langword="null"/> value before the first selection is made
/// to ensure that the first selection starts at <see cref="GameHost.InitialFileSelectorPath"/>.
/// </remarks>
private string? initialChooserPath;
private readonly Bindable<Visibility> popoverState = new Bindable<Visibility>();
/// <summary>
/// Caption describing this file selector, displayed on top of the controls.
/// </summary>
public LocalisableString Caption { get; init; }
/// <summary>
/// Hint text containing an extended description of this file selector, displayed in a tooltip when hovering the caption.
/// </summary>
public LocalisableString HintText { get; init; }
/// <summary>
/// Text displayed in the selector when no file is selected.
/// </summary>
public LocalisableString PlaceholderText { get; init; }
private Box background = null!;
private FormFieldCaption caption = null!;
private OsuSpriteText placeholderText = null!;
private OsuSpriteText filenameText = null!;
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
[Resolved]
private OsuGameBase game { get; set; } = null!;
public FormFileSelector(params string[] handledExtensions)
{
this.handledExtensions = handledExtensions;
}
[BackgroundDependencyLoader]
private void load()
{
RelativeSizeAxes = Axes.X;
Height = 50;
Masking = true;
CornerRadius = 5;
InternalChildren = new Drawable[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background5,
},
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(9),
Children = new Drawable[]
{
caption = new FormFieldCaption
{
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,
},
new SpriteIcon
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
Icon = FontAwesome.Solid.FolderOpen,
Size = new Vector2(16),
Colour = colourProvider.Light1,
}
},
},
};
}
protected override void LoadComplete()
{
base.LoadComplete();
popoverState.BindValueChanged(_ => updateState());
current.BindValueChanged(_ =>
{
updateState();
onFileSelected();
});
current.BindDisabledChanged(_ => updateState(), true);
game.RegisterImportHandler(this);
}
private void onFileSelected()
{
if (Current.Value != null)
this.HidePopover();
initialChooserPath = Current.Value?.DirectoryName;
placeholderText.Alpha = Current.Value == null ? 1 : 0;
filenameText.Text = Current.Value?.Name ?? string.Empty;
background.FlashColour(ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark2), 800, Easing.OutQuint);
}
protected override bool OnClick(ClickEvent e)
{
this.ShowPopover();
return true;
}
protected override bool OnHover(HoverEvent e)
{
updateState();
return true;
}
protected override void OnHoverLost(HoverLostEvent e)
{
base.OnHoverLost(e);
updateState();
}
private void updateState()
{
caption.Colour = Current.Disabled ? colourProvider.Foreground1 : colourProvider.Content2;
filenameText.Colour = Current.Disabled ? colourProvider.Foreground1 : colourProvider.Content1;
if (!Current.Disabled)
{
BorderThickness = IsHovered || popoverState.Value == Visibility.Visible ? 2 : 0;
BorderColour = popoverState.Value == Visibility.Visible ? colourProvider.Highlight1 : colourProvider.Light4;
if (popoverState.Value == Visibility.Visible)
background.Colour = ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark3);
else if (IsHovered)
background.Colour = ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark4);
else
background.Colour = colourProvider.Background5;
}
else
{
background.Colour = colourProvider.Background4;
}
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (game.IsNotNull())
game.UnregisterImportHandler(this);
}
Task ICanAcceptFiles.Import(params string[] paths)
{
Schedule(() => Current.Value = new FileInfo(paths.First()));
return Task.CompletedTask;
}
Task ICanAcceptFiles.Import(ImportTask[] tasks, ImportParameters parameters) => throw new NotImplementedException();
public Popover GetPopover()
{
var popover = new FileChooserPopover(handledExtensions, Current, initialChooserPath);
popoverState.UnbindBindings();
popoverState.BindTo(popover.State);
return popover;
}
private partial class FileChooserPopover : OsuPopover
{
protected override string PopInSampleName => "UI/overlay-big-pop-in";
protected override string PopOutSampleName => "UI/overlay-big-pop-out";
public FileChooserPopover(string[] handledExtensions, Bindable<FileInfo?> currentFile, string? chooserPath)
: base(false)
{
Child = new Container
{
Size = new Vector2(600, 400),
Child = new OsuFileSelector(chooserPath, handledExtensions)
{
RelativeSizeAxes = Axes.Both,
CurrentFile = { BindTarget = currentFile }
},
};
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
Add(new Container
{
RelativeSizeAxes = Axes.Both,
Masking = true,
BorderThickness = 2,
CornerRadius = 10,
BorderColour = colourProvider.Highlight1,
Children = new Drawable[]
{
new Box
{
Colour = Color4.Transparent,
RelativeSizeAxes = Axes.Both,
},
}
});
}
}
}
}

View File

@ -1,41 +1,73 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.IO; using System.IO;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterfaceV2.FileSelection;
using osu.Game.Overlays;
namespace osu.Game.Graphics.UserInterfaceV2 namespace osu.Game.Graphics.UserInterfaceV2
{ {
public partial class OsuDirectorySelector : DirectorySelector public partial class OsuDirectorySelector : DirectorySelector
{ {
public const float ITEM_HEIGHT = 20; public const float ITEM_HEIGHT = 16;
public OsuDirectorySelector(string initialPath = null) private Box hiddenToggleBackground = null!;
public OsuDirectorySelector(string? initialPath = null)
: base(initialPath) : base(initialPath)
{ {
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load(OverlayColourProvider colourProvider)
{ {
Padding = new MarginPadding(10); AddInternal(new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background5,
Depth = float.MaxValue,
});
hiddenToggleBackground.Colour = colourProvider.Background4;
} }
protected override ScrollContainer<Drawable> CreateScrollContainer() => new OsuScrollContainer(); protected override ScrollContainer<Drawable> CreateScrollContainer() => new OsuScrollContainer
{
Padding = new MarginPadding
{
Horizontal = 20,
Vertical = 15,
}
};
protected override DirectorySelectorBreadcrumbDisplay CreateBreadcrumb() => new OsuDirectorySelectorBreadcrumbDisplay(); protected override DirectorySelectorBreadcrumbDisplay CreateBreadcrumb() => new OsuDirectorySelectorBreadcrumbDisplay();
protected override Drawable CreateHiddenToggleButton() => new OsuDirectorySelectorHiddenToggle { Current = { BindTarget = ShowHiddenItems } }; protected override Drawable CreateHiddenToggleButton() => new Container
{
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
Children = new Drawable[]
{
hiddenToggleBackground = new Box
{
RelativeSizeAxes = Axes.Both,
},
new HiddenFilesToggleCheckbox
{
Current = { BindTarget = ShowHiddenItems },
},
}
};
protected override DirectorySelectorDirectory CreateParentDirectoryItem(DirectoryInfo directory) => new OsuDirectorySelectorParentDirectory(directory); protected override DirectorySelectorDirectory CreateParentDirectoryItem(DirectoryInfo directory) => new OsuDirectorySelectorParentDirectory(directory);
protected override DirectorySelectorDirectory CreateDirectoryItem(DirectoryInfo directory, string displayName = null) => new OsuDirectorySelectorDirectory(directory, displayName); protected override DirectorySelectorDirectory CreateDirectoryItem(DirectoryInfo directory, string? displayName = null) => new OsuDirectorySelectorDirectory(directory, displayName);
protected override void NotifySelectionError() => this.FlashColour(Colour4.Red, 300); protected override void NotifySelectionError() => this.FlashColour(Colour4.Red, 300);
} }

View File

@ -1,65 +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.
#nullable disable
using System.IO;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics.Sprites;
using osuTK;
namespace osu.Game.Graphics.UserInterfaceV2
{
internal partial class OsuDirectorySelectorBreadcrumbDisplay : DirectorySelectorBreadcrumbDisplay
{
protected override Drawable CreateCaption() => new OsuSpriteText
{
Text = "Current Directory: ",
Font = OsuFont.Default.With(size: OsuDirectorySelector.ITEM_HEIGHT),
};
protected override DirectorySelectorDirectory CreateRootDirectoryItem() => new OsuBreadcrumbDisplayComputer();
protected override DirectorySelectorDirectory CreateDirectoryItem(DirectoryInfo directory, string displayName = null) => new OsuBreadcrumbDisplayDirectory(directory, displayName);
public OsuDirectorySelectorBreadcrumbDisplay()
{
Padding = new MarginPadding(15);
}
private partial class OsuBreadcrumbDisplayComputer : OsuBreadcrumbDisplayDirectory
{
protected override IconUsage? Icon => null;
public OsuBreadcrumbDisplayComputer()
: base(null, "Computer")
{
}
}
private partial class OsuBreadcrumbDisplayDirectory : OsuDirectorySelectorDirectory
{
public OsuBreadcrumbDisplayDirectory(DirectoryInfo directory, string displayName = null)
: base(directory, displayName)
{
}
[BackgroundDependencyLoader]
private void load()
{
Flow.Add(new SpriteIcon
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Icon = FontAwesome.Solid.ChevronRight,
Size = new Vector2(FONT_SIZE / 2)
});
}
protected override IconUsage? Icon => Directory.Name.Contains(Path.DirectorySeparatorChar) ? base.Icon : null;
}
}
}

View File

@ -1,66 +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.
#nullable disable
using System.IO;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
namespace osu.Game.Graphics.UserInterfaceV2
{
internal partial class OsuDirectorySelectorDirectory : DirectorySelectorDirectory
{
public OsuDirectorySelectorDirectory(DirectoryInfo directory, string displayName = null)
: base(directory, displayName)
{
}
[BackgroundDependencyLoader]
private void load()
{
Flow.AutoSizeAxes = Axes.X;
Flow.Height = OsuDirectorySelector.ITEM_HEIGHT;
AddRangeInternal(new Drawable[]
{
new Background
{
Depth = 1
},
new HoverClickSounds()
});
}
protected override SpriteText CreateSpriteText() => new OsuSpriteText();
protected override IconUsage? Icon => Directory.Name.Contains(Path.DirectorySeparatorChar)
? FontAwesome.Solid.Database
: FontAwesome.Regular.Folder;
internal partial class Background : CompositeDrawable
{
[BackgroundDependencyLoader(true)]
private void load(OverlayColourProvider overlayColourProvider, OsuColour colours)
{
RelativeSizeAxes = Axes.Both;
Masking = true;
CornerRadius = 5;
InternalChild = new Box
{
Colour = overlayColourProvider?.Background5 ?? colours.GreySeaFoamDarker,
RelativeSizeAxes = Axes.Both,
};
}
}
}
}

View File

@ -1,43 +1,74 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2.FileSelection;
using osu.Game.Overlays;
namespace osu.Game.Graphics.UserInterfaceV2 namespace osu.Game.Graphics.UserInterfaceV2
{ {
public partial class OsuFileSelector : FileSelector public partial class OsuFileSelector : FileSelector
{ {
public OsuFileSelector(string initialPath = null, string[] validFileExtensions = null) private Box hiddenToggleBackground = null!;
public OsuFileSelector(string? initialPath = null, string[]? validFileExtensions = null)
: base(initialPath, validFileExtensions) : base(initialPath, validFileExtensions)
{ {
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load(OverlayColourProvider colourProvider)
{ {
Padding = new MarginPadding(10); AddInternal(new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background5,
Depth = float.MaxValue,
});
hiddenToggleBackground.Colour = colourProvider.Background4;
} }
protected override ScrollContainer<Drawable> CreateScrollContainer() => new OsuScrollContainer(); protected override ScrollContainer<Drawable> CreateScrollContainer() => new OsuScrollContainer
{
Padding = new MarginPadding
{
Horizontal = 20,
Vertical = 15,
}
};
protected override DirectorySelectorBreadcrumbDisplay CreateBreadcrumb() => new OsuDirectorySelectorBreadcrumbDisplay(); protected override DirectorySelectorBreadcrumbDisplay CreateBreadcrumb() => new OsuDirectorySelectorBreadcrumbDisplay();
protected override Drawable CreateHiddenToggleButton() => new OsuDirectorySelectorHiddenToggle { Current = { BindTarget = ShowHiddenItems } }; protected override Drawable CreateHiddenToggleButton() => new Container
{
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
Children = new Drawable[]
{
hiddenToggleBackground = new Box
{
RelativeSizeAxes = Axes.Both,
},
new HiddenFilesToggleCheckbox
{
Current = { BindTarget = ShowHiddenItems },
},
}
};
protected override DirectorySelectorDirectory CreateParentDirectoryItem(DirectoryInfo directory) => new OsuDirectorySelectorParentDirectory(directory); protected override DirectorySelectorDirectory CreateParentDirectoryItem(DirectoryInfo directory) => new OsuDirectorySelectorParentDirectory(directory);
protected override DirectorySelectorDirectory CreateDirectoryItem(DirectoryInfo directory, string displayName = null) => new OsuDirectorySelectorDirectory(directory, displayName); protected override DirectorySelectorDirectory CreateDirectoryItem(DirectoryInfo directory, string? displayName = null) => new OsuDirectorySelectorDirectory(directory, displayName);
protected override DirectoryListingFile CreateFileItem(FileInfo file) => new OsuDirectoryListingFile(file); protected override DirectoryListingFile CreateFileItem(FileInfo file) => new OsuDirectoryListingFile(file);
@ -51,19 +82,14 @@ namespace osu.Game.Graphics.UserInterfaceV2
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load(OverlayColourProvider colourProvider)
{ {
Flow.AutoSizeAxes = Axes.X; Flow.AutoSizeAxes = Axes.X;
Flow.Height = OsuDirectorySelector.ITEM_HEIGHT; Flow.Height = OsuDirectorySelector.ITEM_HEIGHT;
AddRangeInternal(new Drawable[] AddInternal(new BackgroundLayer());
{
new OsuDirectorySelectorDirectory.Background Colour = colourProvider.Light3;
{
Depth = 1
},
new HoverClickSounds()
});
} }
protected override IconUsage? Icon protected override IconUsage? Icon
@ -91,7 +117,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
} }
} }
protected override SpriteText CreateSpriteText() => new OsuSpriteText(); protected override SpriteText CreateSpriteText() => new OsuSpriteText().With(t => t.Font = OsuFont.Default.With(weight: FontWeight.SemiBold));
} }
} }
} }

View File

@ -59,6 +59,26 @@ namespace osu.Game.Localisation.SkinComponents
/// </summary> /// </summary>
public static LocalisableString ShowLabelDescription => new TranslatableString(getKey(@"show_label_description"), @"Whether the component's label should be shown."); public static LocalisableString ShowLabelDescription => new TranslatableString(getKey(@"show_label_description"), @"Whether the component's label should be shown.");
/// <summary>
/// "Colour"
/// </summary>
public static LocalisableString Colour => new TranslatableString(getKey(@"colour"), @"Colour");
/// <summary>
/// "The colour of the component."
/// </summary>
public static LocalisableString ColourDescription => new TranslatableString(getKey(@"colour_description"), @"The colour of the component.");
/// <summary>
/// "Text colour"
/// </summary>
public static LocalisableString TextColour => new TranslatableString(getKey(@"text_colour"), @"Text colour");
/// <summary>
/// "The colour of the text."
/// </summary>
public static LocalisableString TextColourDescription => new TranslatableString(getKey(@"text_colour_description"), @"The colour of the text.");
private static string getKey(string key) => $@"{prefix}:{key}"; private static string getKey(string key) => $@"{prefix}:{key}";
} }
} }

View File

@ -40,7 +40,10 @@ namespace osu.Game.Online
// Used to interact with manager classes that don't support interface types. Will eventually be replaced. // Used to interact with manager classes that don't support interface types. Will eventually be replaced.
var beatmapSetInfo = new BeatmapSetInfo { OnlineID = TrackedItem.OnlineID }; var beatmapSetInfo = new BeatmapSetInfo { OnlineID = TrackedItem.OnlineID };
realmSubscription = realm.RegisterForNotifications(r => r.All<BeatmapSetInfo>().Where(s => s.OnlineID == TrackedItem.OnlineID && !s.DeletePending), (items, _) => // Required local for iOS. Will cause runtime crash if inlined.
int onlineId = TrackedItem.OnlineID;
realmSubscription = realm.RegisterForNotifications(r => r.All<BeatmapSetInfo>().Where(s => s.OnlineID == onlineId && !s.DeletePending), (items, _) =>
{ {
if (items.Any()) if (items.Any())
Schedule(() => UpdateState(DownloadState.LocallyAvailable)); Schedule(() => UpdateState(DownloadState.LocallyAvailable));

View File

@ -46,10 +46,15 @@ namespace osu.Game.Online
Downloader.DownloadBegan += downloadBegan; Downloader.DownloadBegan += downloadBegan;
Downloader.DownloadFailed += downloadFailed; Downloader.DownloadFailed += downloadFailed;
// Required local for iOS. Will cause runtime crash if inlined.
long onlineId = TrackedItem.OnlineID;
long legacyOnlineId = TrackedItem.LegacyOnlineID;
string hash = TrackedItem.Hash;
realmSubscription = realm.RegisterForNotifications(r => r.All<ScoreInfo>().Where(s => realmSubscription = realm.RegisterForNotifications(r => r.All<ScoreInfo>().Where(s =>
((s.OnlineID > 0 && s.OnlineID == TrackedItem.OnlineID) ((s.OnlineID > 0 && s.OnlineID == onlineId)
|| (s.LegacyOnlineID > 0 && s.LegacyOnlineID == TrackedItem.LegacyOnlineID) || (s.LegacyOnlineID > 0 && s.LegacyOnlineID == legacyOnlineId)
|| (!string.IsNullOrEmpty(s.Hash) && s.Hash == TrackedItem.Hash)) || (!string.IsNullOrEmpty(s.Hash) && s.Hash == hash))
&& !s.DeletePending), (items, _) => && !s.DeletePending), (items, _) =>
{ {
if (items.Any()) if (items.Any())

View File

@ -314,6 +314,7 @@ namespace osu.Game.Overlays.FirstRunSetup
private partial class DirectoryChooserPopover : OsuPopover private partial class DirectoryChooserPopover : OsuPopover
{ {
public DirectoryChooserPopover(Bindable<DirectoryInfo?> currentDirectory) public DirectoryChooserPopover(Bindable<DirectoryInfo?> currentDirectory)
: base(false)
{ {
Child = new Container Child = new Container
{ {
@ -325,6 +326,13 @@ namespace osu.Game.Overlays.FirstRunSetup
}, },
}; };
} }
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
Body.BorderColour = colourProvider.Highlight1;
Body.BorderThickness = 2;
}
} }
} }
} }

View File

@ -48,8 +48,11 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
/// </summary> /// </summary>
protected virtual DirectoryInfo InitialPath => null; protected virtual DirectoryInfo InitialPath => null;
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load()
{ {
InternalChild = new Container InternalChild = new Container
{ {
@ -64,7 +67,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
new Box new Box
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = colours.GreySeaFoamDark Colour = colourProvider.Background4,
}, },
new GridContainer new GridContainer
{ {

View File

@ -0,0 +1,80 @@
// 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.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterfaceV2;
namespace osu.Game.Overlays.Settings
{
public partial class SettingsColour : SettingsItem<Colour4>
{
protected override Drawable CreateControl() => new ColourControl();
public partial class ColourControl : OsuClickableContainer, IHasPopover, IHasCurrentValue<Colour4>
{
private readonly BindableWithCurrent<Colour4> current = new BindableWithCurrent<Colour4>(Colour4.White);
public Bindable<Colour4> Current
{
get => current.Current;
set => current.Current = value;
}
private readonly Box fill;
private readonly OsuSpriteText colourHexCode;
public ColourControl()
{
RelativeSizeAxes = Axes.X;
Height = 40;
CornerRadius = 20;
Masking = true;
Action = this.ShowPopover;
Children = new Drawable[]
{
fill = new Box
{
RelativeSizeAxes = Axes.Both
},
colourHexCode = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = OsuFont.Default.With(size: 20)
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
Current.BindValueChanged(_ => updateColour(), true);
}
private void updateColour()
{
fill.Colour = Current.Value;
colourHexCode.Text = Current.Value.ToHex();
colourHexCode.Colour = OsuColour.ForegroundTextColourFor(Current.Value);
}
public Popover GetPopover() => new OsuPopover(false)
{
Child = new OsuColourPicker
{
Current = { BindTarget = Current }
}
};
}
}
}

View File

@ -33,6 +33,7 @@ using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Components; using osu.Game.Screens.Edit.Components;
using osu.Game.Screens.Edit.Components.Menus; using osu.Game.Screens.Edit.Components.Menus;
using osu.Game.Skinning; using osu.Game.Skinning;
using osu.Framework.Graphics.Cursor;
namespace osu.Game.Overlays.SkinEditor namespace osu.Game.Overlays.SkinEditor
{ {
@ -118,107 +119,111 @@ namespace osu.Game.Overlays.SkinEditor
InternalChild = new OsuContextMenuContainer InternalChild = new OsuContextMenuContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Child = new GridContainer Child = new PopoverContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
RowDimensions = new[] Child = new GridContainer
{ {
new Dimension(GridSizeMode.AutoSize), RelativeSizeAxes = Axes.Both,
new Dimension(GridSizeMode.AutoSize), RowDimensions = new[]
new Dimension(), {
}, new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
new Dimension(),
},
Content = new[] Content = new[]
{
new Drawable[]
{ {
new Container new Drawable[]
{ {
Name = @"Menu container", new Container
RelativeSizeAxes = Axes.X,
Depth = float.MinValue,
Height = MENU_HEIGHT,
Children = new Drawable[]
{ {
new EditorMenuBar Name = @"Menu container",
RelativeSizeAxes = Axes.X,
Depth = float.MinValue,
Height = MENU_HEIGHT,
Children = new Drawable[]
{ {
Anchor = Anchor.CentreLeft, new EditorMenuBar
Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.Both,
Items = new[]
{ {
new MenuItem(CommonStrings.MenuBarFile) Anchor = Anchor.CentreLeft,
{ Origin = Anchor.CentreLeft,
Items = new OsuMenuItem[]
{
new EditorMenuItem(Web.CommonStrings.ButtonsSave, MenuItemType.Standard, () => Save()),
new EditorMenuItem(CommonStrings.Export, MenuItemType.Standard, () => skins.ExportCurrentSkin()) { Action = { Disabled = !RuntimeInfo.IsDesktop } },
new OsuMenuItemSpacer(),
new EditorMenuItem(CommonStrings.RevertToDefault, MenuItemType.Destructive, () => dialogOverlay?.Push(new RevertConfirmDialog(revert))),
new OsuMenuItemSpacer(),
new EditorMenuItem(CommonStrings.Exit, MenuItemType.Standard, () => skinEditorOverlay?.Hide()),
},
},
new MenuItem(CommonStrings.MenuBarEdit)
{
Items = new OsuMenuItem[]
{
undoMenuItem = new EditorMenuItem(CommonStrings.Undo, MenuItemType.Standard, Undo),
redoMenuItem = new EditorMenuItem(CommonStrings.Redo, MenuItemType.Standard, Redo),
new OsuMenuItemSpacer(),
cutMenuItem = new EditorMenuItem(CommonStrings.Cut, MenuItemType.Standard, Cut),
copyMenuItem = new EditorMenuItem(CommonStrings.Copy, MenuItemType.Standard, Copy),
pasteMenuItem = new EditorMenuItem(CommonStrings.Paste, MenuItemType.Standard, Paste),
cloneMenuItem = new EditorMenuItem(CommonStrings.Clone, MenuItemType.Standard, Clone),
}
},
}
},
headerText = new OsuTextFlowContainer
{
TextAnchor = Anchor.TopRight,
Padding = new MarginPadding(5),
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
AutoSizeAxes = Axes.X,
RelativeSizeAxes = Axes.Y,
},
},
},
},
new Drawable[]
{
new SkinEditorSceneLibrary
{
RelativeSizeAxes = Axes.X,
},
},
new Drawable[]
{
new GridContainer
{
RelativeSizeAxes = Axes.Both,
ColumnDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
new Dimension(),
new Dimension(GridSizeMode.AutoSize),
},
Content = new[]
{
new Drawable[]
{
componentsSidebar = new EditorSidebar(),
content = new Container
{
Depth = float.MaxValue,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Items = new[]
{
new MenuItem(CommonStrings.MenuBarFile)
{
Items = new OsuMenuItem[]
{
new EditorMenuItem(Web.CommonStrings.ButtonsSave, MenuItemType.Standard, () => Save()),
new EditorMenuItem(CommonStrings.Export, MenuItemType.Standard, () => skins.ExportCurrentSkin()) { Action = { Disabled = !RuntimeInfo.IsDesktop } },
new OsuMenuItemSpacer(),
new EditorMenuItem(CommonStrings.RevertToDefault, MenuItemType.Destructive, () => dialogOverlay?.Push(new RevertConfirmDialog(revert))),
new OsuMenuItemSpacer(),
new EditorMenuItem(CommonStrings.Exit, MenuItemType.Standard, () => skinEditorOverlay?.Hide()),
},
},
new MenuItem(CommonStrings.MenuBarEdit)
{
Items = new OsuMenuItem[]
{
undoMenuItem = new EditorMenuItem(CommonStrings.Undo, MenuItemType.Standard, Undo),
redoMenuItem = new EditorMenuItem(CommonStrings.Redo, MenuItemType.Standard, Redo),
new OsuMenuItemSpacer(),
cutMenuItem = new EditorMenuItem(CommonStrings.Cut, MenuItemType.Standard, Cut),
copyMenuItem = new EditorMenuItem(CommonStrings.Copy, MenuItemType.Standard, Copy),
pasteMenuItem = new EditorMenuItem(CommonStrings.Paste, MenuItemType.Standard, Paste),
cloneMenuItem = new EditorMenuItem(CommonStrings.Clone, MenuItemType.Standard, Clone),
}
},
}
}, },
settingsSidebar = new EditorSidebar(), headerText = new OsuTextFlowContainer
{
TextAnchor = Anchor.TopRight,
Padding = new MarginPadding(5),
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
AutoSizeAxes = Axes.X,
RelativeSizeAxes = Axes.Y,
},
},
},
},
new Drawable[]
{
new SkinEditorSceneLibrary
{
RelativeSizeAxes = Axes.X,
},
},
new Drawable[]
{
new GridContainer
{
RelativeSizeAxes = Axes.Both,
ColumnDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
new Dimension(),
new Dimension(GridSizeMode.AutoSize),
},
Content = new[]
{
new Drawable[]
{
componentsSidebar = new EditorSidebar(),
content = new Container
{
Depth = float.MaxValue,
RelativeSizeAxes = Axes.Both,
},
settingsSidebar = new EditorSidebar(),
}
} }
} }
} },
}, }
} }
} }
}; };

View File

@ -323,7 +323,7 @@ namespace osu.Game.Rulesets.Edit
/// <remarks> /// <remarks>
/// A "select" tool is automatically added as the first tool. /// A "select" tool is automatically added as the first tool.
/// </remarks> /// </remarks>
protected abstract IReadOnlyList<HitObjectCompositionTool> CompositionTools { get; } protected abstract IReadOnlyList<CompositionTool> CompositionTools { get; }
/// <summary> /// <summary>
/// A collection of states which will be displayed to the user in the toolbox. /// A collection of states which will be displayed to the user in the toolbox.
@ -466,7 +466,7 @@ namespace osu.Game.Rulesets.Edit
private void setSelectTool() => toolboxCollection.Items.First().Select(); private void setSelectTool() => toolboxCollection.Items.First().Select();
private void toolSelected(HitObjectCompositionTool tool) private void toolSelected(CompositionTool tool)
{ {
BlueprintContainer.CurrentTool = tool; BlueprintContainer.CurrentTool = tool;

View File

@ -9,9 +9,9 @@ namespace osu.Game.Rulesets.Edit
{ {
public class HitObjectCompositionToolButton : RadioButton public class HitObjectCompositionToolButton : RadioButton
{ {
public HitObjectCompositionTool Tool { get; } public CompositionTool Tool { get; }
public HitObjectCompositionToolButton(HitObjectCompositionTool tool, Action? action) public HitObjectCompositionToolButton(CompositionTool tool, Action? action)
: base(tool.Name, action, tool.CreateIcon) : base(tool.Name, action, tool.CreateIcon)
{ {
Tool = tool; Tool = tool;

View File

@ -0,0 +1,126 @@
// 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 System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose;
namespace osu.Game.Rulesets.Edit
{
/// <summary>
/// A blueprint which governs the creation of a new <see cref="HitObject"/> to actualisation.
/// </summary>
public abstract partial class HitObjectPlacementBlueprint : PlacementBlueprint
{
/// <summary>
/// Whether the sample bank should be taken from the previous hit object.
/// </summary>
public bool AutomaticBankAssignment { get; set; }
/// <summary>
/// The <see cref="HitObject"/> that is being placed.
/// </summary>
public readonly HitObject HitObject;
[Resolved]
protected EditorClock EditorClock { get; private set; } = null!;
[Resolved]
private EditorBeatmap beatmap { get; set; } = null!;
private Bindable<double> startTimeBindable = null!;
private HitObject? getPreviousHitObject() => beatmap.HitObjects.TakeWhile(h => h.StartTime <= startTimeBindable.Value).LastOrDefault();
[Resolved]
private IPlacementHandler placementHandler { get; set; } = null!;
protected HitObjectPlacementBlueprint(HitObject hitObject)
{
HitObject = hitObject;
// adding the default hit sample should be the case regardless of the ruleset.
HitObject.Samples.Add(new HitSampleInfo(HitSampleInfo.HIT_NORMAL));
}
[BackgroundDependencyLoader]
private void load()
{
startTimeBindable = HitObject.StartTimeBindable.GetBoundCopy();
startTimeBindable.BindValueChanged(_ => ApplyDefaultsToHitObject(), true);
}
protected override void BeginPlacement(bool commitStart = false)
{
base.BeginPlacement(commitStart);
placementHandler.BeginPlacement(HitObject);
}
public override void EndPlacement(bool commit)
{
base.EndPlacement(commit);
placementHandler.EndPlacement(HitObject, IsValidForPlacement && commit);
}
/// <summary>
/// Updates the time and position of this <see cref="HitObjectPlacementBlueprint"/> based on the provided snap information.
/// </summary>
/// <param name="result">The snap result information.</param>
public override void UpdateTimeAndPosition(SnapResult result)
{
if (PlacementActive == PlacementState.Waiting)
{
HitObject.StartTime = result.Time ?? EditorClock.CurrentTime;
if (HitObject is IHasComboInformation comboInformation)
comboInformation.UpdateComboInformation(getPreviousHitObject() as IHasComboInformation);
}
var lastHitObject = getPreviousHitObject();
if (AutomaticBankAssignment)
{
// Create samples based on the sample settings of the previous hit object
if (lastHitObject != null)
{
for (int i = 0; i < HitObject.Samples.Count; i++)
HitObject.Samples[i] = lastHitObject.CreateHitSampleInfo(HitObject.Samples[i].Name);
}
}
else
{
var lastHitNormal = lastHitObject?.Samples?.FirstOrDefault(o => o.Name == HitSampleInfo.HIT_NORMAL);
if (lastHitNormal != null)
{
// Only inherit the volume from the previous hit object
for (int i = 0; i < HitObject.Samples.Count; i++)
HitObject.Samples[i] = HitObject.Samples[i].With(newVolume: lastHitNormal.Volume);
}
}
if (HitObject is IHasRepeats hasRepeats)
{
// Make sure all the node samples are identical to the hit object's samples
for (int i = 0; i < hasRepeats.NodeSamples.Count; i++)
hasRepeats.NodeSamples[i] = HitObject.Samples.Select(o => o.With()).ToList();
}
}
/// <summary>
/// Invokes <see cref="Objects.HitObject.ApplyDefaults(ControlPointInfo,IBeatmapDifficultyInfo,CancellationToken)"/>,
/// refreshing <see cref="Objects.HitObject.NestedHitObjects"/> and parameters for the <see cref="HitObject"/>.
/// </summary>
protected void ApplyDefaultsToHitObject() => HitObject.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty);
}
}

View File

@ -1,29 +1,19 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Linq;
using System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose;
using osuTK; using osuTK;
using osuTK.Input; using osuTK.Input;
namespace osu.Game.Rulesets.Edit namespace osu.Game.Rulesets.Edit
{ {
/// <summary> /// <summary>
/// A blueprint which governs the creation of a new <see cref="HitObject"/> to actualisation. /// A blueprint which governs the placement of something.
/// </summary> /// </summary>
public abstract partial class PlacementBlueprint : CompositeDrawable, IKeyBindingHandler<GlobalAction> public abstract partial class PlacementBlueprint : CompositeDrawable, IKeyBindingHandler<GlobalAction>
{ {
@ -32,29 +22,6 @@ namespace osu.Game.Rulesets.Edit
/// </summary> /// </summary>
public PlacementState PlacementActive { get; private set; } public PlacementState PlacementActive { get; private set; }
/// <summary>
/// Whether the sample bank should be taken from the previous hit object.
/// </summary>
public bool AutomaticBankAssignment { get; set; }
/// <summary>
/// The <see cref="HitObject"/> that is being placed.
/// </summary>
public readonly HitObject HitObject;
[Resolved]
protected EditorClock EditorClock { get; private set; } = null!;
[Resolved]
private EditorBeatmap beatmap { get; set; } = null!;
private Bindable<double> startTimeBindable = null!;
private HitObject? getPreviousHitObject() => beatmap.HitObjects.TakeWhile(h => h.StartTime <= startTimeBindable.Value).LastOrDefault();
[Resolved]
private IPlacementHandler placementHandler { get; set; } = null!;
/// <summary> /// <summary>
/// Whether this blueprint is currently in a state that can be committed. /// Whether this blueprint is currently in a state that can be committed.
/// </summary> /// </summary>
@ -64,13 +31,8 @@ namespace osu.Game.Rulesets.Edit
/// </remarks> /// </remarks>
protected virtual bool IsValidForPlacement => true; protected virtual bool IsValidForPlacement => true;
protected PlacementBlueprint(HitObject hitObject) protected PlacementBlueprint()
{ {
HitObject = hitObject;
// adding the default hit sample should be the case regardless of the ruleset.
HitObject.Samples.Add(new HitSampleInfo(HitSampleInfo.HIT_NORMAL));
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
// This is required to allow the blueprint's position to be updated via OnMouseMove/Handle // This is required to allow the blueprint's position to be updated via OnMouseMove/Handle
@ -78,30 +40,22 @@ namespace osu.Game.Rulesets.Edit
AlwaysPresent = true; AlwaysPresent = true;
} }
[BackgroundDependencyLoader]
private void load()
{
startTimeBindable = HitObject.StartTimeBindable.GetBoundCopy();
startTimeBindable.BindValueChanged(_ => ApplyDefaultsToHitObject(), true);
}
/// <summary> /// <summary>
/// Signals that the placement of <see cref="HitObject"/> has started. /// Signals that the placement has started.
/// </summary> /// </summary>
/// <param name="commitStart">Whether this call is committing a value for HitObject.StartTime and continuing with further adjustments.</param> /// <param name="commitStart">Whether this call is committing a value and continuing with further adjustments.</param>
protected void BeginPlacement(bool commitStart = false) protected virtual void BeginPlacement(bool commitStart = false)
{ {
placementHandler.BeginPlacement(HitObject);
if (commitStart) if (commitStart)
PlacementActive = PlacementState.Active; PlacementActive = PlacementState.Active;
} }
/// <summary> /// <summary>
/// Signals that the placement of <see cref="HitObject"/> has finished. /// Signals that the placement of <see cref="HitObject"/> has finished.
/// This will destroy this <see cref="PlacementBlueprint"/>, and add the HitObject.StartTime to the <see cref="Beatmap"/>. /// This will destroy this <see cref="PlacementBlueprint"/>, and commit the changes.
/// </summary> /// </summary>
/// <param name="commit">Whether the object should be committed. Note that a commit may fail if <see cref="IsValidForPlacement"/> is <c>false</c>.</param> /// <param name="commit">Whether the changes should be committed. Note that a commit may fail if <see cref="IsValidForPlacement"/> is <c>false</c>.</param>
public void EndPlacement(bool commit) public virtual void EndPlacement(bool commit)
{ {
switch (PlacementActive) switch (PlacementActive)
{ {
@ -114,10 +68,17 @@ namespace osu.Game.Rulesets.Edit
break; break;
} }
placementHandler.EndPlacement(HitObject, IsValidForPlacement && commit);
PlacementActive = PlacementState.Finished; PlacementActive = PlacementState.Finished;
} }
/// <summary>
/// Updates the time and position of this <see cref="PlacementBlueprint"/> based on the provided snap information.
/// </summary>
/// <param name="result">The snap result information.</param>
public virtual void UpdateTimeAndPosition(SnapResult result)
{
}
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e) public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{ {
if (PlacementActive == PlacementState.Waiting) if (PlacementActive == PlacementState.Waiting)
@ -138,57 +99,6 @@ namespace osu.Game.Rulesets.Edit
{ {
} }
/// <summary>
/// Updates the time and position of this <see cref="PlacementBlueprint"/> based on the provided snap information.
/// </summary>
/// <param name="result">The snap result information.</param>
public virtual void UpdateTimeAndPosition(SnapResult result)
{
if (PlacementActive == PlacementState.Waiting)
{
HitObject.StartTime = result.Time ?? EditorClock.CurrentTime;
if (HitObject is IHasComboInformation comboInformation)
comboInformation.UpdateComboInformation(getPreviousHitObject() as IHasComboInformation);
}
var lastHitObject = getPreviousHitObject();
if (AutomaticBankAssignment)
{
// Create samples based on the sample settings of the previous hit object
if (lastHitObject != null)
{
for (int i = 0; i < HitObject.Samples.Count; i++)
HitObject.Samples[i] = lastHitObject.CreateHitSampleInfo(HitObject.Samples[i].Name);
}
}
else
{
var lastHitNormal = lastHitObject?.Samples?.FirstOrDefault(o => o.Name == HitSampleInfo.HIT_NORMAL);
if (lastHitNormal != null)
{
// Only inherit the volume from the previous hit object
for (int i = 0; i < HitObject.Samples.Count; i++)
HitObject.Samples[i] = HitObject.Samples[i].With(newVolume: lastHitNormal.Volume);
}
}
if (HitObject is IHasRepeats hasRepeats)
{
// Make sure all the node samples are identical to the hit object's samples
for (int i = 0; i < hasRepeats.NodeSamples.Count; i++)
hasRepeats.NodeSamples[i] = HitObject.Samples.Select(o => o.With()).ToList();
}
}
/// <summary>
/// Invokes <see cref="Objects.HitObject.ApplyDefaults(ControlPointInfo,IBeatmapDifficultyInfo,CancellationToken)"/>,
/// refreshing <see cref="Objects.HitObject.NestedHitObjects"/> and parameters for the <see cref="HitObject"/>.
/// </summary>
protected void ApplyDefaultsToHitObject() => HitObject.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty);
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
protected override bool Handle(UIEvent e) protected override bool Handle(UIEvent e)

View File

@ -6,13 +6,13 @@ using osu.Framework.Localisation;
namespace osu.Game.Rulesets.Edit.Tools namespace osu.Game.Rulesets.Edit.Tools
{ {
public abstract class HitObjectCompositionTool public abstract class CompositionTool
{ {
public readonly string Name; public readonly string Name;
public LocalisableString TooltipText { get; init; } public LocalisableString TooltipText { get; init; }
protected HitObjectCompositionTool(string name) protected CompositionTool(string name)
{ {
Name = name; Name = name;
} }

View File

@ -9,7 +9,7 @@ using osu.Game.Graphics;
namespace osu.Game.Rulesets.Edit.Tools namespace osu.Game.Rulesets.Edit.Tools
{ {
public class SelectTool : HitObjectCompositionTool public class SelectTool : CompositionTool
{ {
public SelectTool() public SelectTool()
: base("Select") : base("Select")
@ -18,6 +18,6 @@ namespace osu.Game.Rulesets.Edit.Tools
public override Drawable CreateIcon() => new SpriteIcon { Icon = OsuIcon.EditorSelect }; public override Drawable CreateIcon() => new SpriteIcon { Icon = OsuIcon.EditorSelect };
public override PlacementBlueprint CreatePlacementBlueprint() => null; public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => null;
} }
} }

View File

@ -266,8 +266,7 @@ namespace osu.Game.Rulesets.Mods
// TODO: special case for handling number types // TODO: special case for handling number types
PropertyInfo property = targetSetting.GetType().GetProperty(nameof(Bindable<bool>.Value))!; BindableValueAccessor.SetValue(targetSetting, BindableValueAccessor.GetValue(sourceSetting));
property.SetValue(targetSetting, property.GetValue(sourceSetting));
} }
} }

View File

@ -41,6 +41,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
public PlacementBlueprint CurrentPlacement { get; private set; } public PlacementBlueprint CurrentPlacement { get; private set; }
public HitObjectPlacementBlueprint CurrentHitObjectPlacement => CurrentPlacement as HitObjectPlacementBlueprint;
[Resolved(canBeNull: true)] [Resolved(canBeNull: true)]
private EditorScreenWithTimeline editorScreen { get; set; } private EditorScreenWithTimeline editorScreen { get; set; }
@ -164,13 +166,13 @@ namespace osu.Game.Screens.Edit.Compose.Components
private void updatePlacementNewCombo() private void updatePlacementNewCombo()
{ {
if (CurrentPlacement?.HitObject is IHasComboInformation c) if (CurrentHitObjectPlacement?.HitObject is IHasComboInformation c)
c.NewCombo = NewCombo.Value == TernaryState.True; c.NewCombo = NewCombo.Value == TernaryState.True;
} }
private void updatePlacementSamples() private void updatePlacementSamples()
{ {
if (CurrentPlacement == null) return; if (CurrentHitObjectPlacement == null) return;
foreach (var kvp in SelectionHandler.SelectionSampleStates) foreach (var kvp in SelectionHandler.SelectionSampleStates)
sampleChanged(kvp.Key, kvp.Value.Value); sampleChanged(kvp.Key, kvp.Value.Value);
@ -181,9 +183,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
private void sampleChanged(string sampleName, TernaryState state) private void sampleChanged(string sampleName, TernaryState state)
{ {
if (CurrentPlacement == null) return; if (CurrentHitObjectPlacement == null) return;
var samples = CurrentPlacement.HitObject.Samples; var samples = CurrentHitObjectPlacement.HitObject.Samples;
var existingSample = samples.FirstOrDefault(s => s.Name == sampleName); var existingSample = samples.FirstOrDefault(s => s.Name == sampleName);
@ -196,19 +198,19 @@ namespace osu.Game.Screens.Edit.Compose.Components
case TernaryState.True: case TernaryState.True:
if (existingSample == null) if (existingSample == null)
samples.Add(CurrentPlacement.HitObject.CreateHitSampleInfo(sampleName)); samples.Add(CurrentHitObjectPlacement.HitObject.CreateHitSampleInfo(sampleName));
break; break;
} }
} }
private void bankChanged(string bankName, TernaryState state) private void bankChanged(string bankName, TernaryState state)
{ {
if (CurrentPlacement == null) return; if (CurrentHitObjectPlacement == null) return;
if (bankName == EditorSelectionHandler.HIT_BANK_AUTO) if (bankName == EditorSelectionHandler.HIT_BANK_AUTO)
CurrentPlacement.AutomaticBankAssignment = state == TernaryState.True; CurrentHitObjectPlacement.AutomaticBankAssignment = state == TernaryState.True;
else if (state == TernaryState.True) else if (state == TernaryState.True)
CurrentPlacement.HitObject.Samples = CurrentPlacement.HitObject.Samples.Select(s => s.With(newBank: bankName)).ToList(); CurrentHitObjectPlacement.HitObject.Samples = CurrentHitObjectPlacement.HitObject.Samples.Select(s => s.With(newBank: bankName)).ToList();
} }
public readonly Bindable<TernaryState> NewCombo = new Bindable<TernaryState> { Description = "New Combo" }; public readonly Bindable<TernaryState> NewCombo = new Bindable<TernaryState> { Description = "New Combo" };
@ -386,12 +388,12 @@ namespace osu.Game.Screens.Edit.Compose.Components
CurrentPlacement = null; CurrentPlacement = null;
} }
private HitObjectCompositionTool currentTool; private CompositionTool currentTool;
/// <summary> /// <summary>
/// The current placement tool. /// The current placement tool.
/// </summary> /// </summary>
public HitObjectCompositionTool CurrentTool public CompositionTool CurrentTool
{ {
get => currentTool; get => currentTool;

View File

@ -18,6 +18,7 @@ using osu.Framework.Localisation;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays;
using osuTK; using osuTK;
namespace osu.Game.Screens.Edit.Setup namespace osu.Game.Screens.Edit.Setup
@ -118,6 +119,7 @@ namespace osu.Game.Screens.Edit.Setup
protected override string PopOutSampleName => "UI/overlay-big-pop-out"; protected override string PopOutSampleName => "UI/overlay-big-pop-out";
public FileChooserPopover(string[] handledExtensions, Bindable<FileInfo?> currentFile, string? chooserPath) public FileChooserPopover(string[] handledExtensions, Bindable<FileInfo?> currentFile, string? chooserPath)
: base(false)
{ {
Child = new Container Child = new Container
{ {
@ -129,6 +131,13 @@ namespace osu.Game.Screens.Edit.Setup
}, },
}; };
} }
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
Body.BorderColour = colourProvider.Highlight1;
Body.BorderThickness = 2;
}
} }
} }
} }

View File

@ -15,6 +15,7 @@ using osu.Framework.Screens;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays;
using osuTK; using osuTK;
namespace osu.Game.Screens.Import namespace osu.Game.Screens.Import
@ -36,8 +37,8 @@ namespace osu.Game.Screens.Import
[Resolved] [Resolved]
private OsuGameBase game { get; set; } private OsuGameBase game { get; set; }
[Resolved] [Cached]
private OsuColour colours { get; set; } private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader(true)]
private void load() private void load()
@ -52,11 +53,6 @@ namespace osu.Game.Screens.Import
Size = new Vector2(0.9f, 0.8f), Size = new Vector2(0.9f, 0.8f),
Children = new Drawable[] Children = new Drawable[]
{ {
new Box
{
Colour = colours.GreySeaFoamDark,
RelativeSizeAxes = Axes.Both,
},
fileSelector = new OsuFileSelector(validFileExtensions: game.HandledExtensions.ToArray()) fileSelector = new OsuFileSelector(validFileExtensions: game.HandledExtensions.ToArray())
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
@ -72,7 +68,7 @@ namespace osu.Game.Screens.Import
{ {
new Box new Box
{ {
Colour = colours.GreySeaFoamDarker, Colour = colourProvider.Background4,
RelativeSizeAxes = Axes.Both RelativeSizeAxes = Axes.Both
}, },
new Container new Container

View File

@ -9,6 +9,7 @@ using osu.Framework.Graphics.Containers;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Localisation.HUD; using osu.Game.Localisation.HUD;
using osu.Game.Localisation.SkinComponents;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
namespace osu.Game.Screens.Play.HUD namespace osu.Game.Screens.Play.HUD
@ -29,6 +30,9 @@ namespace osu.Game.Screens.Play.HUD
[SettingSource(typeof(SongProgressStrings), nameof(SongProgressStrings.ShowTime), nameof(SongProgressStrings.ShowTimeDescription))] [SettingSource(typeof(SongProgressStrings), nameof(SongProgressStrings.ShowTime), nameof(SongProgressStrings.ShowTimeDescription))]
public Bindable<bool> ShowTime { get; } = new BindableBool(true); public Bindable<bool> ShowTime { get; } = new BindableBool(true);
[SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Colour), nameof(SkinnableComponentStrings.ColourDescription))]
public BindableColour4 AccentColour { get; } = new BindableColour4(Colour4.White);
[Resolved] [Resolved]
private Player? player { get; set; } private Player? player { get; set; }
@ -94,6 +98,7 @@ namespace osu.Game.Screens.Play.HUD
Interactive.BindValueChanged(_ => bar.Interactive = Interactive.Value, true); Interactive.BindValueChanged(_ => bar.Interactive = Interactive.Value, true);
ShowGraph.BindValueChanged(_ => updateGraphVisibility(), true); ShowGraph.BindValueChanged(_ => updateGraphVisibility(), true);
ShowTime.BindValueChanged(_ => info.FadeTo(ShowTime.Value ? 1 : 0, 200, Easing.In), true); ShowTime.BindValueChanged(_ => info.FadeTo(ShowTime.Value ? 1 : 0, 200, Easing.In), true);
AccentColour.BindValueChanged(_ => Colour = AccentColour.Value, true);
} }
protected override void UpdateObjects(IEnumerable<HitObject> objects) protected override void UpdateObjects(IEnumerable<HitObject> objects)

View File

@ -9,6 +9,7 @@ using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Localisation.SkinComponents;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK; using osuTK;
@ -21,6 +22,9 @@ namespace osu.Game.Screens.Play.HUD
[SettingSource("Inverted shear")] [SettingSource("Inverted shear")]
public BindableBool InvertShear { get; } = new BindableBool(); public BindableBool InvertShear { get; } = new BindableBool();
[SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Colour), nameof(SkinnableComponentStrings.ColourDescription))]
public BindableColour4 AccentColour { get; } = new BindableColour4(Color4Extensions.FromHex("#66CCFF"));
public ArgonWedgePiece() public ArgonWedgePiece()
{ {
CornerRadius = 10f; CornerRadius = 10f;
@ -37,7 +41,6 @@ namespace osu.Game.Screens.Play.HUD
InternalChild = new Box InternalChild = new Box
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = ColourInfo.GradientVertical(Color4Extensions.FromHex("#66CCFF").Opacity(0.0f), Color4Extensions.FromHex("#66CCFF").Opacity(0.25f)),
}; };
} }
@ -46,6 +49,7 @@ namespace osu.Game.Screens.Play.HUD
base.LoadComplete(); base.LoadComplete();
InvertShear.BindValueChanged(v => Shear = new Vector2(0.8f, 0f) * (v.NewValue ? -1 : 1), true); InvertShear.BindValueChanged(v => Shear = new Vector2(0.8f, 0f) * (v.NewValue ? -1 : 1), true);
AccentColour.BindValueChanged(c => InternalChild.Colour = ColourInfo.GradientVertical(AccentColour.Value.Opacity(0.0f), AccentColour.Value.Opacity(0.25f)), true);
} }
} }
} }

View File

@ -10,6 +10,7 @@ using osu.Framework.Utils;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Localisation.HUD; using osu.Game.Localisation.HUD;
using osu.Game.Localisation.SkinComponents;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osuTK; using osuTK;
@ -36,6 +37,9 @@ namespace osu.Game.Screens.Play.HUD
[SettingSource(typeof(SongProgressStrings), nameof(SongProgressStrings.ShowTime), nameof(SongProgressStrings.ShowTimeDescription))] [SettingSource(typeof(SongProgressStrings), nameof(SongProgressStrings.ShowTime), nameof(SongProgressStrings.ShowTimeDescription))]
public Bindable<bool> ShowTime { get; } = new BindableBool(true); public Bindable<bool> ShowTime { get; } = new BindableBool(true);
[SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Colour), nameof(SkinnableComponentStrings.ColourDescription))]
public BindableColour4 AccentColour { get; } = new BindableColour4(Colour4.White);
[Resolved] [Resolved]
private Player? player { get; set; } private Player? player { get; set; }
@ -86,6 +90,7 @@ namespace osu.Game.Screens.Play.HUD
Interactive.BindValueChanged(_ => updateBarVisibility(), true); Interactive.BindValueChanged(_ => updateBarVisibility(), true);
ShowGraph.BindValueChanged(_ => updateGraphVisibility(), true); ShowGraph.BindValueChanged(_ => updateGraphVisibility(), true);
ShowTime.BindValueChanged(_ => updateTimeVisibility(), true); ShowTime.BindValueChanged(_ => updateTimeVisibility(), true);
AccentColour.BindValueChanged(_ => Colour = AccentColour.Value, true);
base.LoadComplete(); base.LoadComplete();
} }

View File

@ -3,6 +3,7 @@
using System.Diagnostics; using System.Diagnostics;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
@ -13,12 +14,11 @@ using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables.Cards; using osu.Game.Beatmaps.Drawables.Cards;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Database;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Localisation; using osu.Game.Localisation;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Spectator; using osu.Game.Online.Spectator;
using osu.Game.Overlays; using osu.Game.Overlays;
@ -34,7 +34,7 @@ namespace osu.Game.Screens.Play
public partial class SoloSpectatorScreen : SpectatorScreen, IPreviewTrackOwner public partial class SoloSpectatorScreen : SpectatorScreen, IPreviewTrackOwner
{ {
[Resolved] [Resolved]
private IAPIProvider api { get; set; } = null!; private BeatmapLookupCache beatmapLookupCache { get; set; } = null!;
[Resolved] [Resolved]
private PreviewTrackManager previewTrackManager { get; set; } = null!; private PreviewTrackManager previewTrackManager { get; set; } = null!;
@ -60,7 +60,7 @@ namespace osu.Game.Screens.Play
/// </summary> /// </summary>
private SpectatorGameplayState? immediateSpectatorGameplayState; private SpectatorGameplayState? immediateSpectatorGameplayState;
private GetBeatmapSetRequest? onlineBeatmapRequest; private ScheduledDelegate? beatmapFetchCallback;
private APIBeatmapSet? beatmapSet; private APIBeatmapSet? beatmapSet;
@ -210,7 +210,7 @@ namespace osu.Game.Screens.Play
private void clearDisplay() private void clearDisplay()
{ {
watchButton.Enabled.Value = false; watchButton.Enabled.Value = false;
onlineBeatmapRequest?.Cancel(); beatmapFetchCallback?.Cancel();
beatmapPanelContainer.Clear(); beatmapPanelContainer.Clear();
previewTrackManager.StopAnyPlaying(this); previewTrackManager.StopAnyPlaying(this);
} }
@ -244,15 +244,17 @@ namespace osu.Game.Screens.Play
{ {
Debug.Assert(state.BeatmapID != null); Debug.Assert(state.BeatmapID != null);
onlineBeatmapRequest = new GetBeatmapSetRequest(state.BeatmapID.Value, BeatmapSetLookupType.BeatmapId); beatmapLookupCache.GetBeatmapAsync(state.BeatmapID.Value).ContinueWith(t => beatmapFetchCallback = Schedule(() =>
onlineBeatmapRequest.Success += beatmapSet => Schedule(() =>
{ {
this.beatmapSet = beatmapSet; var beatmap = t.GetResultSafely();
beatmapPanelContainer.Child = new BeatmapCardNormal(this.beatmapSet, allowExpansion: false);
checkForAutomaticDownload();
});
api.Queue(onlineBeatmapRequest); if (beatmap?.BeatmapSet == null)
return;
beatmapSet = beatmap.BeatmapSet;
beatmapPanelContainer.Child = new BeatmapCardNormal(beatmapSet, allowExpansion: false);
checkForAutomaticDownload();
}));
} }
private void checkForAutomaticDownload() private void checkForAutomaticDownload()

View File

@ -234,9 +234,12 @@ namespace osu.Game.Screens.Play
{ {
if (LoadedBeatmapSuccessfully) if (LoadedBeatmapSuccessfully)
{ {
// compare: https://github.com/ppy/osu/blob/ccf1acce56798497edfaf92d3ece933469edcf0a/osu.Game/Screens/Play/Player.cs#L848-L851
var scoreCopy = Score.DeepClone();
Task.Run(async () => Task.Run(async () =>
{ {
await submitScore(Score.DeepClone()).ConfigureAwait(false); await submitScore(scoreCopy).ConfigureAwait(false);
spectatorClient.EndPlaying(GameplayState); spectatorClient.EndPlaying(GameplayState);
}).FireAndForget(); }).FireAndForget();
} }

View File

@ -37,7 +37,7 @@ namespace osu.Game.Screens.Ranking.Statistics
{ {
if (update.NewValue?.Score.MatchesOnlineID(achievedScore) == true) if (update.NewValue?.Score.MatchesOnlineID(achievedScore) == true)
DisplayedUserStatisticsUpdate.Value = update.NewValue; DisplayedUserStatisticsUpdate.Value = update.NewValue;
}); }, true);
} }
} }

View File

@ -123,6 +123,8 @@ namespace osu.Game.Skinning.Components
} }
protected override void SetFont(FontUsage font) => text.Font = font.With(size: 40); protected override void SetFont(FontUsage font) => text.Font = font.With(size: 40);
protected override void SetTextColour(Colour4 textColour) => text.Colour = textColour;
} }
// WARNING: DO NOT ADD ANY VALUES TO THIS ENUM ANYWHERE ELSE THAN AT THE END. // WARNING: DO NOT ADD ANY VALUES TO THIS ENUM ANYWHERE ELSE THAN AT THE END.

View File

@ -27,6 +27,9 @@ namespace osu.Game.Skinning.Components
Precision = 0.01f Precision = 0.01f
}; };
[SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Colour), nameof(SkinnableComponentStrings.ColourDescription))]
public BindableColour4 AccentColour { get; } = new BindableColour4(Colour4.White);
public BoxElement() public BoxElement()
{ {
Size = new Vector2(400, 80); Size = new Vector2(400, 80);
@ -43,6 +46,13 @@ namespace osu.Game.Skinning.Components
Masking = true; Masking = true;
} }
protected override void LoadComplete()
{
base.LoadComplete();
AccentColour.BindValueChanged(_ => Colour = AccentColour.Value, true);
}
protected override void Update() protected override void Update()
{ {
base.Update(); base.Update();

View File

@ -53,5 +53,7 @@ namespace osu.Game.Skinning.Components
} }
protected override void SetFont(FontUsage font) => text.Font = font.With(size: 40); protected override void SetFont(FontUsage font) => text.Font = font.With(size: 40);
protected override void SetTextColour(Colour4 textColour) => text.Colour = textColour;
} }
} }

View File

@ -36,5 +36,7 @@ namespace osu.Game.Skinning.Components
} }
protected override void SetFont(FontUsage font) => text.Font = font.With(size: 40); protected override void SetFont(FontUsage font) => text.Font = font.With(size: 40);
protected override void SetTextColour(Colour4 textColour) => text.Colour = textColour;
} }
} }

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Game.Configuration; using osu.Game.Configuration;
@ -20,11 +21,16 @@ namespace osu.Game.Skinning
[SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Font), nameof(SkinnableComponentStrings.FontDescription))] [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Font), nameof(SkinnableComponentStrings.FontDescription))]
public Bindable<Typeface> Font { get; } = new Bindable<Typeface>(Typeface.Torus); public Bindable<Typeface> Font { get; } = new Bindable<Typeface>(Typeface.Torus);
[SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.TextColour), nameof(SkinnableComponentStrings.TextColourDescription))]
public BindableColour4 TextColour { get; } = new BindableColour4(Colour4.White);
/// <summary> /// <summary>
/// Implement to apply the user font selection to one or more components. /// Implement to apply the user font selection to one or more components.
/// </summary> /// </summary>
protected abstract void SetFont(FontUsage font); protected abstract void SetFont(FontUsage font);
protected abstract void SetTextColour(Colour4 textColour);
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
@ -37,6 +43,8 @@ namespace osu.Game.Skinning
FontUsage f = OsuFont.GetFont(e.NewValue, weight: fontWeight); FontUsage f = OsuFont.GetFont(e.NewValue, weight: fontWeight);
SetFont(f); SetFont(f);
}, true); }, true);
TextColour.BindValueChanged(e => SetTextColour(e.NewValue), true);
} }
} }
} }

View File

@ -29,7 +29,10 @@ namespace osu.Game.Skinning
invalidateCache(); invalidateCache();
Debug.Assert(fileToStoragePathMapping != null); Debug.Assert(fileToStoragePathMapping != null);
realmSubscription = realm?.RegisterForNotifications(r => r.All<T>().Where(s => s.ID == source.ID), skinChanged); // Required local for iOS. Will cause runtime crash if inlined.
Guid id = source.ID;
realmSubscription = realm?.RegisterForNotifications(r => r.All<T>().Where(s => s.ID == id), skinChanged);
} }
protected override void Dispose(bool disposing) protected override void Dispose(bool disposing)

View File

@ -131,9 +131,12 @@ namespace osu.Game.Skinning
{ {
Realm.Run(r => Realm.Run(r =>
{ {
// Required local for iOS. Will cause runtime crash if inlined.
Guid currentSkinId = CurrentSkinInfo.Value.ID;
// choose from only user skins, removing the current selection to ensure a new one is chosen. // choose from only user skins, removing the current selection to ensure a new one is chosen.
var randomChoices = r.All<SkinInfo>() var randomChoices = r.All<SkinInfo>()
.Where(s => !s.DeletePending && s.ID != CurrentSkinInfo.Value.ID) .Where(s => !s.DeletePending && s.ID != currentSkinId)
.ToArray(); .ToArray();
if (randomChoices.Length == 0) if (randomChoices.Length == 0)

View File

@ -19,7 +19,7 @@ namespace osu.Game.Tests.Visual
public abstract partial class PlacementBlueprintTestScene : OsuManualInputManagerTestScene, IPlacementHandler public abstract partial class PlacementBlueprintTestScene : OsuManualInputManagerTestScene, IPlacementHandler
{ {
protected readonly Container HitObjectContainer; protected readonly Container HitObjectContainer;
protected PlacementBlueprint CurrentBlueprint { get; private set; } protected HitObjectPlacementBlueprint CurrentBlueprint { get; private set; }
protected PlacementBlueprintTestScene() protected PlacementBlueprintTestScene()
{ {
@ -87,14 +87,14 @@ namespace osu.Game.Tests.Visual
CurrentBlueprint.UpdateTimeAndPosition(SnapForBlueprint(CurrentBlueprint)); CurrentBlueprint.UpdateTimeAndPosition(SnapForBlueprint(CurrentBlueprint));
} }
protected virtual SnapResult SnapForBlueprint(PlacementBlueprint blueprint) => protected virtual SnapResult SnapForBlueprint(HitObjectPlacementBlueprint blueprint) =>
new SnapResult(InputManager.CurrentState.Mouse.Position, null); new SnapResult(InputManager.CurrentState.Mouse.Position, null);
public override void Add(Drawable drawable) public override void Add(Drawable drawable)
{ {
base.Add(drawable); base.Add(drawable);
if (drawable is PlacementBlueprint blueprint) if (drawable is HitObjectPlacementBlueprint blueprint)
{ {
blueprint.Show(); blueprint.Show();
blueprint.UpdateTimeAndPosition(SnapForBlueprint(blueprint)); blueprint.UpdateTimeAndPosition(SnapForBlueprint(blueprint));
@ -106,6 +106,6 @@ namespace osu.Game.Tests.Visual
protected virtual Container CreateHitObjectContainer() => new Container { RelativeSizeAxes = Axes.Both }; protected virtual Container CreateHitObjectContainer() => new Container { RelativeSizeAxes = Axes.Both };
protected abstract DrawableHitObject CreateHitObject(HitObject hitObject); protected abstract DrawableHitObject CreateHitObject(HitObject hitObject);
protected abstract PlacementBlueprint CreateBlueprint(); protected abstract HitObjectPlacementBlueprint CreateBlueprint();
} }
} }

View File

@ -0,0 +1,39 @@
// 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.Linq;
using System.Reflection;
using osu.Framework.Bindables;
using osu.Framework.Extensions.TypeExtensions;
namespace osu.Game.Utils
{
internal static class BindableValueAccessor
{
private static readonly MethodInfo get_method = typeof(BindableValueAccessor).GetMethod(nameof(getValue), BindingFlags.Static | BindingFlags.NonPublic)!;
private static readonly MethodInfo set_method = typeof(BindableValueAccessor).GetMethod(nameof(setValue), BindingFlags.Static | BindingFlags.NonPublic)!;
public static object GetValue(IBindable bindable)
{
Type? bindableWithValueType = bindable.GetType().GetInterfaces().FirstOrDefault(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IBindable<>));
if (bindableWithValueType == null)
return bindable;
return get_method.MakeGenericMethod(bindableWithValueType.GenericTypeArguments[0]).Invoke(null, [bindable])!;
}
public static void SetValue(IBindable bindable, object value)
{
Type? bindableWithValueType = bindable.GetType().EnumerateBaseTypes().SingleOrDefault(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Bindable<>));
if (bindableWithValueType == null)
return;
set_method.MakeGenericMethod(bindableWithValueType.GenericTypeArguments[0]).Invoke(null, [bindable, value]);
}
private static object getValue<T>(object bindable) => ((IBindable<T>)bindable).Value!;
private static object setValue<T>(object bindable, object value) => ((Bindable<T>)bindable).Value = (T)value;
}
}