1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-17 05:22:54 +08:00

Merge branch 'master' into shuffle

This commit is contained in:
Bartłomiej Dach 2024-10-01 10:07:54 +02:00
commit 8eb5e6b1d5
No known key found for this signature in database
153 changed files with 3370 additions and 693 deletions

View File

@ -135,5 +135,8 @@ jobs:
- name: Install .NET Workloads
run: dotnet workload install maui-ios
- name: Select Xcode 16
run: sudo xcode-select -s /Applications/Xcode_16.app/Contents/Developer
- name: Build
run: dotnet build -c Debug osu.iOS

View File

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

View File

@ -66,7 +66,7 @@ namespace osu.Desktop.Updater
{
Activated = () =>
{
restartToApplyUpdate();
Task.Run(restartToApplyUpdate);
return true;
}
});
@ -88,7 +88,11 @@ namespace osu.Desktop.Updater
{
notification = new UpdateProgressNotification
{
CompletionClickAction = restartToApplyUpdate,
CompletionClickAction = () =>
{
Task.Run(restartToApplyUpdate);
return true;
},
};
Schedule(() => notificationOverlay.Post(notification));
@ -127,13 +131,10 @@ namespace osu.Desktop.Updater
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).
// Currently there's an internal Thread.Sleep(300) which will cause a stutter when the user clicks to restart.
updateManager.WaitExitThenApplyUpdates(pendingUpdate?.TargetFullRelease);
await updateManager.WaitExitThenApplyUpdatesAsync(pendingUpdate?.TargetFullRelease).ConfigureAwait(false);
Schedule(() => game.AttemptExit());
return true;
}
}
}

View File

@ -26,7 +26,7 @@
<ItemGroup Label="Package References">
<PackageReference Include="System.IO.Packaging" Version="8.0.0" />
<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 Label="Resources">
<EmbeddedResource Include="lazer.ico" />

View File

@ -0,0 +1,29 @@
// 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 BenchmarkDotNet.Attributes;
using osu.Framework.Utils;
using osu.Game.Utils;
using osuTK;
namespace osu.Game.Benchmarks
{
public class BenchmarkGeometryUtils : BenchmarkTest
{
[Params(100, 1000, 2000, 4000, 8000, 10000)]
public int N;
private Vector2[] points = null!;
public override void SetUp()
{
points = new Vector2[N];
for (int i = 0; i < points.Length; ++i)
points[i] = new Vector2(RNG.Next(512), RNG.Next(384));
}
[Benchmark]
public void MinimumEnclosingCircle() => GeometryUtils.MinimumEnclosingCircle(points);
}
}

View File

@ -71,7 +71,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
contentContainer.Playfield.HitObjectContainer.Add(hitObject);
}
protected override SnapResult SnapForBlueprint(PlacementBlueprint blueprint)
protected override SnapResult SnapForBlueprint(HitObjectPlacementBlueprint blueprint)
{
var result = base.SnapForBlueprint(blueprint);
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 PlacementBlueprint CreateBlueprint() => new BananaShowerPlacementBlueprint();
protected override HitObjectPlacementBlueprint CreateBlueprint() => new BananaShowerPlacementBlueprint();
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 PlacementBlueprint CreateBlueprint() => new FruitPlacementBlueprint();
protected override HitObjectPlacementBlueprint CreateBlueprint() => new FruitPlacementBlueprint();
[Test]
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 PlacementBlueprint CreateBlueprint() => new JuiceStreamPlacementBlueprint();
protected override HitObjectPlacementBlueprint CreateBlueprint() => new JuiceStreamPlacementBlueprint();
private void addMoveAndClickSteps(double time, float position, bool end = false)
{

View File

@ -3,7 +3,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Beatmaps;
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,
Mods = mods,
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;

View File

@ -10,7 +10,7 @@ using osu.Game.Rulesets.Edit.Tools;
namespace osu.Game.Rulesets.Catch.Edit
{
public class BananaShowerCompositionTool : HitObjectCompositionTool
public class BananaShowerCompositionTool : CompositionTool
{
public BananaShowerCompositionTool()
: base(nameof(BananaShower))
@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Catch.Edit
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
{
public partial class CatchPlacementBlueprint<THitObject> : PlacementBlueprint
public partial class CatchPlacementBlueprint<THitObject> : HitObjectPlacementBlueprint
where THitObject : CatchHitObject, new()
{
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 IReadOnlyList<HitObjectCompositionTool> CompositionTools => new HitObjectCompositionTool[]
protected override IReadOnlyList<CompositionTool> CompositionTools => new CompositionTool[]
{
new FruitCompositionTool(),
new JuiceStreamCompositionTool(),
@ -114,6 +114,26 @@ namespace osu.Game.Rulesets.Catch.Edit
{
}
protected override bool OnKeyDown(KeyDownEvent e)
{
if (e.Repeat)
return false;
handleToggleViaKey(e);
return base.OnKeyDown(e);
}
protected override void OnKeyUp(KeyUpEvent e)
{
handleToggleViaKey(e);
base.OnKeyUp(e);
}
private void handleToggleViaKey(KeyboardEvent key)
{
DistanceSnapProvider.HandleToggleViaKey(key);
}
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
{
var result = base.FindSnappedPositionAndTime(screenSpacePosition, snapType);

View File

@ -10,7 +10,7 @@ using osu.Game.Rulesets.Edit.Tools;
namespace osu.Game.Rulesets.Catch.Edit
{
public class FruitCompositionTool : HitObjectCompositionTool
public class FruitCompositionTool : CompositionTool
{
public FruitCompositionTool()
: base(nameof(Fruit))
@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Catch.Edit
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
{
public class JuiceStreamCompositionTool : HitObjectCompositionTool
public class JuiceStreamCompositionTool : CompositionTool
{
public JuiceStreamCompositionTool()
: base(nameof(JuiceStream))
@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Catch.Edit
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);
var pos = column.ScreenSpacePositionAtTime(time);

View File

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

View File

@ -6,6 +6,7 @@ using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Visual;
@ -92,5 +93,30 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
AddAssert("second object flipped", () => second.StartTime, () => Is.EqualTo(250));
AddAssert("third object flipped", () => third.StartTime, () => Is.EqualTo(1250));
}
[Test]
public void TestOffScreenObjectsRemainSelectedOnColumnChange()
{
AddStep("create objects", () =>
{
for (int i = 0; i < 20; ++i)
EditorBeatmap.Add(new Note { StartTime = 1000 * i, Column = 0 });
});
AddStep("select everything", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
AddStep("start drag", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<Column>().First());
InputManager.PressButton(MouseButton.Left);
});
AddStep("end drag", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<Column>().Last());
InputManager.ReleaseButton(MouseButton.Left);
});
AddAssert("all objects in last column", () => EditorBeatmap.HitObjects.All(ho => ((ManiaHitObject)ho).Column == 3));
AddAssert("all objects remain selected", () => EditorBeatmap.SelectedHitObjects.SequenceEqual(EditorBeatmap.HitObjects));
}
}
}

View File

@ -64,6 +64,6 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
private Note getNote() => this.ChildrenOfType<DrawableNote>().FirstOrDefault()?.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
{
public abstract partial class ManiaPlacementBlueprint<T> : PlacementBlueprint
public abstract partial class ManiaPlacementBlueprint<T> : HitObjectPlacementBlueprint
where T : ManiaHitObject
{
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
{
public class HoldNoteCompositionTool : HitObjectCompositionTool
public class HoldNoteCompositionTool : CompositionTool
{
public HoldNoteCompositionTool()
: base("Hold")
@ -18,6 +18,6 @@ namespace osu.Game.Rulesets.Mania.Edit
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 IReadOnlyList<HitObjectCompositionTool> CompositionTools => new HitObjectCompositionTool[]
protected override IReadOnlyList<CompositionTool> CompositionTools => new CompositionTool[]
{
new NoteCompositionTool(),
new HoldNoteCompositionTool()

View File

@ -104,8 +104,10 @@ namespace osu.Game.Rulesets.Mania.Edit
int minColumn = int.MaxValue;
int maxColumn = int.MinValue;
var selectedObjects = EditorBeatmap.SelectedHitObjects.OfType<ManiaHitObject>().ToArray();
// find min/max in an initial pass before actually performing the movement.
foreach (var obj in EditorBeatmap.SelectedHitObjects.OfType<ManiaHitObject>())
foreach (var obj in selectedObjects)
{
if (obj.Column < minColumn)
minColumn = obj.Column;
@ -121,6 +123,13 @@ namespace osu.Game.Rulesets.Mania.Edit
((ManiaHitObject)h).Column += columnDelta;
maniaPlayfield.Add(h);
});
// `HitObjectUsageEventBuffer`'s usage transferal flows and the playfield's `SetKeepAlive()` functionality do not combine well with this operation's usage pattern,
// leading to selections being sometimes partially dropped if some of the objects being moved are off screen
// (check blame for detailed explanation).
// thus, ensure that selection is preserved manually.
EditorBeatmap.SelectedHitObjects.Clear();
EditorBeatmap.SelectedHitObjects.AddRange(selectedObjects);
}
}
}

View File

@ -10,7 +10,7 @@ using osu.Game.Rulesets.Mania.Objects;
namespace osu.Game.Rulesets.Mania.Edit
{
public class NoteCompositionTool : HitObjectCompositionTool
public class NoteCompositionTool : CompositionTool
{
public NoteCompositionTool()
: base(nameof(Note))
@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Mania.Edit
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
{
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableHitCircle((HitCircle)hitObject);
protected override PlacementBlueprint CreateBlueprint() => new HitCirclePlacementBlueprint();
protected override HitObjectPlacementBlueprint CreateBlueprint() => new HitCirclePlacementBlueprint();
}
}

View File

@ -116,7 +116,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
() => EditorBeatmap.HitObjects.OfType<HitCircle>().ElementAt(1).Position,
() => Is.EqualTo(OsuPlayfield.BASE_SIZE - new Vector2(200)));
AddStep("change rotation origin", () => getPopover().ChildrenOfType<EditorRadioButton>().ElementAt(1).TriggerClick());
AddStep("change rotation origin", () => getPopover().ChildrenOfType<EditorRadioButton>().ElementAt(2).TriggerClick());
AddAssert("first object rotated 90deg around selection centre",
() => EditorBeatmap.HitObjects.OfType<HitCircle>().ElementAt(0).Position, () => Is.EqualTo(new Vector2(200, 200)));
AddAssert("second object rotated 90deg around selection centre",

View File

@ -514,6 +514,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
private Slider? getSlider() => HitObjectContainer.Count > 0 ? ((DrawableSlider)HitObjectContainer[0]).HitObject : null;
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 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 drainRate = beatmap.Difficulty.DrainRate;
int maxCombo = beatmap.GetMaxCombo();
int hitCirclesCount = beatmap.HitObjects.Count(h => h is HitCircle);
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,
OverallDifficulty = (80 - hitWindowGreat) / 6,
DrainRate = drainRate,
MaxCombo = maxCombo,
MaxCombo = beatmap.GetMaxCombo(),
HitCircleCount = hitCirclesCount,
SliderCount = sliderCount,
SpinnerCount = spinnerCount,

View File

@ -9,7 +9,7 @@ using osuTK.Input;
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;

View File

@ -353,6 +353,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{
changeHandler?.BeginChange();
double originalDistance = hitObject.Path.Distance;
foreach (var p in Pieces.Where(p => p.IsSelected.Value))
{
var pointsInSegment = hitObject.Path.PointsInSegment(p.ControlPoint);
@ -375,6 +377,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
EnsureValidPathTypes();
if (hitObject.Path.Distance < originalDistance)
hitObject.SnapTo(distanceSnapProvider);
else
hitObject.Path.ExpectedDistance.Value = originalDistance;
changeHandler?.EndChange();
}

View File

@ -21,7 +21,7 @@ using osuTK.Input;
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;
@ -401,7 +401,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
if (state == SliderPlacementState.Drawing)
HitObject.Path.ExpectedDistance.Value = (float)HitObject.Path.CalculatedDistance;
else
HitObject.Path.ExpectedDistance.Value = distanceSnapProvider?.FindSnappedDistance(HitObject, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance;
HitObject.Path.ExpectedDistance.Value = distanceSnapProvider?.FindSnappedDistance(HitObject, (float)HitObject.Path.CalculatedDistance, DistanceSnapTarget.Start) ?? (float)HitObject.Path.CalculatedDistance;
bodyPiece.UpdateFrom(HitObject);
headCirclePiece.UpdateFrom(HitObject.HeadCircle);

View File

@ -269,7 +269,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{
double minDistance = distanceSnapProvider?.GetBeatSnapDistanceAt(HitObject, false) * oldVelocityMultiplier ?? 1;
// Add a small amount to the proposed distance to make it easier to snap to the full length of the slider.
proposedDistance = distanceSnapProvider?.FindSnappedDistance(HitObject, (float)proposedDistance + 1) ?? proposedDistance;
proposedDistance = distanceSnapProvider?.FindSnappedDistance(HitObject, (float)proposedDistance + 1, DistanceSnapTarget.Start) ?? proposedDistance;
proposedDistance = MathHelper.Clamp(proposedDistance, minDistance, HitObject.Path.CalculatedDistance);
}

View File

@ -13,7 +13,7 @@ using osuTK.Input;
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;

View File

@ -10,7 +10,7 @@ using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Rulesets.Osu.Edit
{
public class HitCircleCompositionTool : HitObjectCompositionTool
public class HitCircleCompositionTool : CompositionTool
{
public HitCircleCompositionTool()
: base(nameof(HitCircle))
@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Osu.Edit
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)
=> new DrawableOsuEditorRuleset(ruleset, beatmap, mods);
protected override IReadOnlyList<HitObjectCompositionTool> CompositionTools => new HitObjectCompositionTool[]
protected override IReadOnlyList<CompositionTool> CompositionTools => new CompositionTool[]
{
new HitCircleCompositionTool(),
new SliderCompositionTool(),
@ -106,6 +106,7 @@ namespace osu.Game.Rulesets.Osu.Edit
{
RotationHandler = BlueprintContainer.SelectionHandler.RotationHandler,
ScaleHandler = (OsuSelectionScaleHandler)BlueprintContainer.SelectionHandler.ScaleHandler,
GridToolbox = OsuGridToolboxGroup,
},
new GenerateToolboxGroup(),
FreehandSliderToolboxGroup
@ -368,6 +369,8 @@ namespace osu.Game.Rulesets.Osu.Edit
gridSnapMomentary = shiftPressed;
rectangularGridSnapToggle.Value = rectangularGridSnapToggle.Value == TernaryState.False ? TernaryState.True : TernaryState.False;
}
DistanceSnapProvider.HandleToggleViaKey(key);
}
private DistanceSnapGrid createDistanceSnapGrid(IEnumerable<HitObject> selectedHitObjects)

View File

@ -3,6 +3,7 @@
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.UserInterface;
@ -25,6 +26,9 @@ namespace osu.Game.Rulesets.Osu.Edit
{
public partial class OsuSelectionHandler : EditorSelectionHandler
{
[Resolved]
private OsuGridToolboxGroup gridToolbox { get; set; } = null!;
protected override void OnSelectionChanged()
{
base.OnSelectionChanged();
@ -123,13 +127,43 @@ namespace osu.Game.Rulesets.Osu.Edit
{
var hitObjects = selectedMovableObjects;
var flipQuad = flipOverOrigin ? new Quad(0, 0, OsuPlayfield.BASE_SIZE.X, OsuPlayfield.BASE_SIZE.Y) : GeometryUtils.GetSurroundingQuad(hitObjects);
// If we're flipping over the origin, we take the grid origin position from the grid toolbox.
var flipQuad = flipOverOrigin ? new Quad(gridToolbox.StartPositionX.Value, gridToolbox.StartPositionY.Value, 0, 0) : GeometryUtils.GetSurroundingQuad(hitObjects);
Vector2 flipAxis = direction == Direction.Vertical ? Vector2.UnitY : Vector2.UnitX;
if (flipOverOrigin)
{
// If we're flipping over the origin, we take one of the axes of the grid.
// Take the axis closest to the direction we want to flip over.
switch (gridToolbox.GridType.Value)
{
case PositionSnapGridType.Square:
flipAxis = GeometryUtils.RotateVector(Vector2.UnitX, -((gridToolbox.GridLinesRotation.Value + 360 + 45) % 90 - 45));
flipAxis = direction == Direction.Vertical ? flipAxis.PerpendicularLeft : flipAxis;
break;
case PositionSnapGridType.Triangle:
// Hex grid has 3 axes, so you can not directly flip over one of the axes,
// however it's still possible to achieve that flip by combining multiple flips over the other axes.
// Angle degree range for vertical = (-120, -60]
// Angle degree range for horizontal = [-30, 30)
flipAxis = direction == Direction.Vertical
? GeometryUtils.RotateVector(Vector2.UnitX, -((gridToolbox.GridLinesRotation.Value + 360 + 30) % 60 + 60))
: GeometryUtils.RotateVector(Vector2.UnitX, -((gridToolbox.GridLinesRotation.Value + 360) % 60 - 30));
break;
}
}
var controlPointFlipQuad = new Quad();
bool didFlip = false;
foreach (var h in hitObjects)
{
var flippedPosition = GeometryUtils.GetFlippedPosition(direction, flipQuad, h.Position);
var flippedPosition = GeometryUtils.GetFlippedPosition(flipAxis, flipQuad, h.Position);
// Clamp the flipped position inside the playfield bounds, because the flipped position might be outside the playfield bounds if the origin is not centered.
flippedPosition = Vector2.Clamp(flippedPosition, Vector2.Zero, OsuPlayfield.BASE_SIZE);
if (!Precision.AlmostEquals(flippedPosition, h.Position))
{
@ -142,12 +176,7 @@ namespace osu.Game.Rulesets.Osu.Edit
didFlip = true;
foreach (var cp in slider.Path.ControlPoints)
{
cp.Position = new Vector2(
(direction == Direction.Horizontal ? -1 : 1) * cp.Position.X,
(direction == Direction.Vertical ? -1 : 1) * cp.Position.Y
);
}
cp.Position = GeometryUtils.GetFlippedPosition(flipAxis, controlPointFlipQuad, cp.Position);
}
}

View File

@ -47,7 +47,6 @@ namespace osu.Game.Rulesets.Osu.Edit
private OsuHitObject[]? objectsInRotation;
private Vector2? defaultOrigin;
private Dictionary<OsuHitObject, Vector2>? originalPositions;
private Dictionary<IHasPath, Vector2[]>? originalPathControlPointPositions;
@ -61,7 +60,7 @@ namespace osu.Game.Rulesets.Osu.Edit
changeHandler?.BeginChange();
objectsInRotation = selectedMovableObjects.ToArray();
defaultOrigin = GeometryUtils.GetSurroundingQuad(objectsInRotation).Centre;
DefaultOrigin = GeometryUtils.MinimumEnclosingCircle(objectsInRotation).Item1;
originalPositions = objectsInRotation.ToDictionary(obj => obj, obj => obj.Position);
originalPathControlPointPositions = objectsInRotation.OfType<IHasPath>().ToDictionary(
obj => obj,
@ -73,9 +72,9 @@ namespace osu.Game.Rulesets.Osu.Edit
if (!OperationInProgress.Value)
throw new InvalidOperationException($"Cannot {nameof(Update)} a rotate operation without calling {nameof(Begin)} first!");
Debug.Assert(objectsInRotation != null && originalPositions != null && originalPathControlPointPositions != null && defaultOrigin != null);
Debug.Assert(objectsInRotation != null && originalPositions != null && originalPathControlPointPositions != null && DefaultOrigin != null);
Vector2 actualOrigin = origin ?? defaultOrigin.Value;
Vector2 actualOrigin = origin ?? DefaultOrigin.Value;
foreach (var ho in objectsInRotation)
{
@ -103,7 +102,7 @@ namespace osu.Game.Rulesets.Osu.Edit
objectsInRotation = null;
originalPositions = null;
originalPathControlPointPositions = null;
defaultOrigin = null;
DefaultOrigin = null;
}
private IEnumerable<OsuHitObject> selectedMovableObjects => selectedItems.Cast<OsuHitObject>()

View File

@ -69,6 +69,7 @@ namespace osu.Game.Rulesets.Osu.Edit
private Dictionary<OsuHitObject, OriginalHitObjectState>? objectsInScale;
private Vector2? defaultOrigin;
private List<Vector2>? originalConvexHull;
public override void Begin()
{
@ -83,10 +84,13 @@ namespace osu.Game.Rulesets.Osu.Edit
OriginalSurroundingQuad = objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider
? GeometryUtils.GetSurroundingQuad(slider.Path.ControlPoints.Select(p => slider.Position + p.Position))
: GeometryUtils.GetSurroundingQuad(objectsInScale.Keys);
defaultOrigin = OriginalSurroundingQuad.Value.Centre;
originalConvexHull = objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider2
? GeometryUtils.GetConvexHull(slider2.Path.ControlPoints.Select(p => slider2.Position + p.Position))
: GeometryUtils.GetConvexHull(objectsInScale.Keys);
defaultOrigin = GeometryUtils.MinimumEnclosingCircle(originalConvexHull).Item1;
}
public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both)
public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both, float axisRotation = 0)
{
if (!OperationInProgress.Value)
throw new InvalidOperationException($"Cannot {nameof(Update)} a scale operation without calling {nameof(Begin)} first!");
@ -94,23 +98,22 @@ namespace osu.Game.Rulesets.Osu.Edit
Debug.Assert(objectsInScale != null && defaultOrigin != null && OriginalSurroundingQuad != null);
Vector2 actualOrigin = origin ?? defaultOrigin.Value;
scale = clampScaleToAdjustAxis(scale, adjustAxis);
// for the time being, allow resizing of slider paths only if the slider is
// the only hit object selected. with a group selection, it's likely the user
// 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)
{
var originalInfo = objectsInScale[slider];
Debug.Assert(originalInfo.PathControlPointPositions != null && originalInfo.PathControlPointTypes != null);
scaleSlider(slider, scale, originalInfo.PathControlPointPositions, originalInfo.PathControlPointTypes);
scaleSlider(slider, scale, actualOrigin, objectsInScale[slider], axisRotation);
}
else
{
scale = ClampScaleToPlayfieldBounds(scale, actualOrigin);
scale = ClampScaleToPlayfieldBounds(scale, actualOrigin, adjustAxis, axisRotation);
foreach (var (ho, originalState) in objectsInScale)
{
ho.Position = GeometryUtils.GetScaledPosition(scale, actualOrigin, originalState.Position);
ho.Position = GeometryUtils.GetScaledPosition(scale, actualOrigin, originalState.Position, axisRotation);
}
}
@ -134,21 +137,45 @@ namespace osu.Game.Rulesets.Osu.Edit
private IEnumerable<OsuHitObject> selectedMovableObjects => selectedItems.Cast<OsuHitObject>()
.Where(h => h is not Spinner);
private void scaleSlider(Slider slider, Vector2 scale, Vector2[] originalPathPositions, PathType?[] originalPathTypes)
private Vector2 clampScaleToAdjustAxis(Vector2 scale, Axes adjustAxis)
{
switch (adjustAxis)
{
case Axes.Y:
scale.X = 1;
break;
case Axes.X:
scale.Y = 1;
break;
case Axes.None:
scale = Vector2.One;
break;
}
return scale;
}
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));
// 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++)
{
slider.Path.ControlPoints[i].Position = originalPathPositions[i] * scale;
slider.Path.ControlPoints[i].Type = originalPathTypes[i];
slider.Path.ControlPoints[i].Position = GeometryUtils.GetScaledPosition(scale, Vector2.Zero, originalInfo.PathControlPointPositions[i], axisRotation);
slider.Path.ControlPoints[i].Type = originalInfo.PathControlPointTypes[i];
}
// Snap the slider's length to the current beat divisor
// to calculate the final resulting duration / bounding box before the final checks.
slider.SnapTo(snapProvider);
slider.Position = GeometryUtils.GetScaledPosition(scale, origin, originalInfo.Position, axisRotation);
//if sliderhead or sliderend end up outside playfield, revert scaling.
Quad scaledQuad = GeometryUtils.GetSurroundingQuad(new OsuHitObject[] { slider });
(bool xInBounds, bool yInBounds) = isQuadInBounds(scaledQuad);
@ -157,7 +184,9 @@ namespace osu.Game.Rulesets.Osu.Edit
return;
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.
slider.SnapTo(snapProvider);
@ -176,11 +205,13 @@ namespace osu.Game.Rulesets.Osu.Edit
/// </summary>
/// <param name="origin">The origin from which the scale operation is performed</param>
/// <param name="scale">The scale to be clamped</param>
/// <param name="adjustAxis">The axes to adjust the scale in.</param>
/// <param name="axisRotation">The rotation of the axes in degrees</param>
/// <returns>The clamped scale vector</returns>
public Vector2 ClampScaleToPlayfieldBounds(Vector2 scale, Vector2? origin = null)
public Vector2 ClampScaleToPlayfieldBounds(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both, float axisRotation = 0)
{
//todo: this is not always correct for selections involving sliders. This approximation assumes each point is scaled independently, but sliderends move with the sliderhead.
if (objectsInScale == null)
if (objectsInScale == null || adjustAxis == Axes.None)
return scale;
Debug.Assert(defaultOrigin != null && OriginalSurroundingQuad != null);
@ -188,24 +219,60 @@ namespace osu.Game.Rulesets.Osu.Edit
if (objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider)
origin = slider.Position;
float cos = MathF.Cos(float.DegreesToRadians(-axisRotation));
float sin = MathF.Sin(float.DegreesToRadians(-axisRotation));
scale = clampScaleToAdjustAxis(scale, adjustAxis);
Vector2 actualOrigin = origin ?? defaultOrigin.Value;
var selectionQuad = OriginalSurroundingQuad.Value;
IEnumerable<Vector2> points;
var tl1 = Vector2.Divide(-actualOrigin, selectionQuad.TopLeft - actualOrigin);
var tl2 = Vector2.Divide(OsuPlayfield.BASE_SIZE - actualOrigin, selectionQuad.TopLeft - actualOrigin);
var br1 = Vector2.Divide(-actualOrigin, selectionQuad.BottomRight - actualOrigin);
var br2 = Vector2.Divide(OsuPlayfield.BASE_SIZE - actualOrigin, selectionQuad.BottomRight - actualOrigin);
if (axisRotation == 0)
{
var selectionQuad = OriginalSurroundingQuad.Value;
points = new[]
{
selectionQuad.TopLeft,
selectionQuad.TopRight,
selectionQuad.BottomLeft,
selectionQuad.BottomRight
};
}
else
points = originalConvexHull!;
if (!Precision.AlmostEquals(selectionQuad.TopLeft.X - actualOrigin.X, 0))
scale.X = selectionQuad.TopLeft.X - actualOrigin.X < 0 ? MathHelper.Clamp(scale.X, tl2.X, tl1.X) : MathHelper.Clamp(scale.X, tl1.X, tl2.X);
if (!Precision.AlmostEquals(selectionQuad.TopLeft.Y - actualOrigin.Y, 0))
scale.Y = selectionQuad.TopLeft.Y - actualOrigin.Y < 0 ? MathHelper.Clamp(scale.Y, tl2.Y, tl1.Y) : MathHelper.Clamp(scale.Y, tl1.Y, tl2.Y);
if (!Precision.AlmostEquals(selectionQuad.BottomRight.X - actualOrigin.X, 0))
scale.X = selectionQuad.BottomRight.X - actualOrigin.X < 0 ? MathHelper.Clamp(scale.X, br2.X, br1.X) : MathHelper.Clamp(scale.X, br1.X, br2.X);
if (!Precision.AlmostEquals(selectionQuad.BottomRight.Y - actualOrigin.Y, 0))
scale.Y = selectionQuad.BottomRight.Y - actualOrigin.Y < 0 ? MathHelper.Clamp(scale.Y, br2.Y, br1.Y) : MathHelper.Clamp(scale.Y, br1.Y, br2.Y);
foreach (var point in points)
{
scale = clampToBound(scale, point, Vector2.Zero);
scale = clampToBound(scale, point, OsuPlayfield.BASE_SIZE);
}
return Vector2.ComponentMax(scale, new Vector2(Precision.FLOAT_EPSILON));
float minPositiveComponent(Vector2 v) => MathF.Min(v.X < 0 ? float.PositiveInfinity : v.X, v.Y < 0 ? float.PositiveInfinity : v.Y);
Vector2 clampToBound(Vector2 s, Vector2 p, Vector2 bound)
{
p -= actualOrigin;
bound -= actualOrigin;
var a = new Vector2(cos * cos * p.X - sin * cos * p.Y, -sin * cos * p.X + sin * sin * p.Y);
var b = new Vector2(sin * sin * p.X + sin * cos * p.Y, sin * cos * p.X + cos * cos * p.Y);
switch (adjustAxis)
{
case Axes.X:
s.X = MathF.Min(scale.X, minPositiveComponent(Vector2.Divide(bound - b, a)));
break;
case Axes.Y:
s.Y = MathF.Min(scale.Y, minPositiveComponent(Vector2.Divide(bound - a, b)));
break;
case Axes.Both:
s = Vector2.ComponentMin(s, s * minPositiveComponent(Vector2.Divide(bound, a * s.X + b * s.Y)));
break;
}
return s;
}
}
private void moveSelectionInBounds()

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@ -19,16 +20,19 @@ namespace osu.Game.Rulesets.Osu.Edit
{
private readonly SelectionRotationHandler rotationHandler;
private readonly Bindable<PreciseRotationInfo> rotationInfo = new Bindable<PreciseRotationInfo>(new PreciseRotationInfo(0, RotationOrigin.PlayfieldCentre));
private readonly OsuGridToolboxGroup gridToolbox;
private readonly Bindable<PreciseRotationInfo> rotationInfo = new Bindable<PreciseRotationInfo>(new PreciseRotationInfo(0, RotationOrigin.GridCentre));
private SliderWithTextBoxInput<float> angleInput = null!;
private EditorRadioButtonCollection rotationOrigin = null!;
private RadioButton selectionCentreButton = null!;
public PreciseRotationPopover(SelectionRotationHandler rotationHandler)
public PreciseRotationPopover(SelectionRotationHandler rotationHandler, OsuGridToolboxGroup gridToolbox)
{
this.rotationHandler = rotationHandler;
this.gridToolbox = gridToolbox;
AllowableAnchors = new[] { Anchor.CentreLeft, Anchor.CentreRight };
}
@ -58,6 +62,9 @@ namespace osu.Game.Rulesets.Osu.Edit
RelativeSizeAxes = Axes.X,
Items = new[]
{
new RadioButton("Grid centre",
() => rotationInfo.Value = rotationInfo.Value with { Origin = RotationOrigin.GridCentre },
() => new SpriteIcon { Icon = FontAwesome.Regular.PlusSquare }),
new RadioButton("Playfield centre",
() => rotationInfo.Value = rotationInfo.Value with { Origin = RotationOrigin.PlayfieldCentre },
() => new SpriteIcon { Icon = FontAwesome.Regular.Square }),
@ -93,10 +100,19 @@ namespace osu.Game.Rulesets.Osu.Edit
rotationInfo.BindValueChanged(rotation =>
{
rotationHandler.Update(rotation.NewValue.Degrees, rotation.NewValue.Origin == RotationOrigin.PlayfieldCentre ? OsuPlayfield.BASE_SIZE / 2 : null);
rotationHandler.Update(rotation.NewValue.Degrees, getOriginPosition(rotation.NewValue));
});
}
private Vector2? getOriginPosition(PreciseRotationInfo rotation) =>
rotation.Origin switch
{
RotationOrigin.GridCentre => gridToolbox.StartPosition.Value,
RotationOrigin.PlayfieldCentre => OsuPlayfield.BASE_SIZE / 2,
RotationOrigin.SelectionCentre => null,
_ => throw new ArgumentOutOfRangeException(nameof(rotation))
};
protected override void PopIn()
{
base.PopIn();
@ -114,6 +130,7 @@ namespace osu.Game.Rulesets.Osu.Edit
public enum RotationOrigin
{
GridCentre,
PlayfieldCentre,
SelectionCentre
}

View File

@ -10,7 +10,10 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
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.Screens.Edit;
using osu.Game.Screens.Edit.Components.RadioButtons;
using osuTK;
@ -20,28 +23,36 @@ namespace osu.Game.Rulesets.Osu.Edit
{
private readonly OsuSelectionScaleHandler scaleHandler;
private readonly Bindable<PreciseScaleInfo> scaleInfo = new Bindable<PreciseScaleInfo>(new PreciseScaleInfo(1, ScaleOrigin.PlayfieldCentre, true, true));
private readonly OsuGridToolboxGroup gridToolbox;
private readonly Bindable<PreciseScaleInfo> scaleInfo = new Bindable<PreciseScaleInfo>(new PreciseScaleInfo(1, ScaleOrigin.GridCentre, true, true));
private SliderWithTextBoxInput<float> scaleInput = null!;
private BindableNumber<float> scaleInputBindable = null!;
private EditorRadioButtonCollection scaleOrigin = null!;
private RadioButton gridCentreButton = null!;
private RadioButton playfieldCentreButton = null!;
private RadioButton selectionCentreButton = null!;
private OsuCheckbox xCheckBox = null!;
private OsuCheckbox yCheckBox = null!;
public PreciseScalePopover(OsuSelectionScaleHandler scaleHandler)
private BindableList<HitObject> selectedItems { get; } = new BindableList<HitObject>();
public PreciseScalePopover(OsuSelectionScaleHandler scaleHandler, OsuGridToolboxGroup gridToolbox)
{
this.scaleHandler = scaleHandler;
this.gridToolbox = gridToolbox;
AllowableAnchors = new[] { Anchor.CentreLeft, Anchor.CentreRight };
}
[BackgroundDependencyLoader]
private void load()
private void load(EditorBeatmap editorBeatmap)
{
selectedItems.BindTo(editorBeatmap.SelectedHitObjects);
Child = new FillFlowContainer
{
Width = 220,
@ -66,6 +77,9 @@ namespace osu.Game.Rulesets.Osu.Edit
RelativeSizeAxes = Axes.X,
Items = new[]
{
gridCentreButton = new RadioButton("Grid centre",
() => setOrigin(ScaleOrigin.GridCentre),
() => new SpriteIcon { Icon = FontAwesome.Regular.PlusSquare }),
playfieldCentreButton = new RadioButton("Playfield centre",
() => setOrigin(ScaleOrigin.PlayfieldCentre),
() => new SpriteIcon { Icon = FontAwesome.Regular.Square }),
@ -97,6 +111,10 @@ namespace osu.Game.Rulesets.Osu.Edit
},
}
};
gridCentreButton.Selected.DisabledChanged += isDisabled =>
{
gridCentreButton.TooltipText = isDisabled ? "The current selection cannot be scaled relative to grid centre." : string.Empty;
};
playfieldCentreButton.Selected.DisabledChanged += isDisabled =>
{
playfieldCentreButton.TooltipText = isDisabled ? "The current selection cannot be scaled relative to playfield centre." : string.Empty;
@ -123,19 +141,20 @@ namespace osu.Game.Rulesets.Osu.Edit
selectionCentreButton.Selected.Disabled = !(scaleHandler.CanScaleX.Value || scaleHandler.CanScaleY.Value);
playfieldCentreButton.Selected.Disabled = scaleHandler.IsScalingSlider.Value && !selectionCentreButton.Selected.Disabled;
gridCentreButton.Selected.Disabled = playfieldCentreButton.Selected.Disabled;
scaleOrigin.Items.First(b => !b.Selected.Disabled).Select();
scaleInfo.BindValueChanged(scale =>
{
var newScale = new Vector2(scale.NewValue.XAxis ? scale.NewValue.Scale : 1, scale.NewValue.YAxis ? scale.NewValue.Scale : 1);
scaleHandler.Update(newScale, getOriginPosition(scale.NewValue));
var newScale = new Vector2(scale.NewValue.Scale, scale.NewValue.Scale);
scaleHandler.Update(newScale, getOriginPosition(scale.NewValue), getAdjustAxis(scale.NewValue), getRotation(scale.NewValue));
});
}
private void updateAxisCheckBoxesEnabled()
{
if (scaleInfo.Value.Origin == ScaleOrigin.PlayfieldCentre)
if (scaleInfo.Value.Origin != ScaleOrigin.SelectionCentre)
{
toggleAxisAvailable(xCheckBox.Current, true);
toggleAxisAvailable(yCheckBox.Current, true);
@ -162,7 +181,7 @@ namespace osu.Game.Rulesets.Osu.Edit
return;
const float max_scale = 10;
var scale = scaleHandler.ClampScaleToPlayfieldBounds(new Vector2(max_scale), getOriginPosition(scaleInfo.Value));
var scale = scaleHandler.ClampScaleToPlayfieldBounds(new Vector2(max_scale), getOriginPosition(scaleInfo.Value), getAdjustAxis(scaleInfo.Value), getRotation(scaleInfo.Value));
if (!scaleInfo.Value.XAxis)
scale.X = max_scale;
@ -179,7 +198,30 @@ namespace osu.Game.Rulesets.Osu.Edit
updateAxisCheckBoxesEnabled();
}
private Vector2? getOriginPosition(PreciseScaleInfo scale) => scale.Origin == ScaleOrigin.PlayfieldCentre ? OsuPlayfield.BASE_SIZE / 2 : null;
private Vector2? getOriginPosition(PreciseScaleInfo scale)
{
switch (scale.Origin)
{
case ScaleOrigin.GridCentre:
return gridToolbox.StartPosition.Value;
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 float getRotation(PreciseScaleInfo scale) => scale.Origin == ScaleOrigin.GridCentre ? gridToolbox.GridLinesRotation.Value : 0;
private void setAxis(bool x, bool y)
{
@ -204,6 +246,7 @@ namespace osu.Game.Rulesets.Osu.Edit
public enum ScaleOrigin
{
GridCentre,
PlayfieldCentre,
SelectionCentre
}

View File

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

View File

@ -27,6 +27,8 @@ namespace osu.Game.Rulesets.Osu.Edit
public SelectionRotationHandler RotationHandler { get; init; } = null!;
public OsuSelectionScaleHandler ScaleHandler { get; init; } = null!;
public OsuGridToolboxGroup GridToolbox { get; init; } = null!;
public TransformToolboxGroup()
: base("transform")
{
@ -44,10 +46,10 @@ namespace osu.Game.Rulesets.Osu.Edit
{
rotateButton = new EditorToolButton("Rotate",
() => new SpriteIcon { Icon = FontAwesome.Solid.Undo },
() => new PreciseRotationPopover(RotationHandler)),
() => new PreciseRotationPopover(RotationHandler, GridToolbox)),
scaleButton = new EditorToolButton("Scale",
() => new SpriteIcon { Icon = FontAwesome.Solid.ArrowsAlt },
() => new PreciseScalePopover(ScaleHandler))
() => new PreciseScalePopover(ScaleHandler, GridToolbox))
}
};
}

View File

@ -91,20 +91,35 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
drawableObjectPiece.ApplyCustomUpdateState -= applyDimToDrawableHitObject;
drawableObjectPiece.ApplyCustomUpdateState += applyDimToDrawableHitObject;
}
else
applyDim(piece);
}
void applyDim(Drawable piece)
{
piece.FadeColour(new Color4(195, 195, 195, 255));
using (piece.BeginDelayedSequence(InitialLifetimeOffset - OsuHitWindows.MISS_WINDOW))
piece.FadeColour(Color4.White, 100);
// 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.
applyDim(piece);
}
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;
private OsuInputManager osuActionInputManager;

View File

@ -0,0 +1,27 @@
// 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.Game.Audio;
using osu.Game.Rulesets.Taiko.Skinning.Argon;
namespace osu.Game.Rulesets.Taiko.Tests
{
[TestFixture]
public class VolumeAwareHitSampleInfoTest
{
[Test]
public void TestVolumeAwareHitSampleInfoIsNotEqualToItsUnderlyingSample(
[Values(HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_CLAP)]
string sample,
[Values(HitSampleInfo.BANK_NORMAL, HitSampleInfo.BANK_SOFT)]
string bank,
[Values(30, 70, 100)] int volume)
{
var underlyingSample = new HitSampleInfo(sample, bank, volume: volume);
var volumeAwareSample = new VolumeAwareHitSampleInfo(underlyingSample);
Assert.That(underlyingSample, Is.Not.EqualTo(volumeAwareSample));
}
}
}

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

View File

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

View File

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

View File

@ -10,7 +10,7 @@ using osu.Game.Rulesets.Taiko.Objects;
namespace osu.Game.Rulesets.Taiko.Edit
{
public class DrumRollCompositionTool : HitObjectCompositionTool
public class DrumRollCompositionTool : CompositionTool
{
public DrumRollCompositionTool()
: base(nameof(DrumRoll))
@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Taiko.Edit
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
{
public class HitCompositionTool : HitObjectCompositionTool
public class HitCompositionTool : CompositionTool
{
public HitCompositionTool()
: base(nameof(Hit))
@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Taiko.Edit
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
{
public class SwellCompositionTool : HitObjectCompositionTool
public class SwellCompositionTool : CompositionTool
{
public SwellCompositionTool()
: base(nameof(Swell))
@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Taiko.Edit
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 DrumRollCompositionTool(),

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using osu.Game.Audio;
@ -48,5 +49,24 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
return originalBank;
}
}
public override bool Equals(HitSampleInfo? other) => other is VolumeAwareHitSampleInfo && base.Equals(other);
/// <remarks>
/// <para>
/// This override attempts to match the <see cref="Equals"/> override above, but in theory it is not strictly necessary.
/// Recall that <see cref="GetHashCode"/> <a href="https://learn.microsoft.com/en-us/dotnet/api/system.object.gethashcode?view=net-8.0#notes-to-inheritors">must meet the following requirements</a>:
/// </para>
/// <para>
/// "If two objects compare as equal, the <see cref="GetHashCode"/> method for each object must return the same value.
/// However, if two objects do not compare as equal, <see cref="GetHashCode"/> methods for the two objects do not have to return different values."
/// </para>
/// <para>
/// Making this override combine the value generated by the base <see cref="GetHashCode"/> implementation with a constant means
/// that <see cref="HitSampleInfo"/> and <see cref="VolumeAwareHitSampleInfo"/> instances which have the same values of their members
/// will not have equal hash codes, which is slightly more efficient when these objects are used as dictionary keys.
/// </para>
/// </remarks>
public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), 1);
}
}

View File

@ -112,6 +112,7 @@ namespace osu.Game.Tests.Editing
{
SliderVelocityMultiplier = slider_velocity
};
AddStep("add to beatmap", () => composer.EditorBeatmap.Add(referenceObject));
assertSnapDistance(base_distance * slider_velocity, referenceObject, true);
assertSnappedDistance(base_distance * slider_velocity + 10, base_distance * slider_velocity, referenceObject);
@ -227,6 +228,42 @@ namespace osu.Game.Tests.Editing
assertSnappedDistance(400, 400);
}
[Test]
public void TestUnsnappedObject()
{
var slider = new Slider
{
StartTime = 0,
Path = new SliderPath
{
ControlPoints =
{
new PathControlPoint(),
// simulate object snapped to 1/3rds
// this object's end time will be 2000 / 3 = 666.66... ms
new PathControlPoint(new Vector2(200 / 3f, 0)),
}
}
};
AddStep("add slider", () => composer.EditorBeatmap.Add(slider));
AddStep("set snap to 1/4", () => BeatDivisor.Value = 4);
// with default beat length of 1000ms and snap at 1/4, the valid snap times are 500ms, 750ms, and 1000ms
// with default settings, the snapped distance will be a tenth of the difference of the time delta
// (500 - 666.66...) / 10 = -16.66... = -100 / 6
assertSnappedDistance(0, -100 / 6f, slider);
assertSnappedDistance(7, -100 / 6f, slider);
// (750 - 666.66...) / 10 = 8.33... = 100 / 12
assertSnappedDistance(9, 100 / 12f, slider);
assertSnappedDistance(33, 100 / 12f, slider);
// (1000 - 666.66...) / 10 = 33.33... = 100 / 3
assertSnappedDistance(34, 100 / 3f, slider);
}
[Test]
public void TestUseCurrentSnap()
{
@ -262,7 +299,7 @@ namespace osu.Game.Tests.Editing
=> AddAssert($"distance = {distance} -> duration = {expectedDuration} (snapped)", () => composer.DistanceSnapProvider.FindSnappedDuration(referenceObject ?? new HitObject(), distance), () => Is.EqualTo(expectedDuration).Within(Precision.FLOAT_EPSILON));
private void assertSnappedDistance(float distance, float expectedDistance, HitObject? referenceObject = null)
=> AddAssert($"distance = {distance} -> distance = {expectedDistance} (snapped)", () => composer.DistanceSnapProvider.FindSnappedDistance(referenceObject ?? new HitObject(), distance), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON));
=> AddAssert($"distance = {distance} -> distance = {expectedDistance} (snapped)", () => composer.DistanceSnapProvider.FindSnappedDistance(referenceObject ?? new HitObject(), distance, DistanceSnapTarget.End), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON));
private partial class TestHitObjectComposer : OsuHitObjectComposer
{

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

@ -0,0 +1,51 @@
// 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.Game.Utils;
using osuTK;
namespace osu.Game.Tests.Utils
{
[TestFixture]
public class GeometryUtilsTest
{
[TestCase(new int[] { }, new int[] { })]
[TestCase(new[] { 0, 0 }, new[] { 0, 0 })]
[TestCase(new[] { 0, 0, 1, 1, 1, -1, 2, 0 }, new[] { 0, 0, 1, 1, 2, 0, 1, -1 })]
[TestCase(new[] { 0, 0, 1, 1, 1, -1, 2, 0, 1, 0 }, new[] { 0, 0, 1, 1, 2, 0, 1, -1 })]
[TestCase(new[] { 0, 0, 1, 1, 2, -1, 2, 0, 1, 0, 4, 10 }, new[] { 0, 0, 4, 10, 2, -1 })]
public void TestConvexHull(int[] values, int[] expected)
{
var points = new Vector2[values.Length / 2];
for (int i = 0; i < values.Length; i += 2)
points[i / 2] = new Vector2(values[i], values[i + 1]);
var expectedPoints = new Vector2[expected.Length / 2];
for (int i = 0; i < expected.Length; i += 2)
expectedPoints[i / 2] = new Vector2(expected[i], expected[i + 1]);
var hull = GeometryUtils.GetConvexHull(points);
Assert.That(hull, Is.EquivalentTo(expectedPoints));
}
[TestCase(new int[] { }, 0, 0, 0)]
[TestCase(new[] { 0, 0 }, 0, 0, 0)]
[TestCase(new[] { 0, 0, 1, 1, 1, -1, 2, 0 }, 1, 0, 1)]
[TestCase(new[] { 0, 0, 1, 1, 1, -1, 2, 0, 1, 0 }, 1, 0, 1)]
[TestCase(new[] { 0, 0, 1, 1, 2, -1, 2, 0, 1, 0, 4, 10 }, 3, 4.5f, 5.5901699f)]
public void TestMinimumEnclosingCircle(int[] values, float x, float y, float r)
{
var points = new Vector2[values.Length / 2];
for (int i = 0; i < values.Length; i += 2)
points[i / 2] = new Vector2(values[i], values[i + 1]);
(var centre, float radius) = GeometryUtils.MinimumEnclosingCircle(points);
Assert.That(centre.X, Is.EqualTo(x).Within(0.0001));
Assert.That(centre.Y, Is.EqualTo(y).Within(0.0001));
Assert.That(radius, Is.EqualTo(r).Within(0.0001));
}
}
}

View File

@ -84,6 +84,7 @@ namespace osu.Game.Tests.Visual.Editing
targetContainer = getTargetContainer();
initialRotation = targetContainer!.Rotation;
DefaultOrigin = ToLocalSpace(targetContainer.ToScreenSpace(Vector2.Zero));
base.Begin();
}
@ -134,7 +135,7 @@ namespace osu.Game.Tests.Visual.Editing
OriginalSurroundingQuad = new Quad(targetContainer!.X, targetContainer.Y, targetContainer.Width, targetContainer.Height);
}
public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both)
public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both, float axisRotation = 0)
{
if (targetContainer == null)
throw new InvalidOperationException($"Cannot {nameof(Update)} a scale operation without calling {nameof(Begin)} first!");

View File

@ -4,6 +4,7 @@
using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Testing;
@ -36,6 +37,9 @@ namespace osu.Game.Tests.Visual.Editing
private ContextMenuContainer contextMenuContainer
=> Editor.ChildrenOfType<ContextMenuContainer>().First();
private SelectionBoxScaleHandle getScaleHandle(Anchor anchor)
=> Editor.ChildrenOfType<SelectionBoxScaleHandle>().First(it => it.Anchor == anchor);
private void moveMouseToObject(Func<HitObject> targetFunc)
{
AddStep("move mouse to object", () =>
@ -215,6 +219,51 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("2 hitobjects selected", () => EditorBeatmap.SelectedHitObjects.Count == 2 && !EditorBeatmap.SelectedHitObjects.Contains(addedObjects[1]));
}
[Test]
public void TestMultiSelectWithDragBox()
{
var addedObjects = new[]
{
new HitCircle { StartTime = 100 },
new HitCircle { StartTime = 200, Position = new Vector2(100) },
new HitCircle { StartTime = 300, Position = new Vector2(512, 0) },
new HitCircle { StartTime = 400, Position = new Vector2(412, 100) },
};
AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects));
AddStep("start dragging", () =>
{
InputManager.MoveMouseTo(blueprintContainer.ScreenSpaceDrawQuad.Centre);
InputManager.PressButton(MouseButton.Left);
});
AddStep("drag to left corner", () => InputManager.MoveMouseTo(blueprintContainer.ScreenSpaceDrawQuad.TopLeft - new Vector2(5)));
AddStep("end dragging", () => InputManager.ReleaseButton(MouseButton.Left));
AddAssert("2 hitobjects selected", () => EditorBeatmap.SelectedHitObjects, () => Has.Count.EqualTo(2));
AddStep("start dragging with control", () =>
{
InputManager.MoveMouseTo(blueprintContainer.ScreenSpaceDrawQuad.Centre);
InputManager.PressButton(MouseButton.Left);
InputManager.PressKey(Key.ControlLeft);
});
AddStep("drag to left corner", () => InputManager.MoveMouseTo(blueprintContainer.ScreenSpaceDrawQuad.TopRight + new Vector2(5, -5)));
AddStep("end dragging", () => InputManager.ReleaseButton(MouseButton.Left));
AddStep("release control", () => InputManager.ReleaseKey(Key.ControlLeft));
AddAssert("4 hitobjects selected", () => EditorBeatmap.SelectedHitObjects, () => Has.Count.EqualTo(4));
AddStep("start dragging without control", () =>
{
InputManager.MoveMouseTo(blueprintContainer.ScreenSpaceDrawQuad.Centre);
InputManager.PressButton(MouseButton.Left);
});
AddStep("drag to left corner", () => InputManager.MoveMouseTo(blueprintContainer.ScreenSpaceDrawQuad.TopRight + new Vector2(5, -5)));
AddStep("end dragging", () => InputManager.ReleaseButton(MouseButton.Left));
AddAssert("2 hitobjects selected", () => EditorBeatmap.SelectedHitObjects, () => Has.Count.EqualTo(2));
}
[Test]
public void TestNearestSelection()
{
@ -519,5 +568,137 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft));
}
[Test]
public void TestShiftModifierMaintainsAspectRatio()
{
HitCircle[] addedObjects = null!;
float aspectRatioBeforeDrag = 0;
float getAspectRatio() => (addedObjects[1].X - addedObjects[0].X) / (addedObjects[1].Y - addedObjects[0].Y);
AddStep("add hitobjects", () =>
{
EditorBeatmap.AddRange(addedObjects = new[]
{
new HitCircle { StartTime = 100, Position = new Vector2(150, 150) },
new HitCircle { StartTime = 200, Position = new Vector2(250, 200) },
});
aspectRatioBeforeDrag = getAspectRatio();
});
AddStep("select objects", () => EditorBeatmap.SelectedHitObjects.AddRange(addedObjects));
AddStep("move mouse to handle", () => InputManager.MoveMouseTo(getScaleHandle(Anchor.BottomRight).ScreenSpaceDrawQuad.Centre));
AddStep("begin drag", () => InputManager.PressButton(MouseButton.Left));
AddStep("move mouse", () => InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + new Vector2(50, 0)));
AddStep("aspect ratio does not equal", () => Assert.AreNotEqual(aspectRatioBeforeDrag, getAspectRatio()));
AddStep("press shift", () => InputManager.PressKey(Key.ShiftLeft));
AddStep("aspect ratio does equal", () => Assert.AreEqual(aspectRatioBeforeDrag, getAspectRatio()));
AddStep("end drag", () => InputManager.ReleaseButton(MouseButton.Left));
AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft));
}
[Test]
public void TestAltModifierScalesAroundCenter()
{
HitCircle[] addedObjects = null!;
Vector2 centerBeforeDrag = Vector2.Zero;
Vector2 getCenter() => (addedObjects[0].Position + addedObjects[1].Position) / 2;
AddStep("add hitobjects", () =>
{
EditorBeatmap.AddRange(addedObjects = new[]
{
new HitCircle { StartTime = 100, Position = new Vector2(150, 150) },
new HitCircle { StartTime = 200, Position = new Vector2(250, 200) },
});
centerBeforeDrag = getCenter();
});
AddStep("select objects", () => EditorBeatmap.SelectedHitObjects.AddRange(addedObjects));
AddStep("move mouse to handle", () => InputManager.MoveMouseTo(getScaleHandle(Anchor.BottomRight).ScreenSpaceDrawQuad.Centre));
AddStep("begin drag", () => InputManager.PressButton(MouseButton.Left));
AddStep("move mouse", () => InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + new Vector2(50, 0)));
AddStep("center does not equal", () => Assert.AreNotEqual(centerBeforeDrag, getCenter()));
AddStep("press alt", () => InputManager.PressKey(Key.AltLeft));
AddStep("center does equal", () => Assert.AreEqual(centerBeforeDrag, getCenter()));
AddStep("end drag", () => InputManager.ReleaseButton(MouseButton.Left));
AddStep("release alt", () => InputManager.ReleaseKey(Key.AltLeft));
}
[Test]
public void TestShiftAndAltModifierKeys()
{
HitCircle[] addedObjects = null!;
float aspectRatioBeforeDrag = 0;
Vector2 centerBeforeDrag = Vector2.Zero;
float getAspectRatio() => (addedObjects[1].X - addedObjects[0].X) / (addedObjects[1].Y - addedObjects[0].Y);
Vector2 getCenter() => (addedObjects[0].Position + addedObjects[1].Position) / 2;
AddStep("add hitobjects", () =>
{
EditorBeatmap.AddRange(addedObjects = new[]
{
new HitCircle { StartTime = 100, Position = new Vector2(150, 150) },
new HitCircle { StartTime = 200, Position = new Vector2(250, 200) },
});
aspectRatioBeforeDrag = getAspectRatio();
centerBeforeDrag = getCenter();
});
AddStep("select objects", () => EditorBeatmap.SelectedHitObjects.AddRange(addedObjects));
AddStep("move mouse to handle", () => InputManager.MoveMouseTo(getScaleHandle(Anchor.BottomRight).ScreenSpaceDrawQuad.Centre));
AddStep("begin drag", () => InputManager.PressButton(MouseButton.Left));
AddStep("move mouse", () => InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + new Vector2(50, 0)));
AddStep("aspect ratio does not equal", () => Assert.AreNotEqual(aspectRatioBeforeDrag, getAspectRatio()));
AddStep("center does not equal", () => Assert.AreNotEqual(centerBeforeDrag, getCenter()));
AddStep("press shift", () => InputManager.PressKey(Key.ShiftLeft));
AddStep("aspect ratio does equal", () => Assert.AreEqual(aspectRatioBeforeDrag, getAspectRatio()));
AddStep("center does not equal", () => Assert.AreNotEqual(centerBeforeDrag, getCenter()));
AddStep("press alt", () => InputManager.PressKey(Key.AltLeft));
AddStep("center does equal", () => Assert.AreEqual(centerBeforeDrag, getCenter()));
AddStep("end drag", () => InputManager.ReleaseButton(MouseButton.Left));
AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft));
AddStep("release alt", () => InputManager.ReleaseKey(Key.AltLeft));
}
}
}

View File

@ -199,7 +199,7 @@ namespace osu.Game.Tests.Visual.Editing
public double FindSnappedDuration(HitObject referenceObject, float distance) => 0;
public float FindSnappedDistance(HitObject referenceObject, float distance) => 0;
public float FindSnappedDistance(HitObject referenceObject, float distance, DistanceSnapTarget target) => 0;
}
}
}

View File

@ -100,6 +100,20 @@ namespace osu.Game.Tests.Visual.Editing
assertOnScreenAt(EditorScreenMode.Compose, 0);
}
[Test]
public void TestUrlDecodingOfArgs()
{
setUpEditor(new OsuRuleset().RulesetInfo);
AddAssert("is osu! ruleset", () => editorBeatmap.BeatmapInfo.Ruleset.Equals(new OsuRuleset().RulesetInfo));
AddStep("jump to encoded link", () => Game.HandleLink("osu://edit/00:14:142%20(1)"));
AddUntilStep("wait for seek", () => editorClock.SeekingOrStopped.Value);
AddAssert("time is correct", () => editorClock.CurrentTime, () => Is.EqualTo(14_142));
AddAssert("selected object is correct", () => editorBeatmap.SelectedHitObjects.Single().StartTime, () => Is.EqualTo(14_142));
}
private void addStepClickLink(string timestamp, string step = "", bool waitForSeek = true)
{
AddStep($"{step} {timestamp}", () =>

View File

@ -19,6 +19,7 @@ using osu.Game.Rulesets.UI;
using osu.Game.Scoring;
using osu.Game.Screens;
using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD.JudgementCounter;
using osu.Game.Tests.Beatmaps.IO;
using osu.Game.Tests.Gameplay;
using osu.Game.Tests.Visual.Multiplayer;
@ -167,14 +168,16 @@ namespace osu.Game.Tests.Visual.Gameplay
public void TestSpectatingDuringGameplay()
{
start();
sendFrames(300);
sendFrames(300, initialResultCount: 100);
loadSpectatingScreen();
waitForPlayerCurrent();
sendFrames(300);
sendFrames(300, initialResultCount: 100);
AddUntilStep("playing from correct point in time", () => player.ChildrenOfType<DrawableRuleset>().First().FrameStableClock.CurrentTime, () => Is.GreaterThan(30000));
AddAssert("check judgement counts are correct", () => player.ChildrenOfType<JudgementCountController>().Single().Counters.Sum(c => c.ResultCount.Value),
() => Is.GreaterThanOrEqualTo(100));
}
[Test]
@ -405,9 +408,9 @@ namespace osu.Game.Tests.Visual.Gameplay
private void checkPaused(bool state) =>
AddUntilStep($"game is {(state ? "paused" : "playing")}", () => player.ChildrenOfType<DrawableRuleset>().First().IsPaused.Value == state);
private void sendFrames(int count = 10, double startTime = 0)
private void sendFrames(int count = 10, double startTime = 0, int initialResultCount = 0)
{
AddStep("send frames", () => spectatorClient.SendFramesFromUser(streamingUser.Id, count, startTime));
AddStep("send frames", () => spectatorClient.SendFramesFromUser(streamingUser.Id, count, startTime, initialResultCount));
}
private void loadSpectatingScreen()

View File

@ -5,14 +5,18 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
using osu.Game.Rulesets;
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.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Ranking.Statistics.User;
using osu.Game.Tests.Resources;
using osu.Game.Users;
using osuTK;
@ -80,6 +85,69 @@ namespace osu.Game.Tests.Visual.Ranking
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", () =>
{
Child = new UserStatisticsPanel(score)

View File

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

View File

@ -1,37 +1,49 @@
// 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 NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays;
using osu.Game.Tests.Visual.UserInterface;
namespace osu.Game.Tests.Visual.Settings
{
public partial class TestSceneFileSelector : ThemeComparisonTestScene
{
[Resolved]
private OsuColour colours { get; set; } = null!;
public TestSceneFileSelector()
: base(false)
{
}
[Test]
public void TestJpgFilesOnly()
{
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,
Colour = colours.GreySeaFoam
(typeof(OverlayColourProvider), colourProvider)
},
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.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Overlays.Settings;
@ -19,16 +20,20 @@ namespace osu.Game.Tests.Visual.Settings
{
Children = new Drawable[]
{
new FillFlowContainer
new PopoverContainer
{
RelativeSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Spacing = new Vector2(20),
Width = 0.5f,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Padding = new MarginPadding(50),
ChildrenEnumerable = new TestTargetClass().CreateSettingsControls()
Child = new FillFlowContainer
{
RelativeSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Spacing = new Vector2(20),
Width = 0.5f,
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))]
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

View File

@ -175,6 +175,29 @@ namespace osu.Game.Tests.Visual.SongSelect
increaseModSpeed();
AddAssert("adaptive speed still active", () => songSelect!.Mods.Value.First() is ModAdaptiveSpeed);
OsuModDoubleTime dtWithAdjustPitch = new OsuModDoubleTime
{
SpeedChange = { Value = 1.05 },
AdjustPitch = { Value = true },
};
changeMods(dtWithAdjustPitch);
decreaseModSpeed();
AddAssert("no mods selected", () => songSelect!.Mods.Value.Count == 0);
decreaseModSpeed();
AddAssert("half time activated at 0.95x", () => songSelect!.Mods.Value.OfType<ModHalfTime>().Single().SpeedChange.Value, () => Is.EqualTo(0.95).Within(0.005));
AddAssert("half time has adjust pitch active", () => songSelect!.Mods.Value.OfType<ModHalfTime>().Single().AdjustPitch.Value, () => Is.True);
AddStep("turn off adjust pitch", () => songSelect!.Mods.Value.OfType<ModHalfTime>().Single().AdjustPitch.Value = false);
increaseModSpeed();
AddAssert("no mods selected", () => songSelect!.Mods.Value.Count == 0);
increaseModSpeed();
AddAssert("double time activated at 1.05x", () => songSelect!.Mods.Value.OfType<ModDoubleTime>().Single().SpeedChange.Value, () => Is.EqualTo(1.05).Within(0.005));
AddAssert("double time has adjust pitch inactive", () => songSelect!.Mods.Value.OfType<ModDoubleTime>().Single().AdjustPitch.Value, () => Is.False);
void increaseModSpeed() => AddStep("increase mod speed", () =>
{
InputManager.PressKey(Key.ControlLeft);

View File

@ -1,9 +1,11 @@
// 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.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Localisation;
@ -68,6 +70,41 @@ namespace osu.Game.Tests.Visual.UserInterface
HintText = EditorSetupStrings.LetterboxDuringBreaksDescription,
Current = { Disabled = true },
},
new FormSliderBar<float>
{
Caption = "Instantaneous slider",
Current = new BindableFloat
{
MinValue = 0,
MaxValue = 10,
Value = 5,
Precision = 0.1f,
},
TabbableContentContainer = this,
},
new FormSliderBar<float>
{
Caption = "Non-instantaneous slider",
Current = new BindableFloat
{
MinValue = 0,
MaxValue = 10,
Value = 5,
Precision = 0.1f,
},
Instantaneous = false,
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]
private MatchIPCInfo ipc { get; set; } = null!;
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
private OsuDirectorySelector directorySelector = null!;
private DialogOverlay? overlay;

View File

@ -96,7 +96,7 @@ namespace osu.Game.Audio
public virtual HitSampleInfo With(Optional<string> newName = default, Optional<string> newBank = default, Optional<string?> newSuffix = default, Optional<int> newVolume = default)
=> new HitSampleInfo(newName.GetOr(Name), newBank.GetOr(Bank), newSuffix.GetOr(Suffix), newVolume.GetOr(Volume));
public bool Equals(HitSampleInfo? other)
public virtual bool Equals(HitSampleInfo? other)
=> other != null && Name == other.Name && Bank == other.Bank && Suffix == other.Suffix;
public override bool Equals(object? obj)

View File

@ -198,8 +198,11 @@ namespace osu.Game.Beatmaps
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.
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.OnlineID = -1;

View File

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

View File

@ -46,7 +46,7 @@ namespace osu.Game.Graphics.UserInterface
protected override void LoadComplete()
{
base.LoadComplete();
CurrentNumber.BindValueChanged(current => TooltipText = getTooltipText(current.NewValue), true);
CurrentNumber.BindValueChanged(current => TooltipText = GetDisplayableValue(current.NewValue), true);
}
protected override void OnUserChange(T value)
@ -55,7 +55,7 @@ namespace osu.Game.Graphics.UserInterface
playSample(value);
TooltipText = getTooltipText(value);
TooltipText = GetDisplayableValue(value);
}
private void playSample(T value)
@ -83,7 +83,7 @@ namespace osu.Game.Graphics.UserInterface
channel.Play();
}
private LocalisableString getTooltipText(T value)
public LocalisableString GetDisplayableValue(T value)
{
if (CurrentNumber.IsInteger)
return int.CreateTruncating(value).ToString("N0");

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.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;
AutoSizeAxes = Axes.None;
Size = new Vector2(100, 50);
Size = new Vector2(140, OsuDirectorySelectorBreadcrumbDisplay.HEIGHT);
Margin = new MarginPadding { Right = OsuDirectorySelectorBreadcrumbDisplay.HORIZONTAL_PADDING, };
Anchor = Anchor.CentreLeft;
Origin = Anchor.CentreLeft;
LabelTextFlowContainer.Anchor = Anchor.CentreLeft;
LabelTextFlowContainer.Origin = Anchor.CentreLeft;
LabelText = @"Show hidden";
Scale = new Vector2(0.8f);
}
[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.
using System.IO;
using osu.Framework.Allocation;
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
{
@ -14,5 +16,11 @@ namespace osu.Game.Graphics.UserInterfaceV2
: base(directory, "..")
{
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
Colour = colourProvider.Content1;
}
}
}

View File

@ -29,7 +29,14 @@ namespace osu.Game.Graphics.UserInterfaceV2
private readonly BindableWithCurrent<bool> current = new BindableWithCurrent<bool>();
/// <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 Box background = null!;

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,280 @@
// 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),
// simplest solution to avoid underlying text to bleed through the bottom border
// https://github.com/ppy/osu/pull/30005#issuecomment-2378884430
Padding = new MarginPadding { Bottom = 1 },
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

@ -0,0 +1,374 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Globalization;
using System.Numerics;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
namespace osu.Game.Graphics.UserInterfaceV2
{
public partial class FormSliderBar<T> : CompositeDrawable, IHasCurrentValue<T>
where T : struct, INumber<T>, IMinMaxValue<T>
{
public Bindable<T> Current
{
get => current.Current;
set => current.Current = value;
}
private bool instantaneous = true;
/// <summary>
/// Whether changes to the slider should instantaneously transfer to the text box (and vice versa).
/// If <see langword="false"/>, the transfer will happen on text box commit (explicit, or implicit via focus loss), or on slider drag end.
/// </summary>
public bool Instantaneous
{
get => instantaneous;
set
{
instantaneous = value;
if (slider.IsNotNull())
slider.TransferValueOnCommit = !instantaneous;
}
}
private CompositeDrawable? tabbableContentContainer;
public CompositeDrawable? TabbableContentContainer
{
set
{
tabbableContentContainer = value;
if (textBox.IsNotNull())
textBox.TabbableContentContainer = tabbableContentContainer;
}
}
private readonly BindableNumberWithCurrent<T> current = new BindableNumberWithCurrent<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 Box background = null!;
private Box flashLayer = null!;
private FormTextBox.InnerTextBox textBox = null!;
private Slider slider = null!;
private FormFieldCaption caption = null!;
private IFocusManager focusManager = null!;
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
RelativeSizeAxes = Axes.X;
Height = 50;
Masking = true;
CornerRadius = 5;
InternalChildren = new Drawable[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background5,
},
flashLayer = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Colour4.Transparent,
},
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,
},
textBox = new FormNumberBox.InnerNumberBox
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
RelativeSizeAxes = Axes.X,
Width = 0.5f,
CommitOnFocusLost = true,
SelectAllOnFocus = true,
AllowDecimals = true,
OnInputError = () =>
{
flashLayer.Colour = ColourInfo.GradientVertical(colours.Red3.Opacity(0), colours.Red3);
flashLayer.FadeOutFromOne(200, Easing.OutQuint);
},
TabbableContentContainer = tabbableContentContainer,
},
slider = new Slider
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
RelativeSizeAxes = Axes.X,
Width = 0.5f,
Current = Current,
TransferValueOnCommit = !instantaneous,
}
},
},
};
}
protected override void LoadComplete()
{
base.LoadComplete();
focusManager = GetContainingFocusManager()!;
textBox.Focused.BindValueChanged(_ => updateState());
textBox.OnCommit += textCommitted;
textBox.Current.BindValueChanged(textChanged);
slider.IsDragging.BindValueChanged(_ => updateState());
current.BindValueChanged(_ =>
{
updateState();
updateTextBoxFromSlider();
}, true);
}
private bool updatingFromTextBox;
private void textChanged(ValueChangedEvent<string> change)
{
if (!instantaneous) return;
tryUpdateSliderFromTextBox();
}
private void textCommitted(TextBox t, bool isNew)
{
tryUpdateSliderFromTextBox();
// If the attempted update above failed, restore text box to match the slider.
Current.TriggerChange();
flashLayer.Colour = ColourInfo.GradientVertical(colourProvider.Dark2.Opacity(0), colourProvider.Dark2);
flashLayer.FadeOutFromOne(800, Easing.OutQuint);
}
private void tryUpdateSliderFromTextBox()
{
updatingFromTextBox = true;
try
{
switch (Current)
{
case Bindable<int> bindableInt:
bindableInt.Value = int.Parse(textBox.Current.Value);
break;
case Bindable<double> bindableDouble:
bindableDouble.Value = double.Parse(textBox.Current.Value);
break;
default:
Current.Parse(textBox.Current.Value, CultureInfo.CurrentCulture);
break;
}
}
catch
{
// ignore parsing failures.
// sane state will eventually be restored by a commit (either explicit, or implicit via focus loss).
}
updatingFromTextBox = false;
}
protected override bool OnHover(HoverEvent e)
{
updateState();
return true;
}
protected override void OnHoverLost(HoverLostEvent e)
{
base.OnHoverLost(e);
updateState();
}
protected override bool OnClick(ClickEvent e)
{
focusManager.ChangeFocus(textBox);
return true;
}
private void updateState()
{
textBox.Alpha = 1;
background.Colour = Current.Disabled ? colourProvider.Background4 : colourProvider.Background5;
caption.Colour = Current.Disabled ? colourProvider.Foreground1 : colourProvider.Content2;
textBox.Colour = Current.Disabled ? colourProvider.Foreground1 : colourProvider.Content1;
BorderThickness = IsHovered || textBox.Focused.Value || slider.IsDragging.Value ? 2 : 0;
BorderColour = textBox.Focused.Value ? colourProvider.Highlight1 : colourProvider.Light4;
if (textBox.Focused.Value)
background.Colour = ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark3);
else if (IsHovered || slider.IsDragging.Value)
background.Colour = ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark4);
else
background.Colour = colourProvider.Background5;
}
private void updateTextBoxFromSlider()
{
if (updatingFromTextBox) return;
textBox.Text = slider.GetDisplayableValue(Current.Value).ToString();
}
private partial class Slider : OsuSliderBar<T>
{
public BindableBool IsDragging { get; set; } = new BindableBool();
private Box leftBox = null!;
private Box rightBox = null!;
private Circle nub = null!;
private const float nub_width = 10;
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
[BackgroundDependencyLoader]
private void load()
{
Height = 40;
RelativeSizeAxes = Axes.X;
RangePadding = nub_width / 2;
Children = new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
Masking = true,
CornerRadius = 5,
Children = new Drawable[]
{
leftBox = new Box
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
rightBox = new Box
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
},
},
},
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Horizontal = RangePadding, },
Child = nub = new Circle
{
Width = nub_width,
RelativeSizeAxes = Axes.Y,
RelativePositionAxes = Axes.X,
Origin = Anchor.TopCentre,
}
},
new HoverClickSounds()
};
}
protected override void LoadComplete()
{
base.LoadComplete();
updateState();
}
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
leftBox.Width = Math.Clamp(RangePadding + nub.DrawPosition.X, 0, Math.Max(0, DrawWidth)) / DrawWidth;
rightBox.Width = Math.Clamp(DrawWidth - nub.DrawPosition.X - RangePadding, 0, Math.Max(0, DrawWidth)) / DrawWidth;
}
protected override bool OnDragStart(DragStartEvent e)
{
bool dragging = base.OnDragStart(e);
IsDragging.Value = dragging;
updateState();
return dragging;
}
protected override void OnDragEnd(DragEndEvent e)
{
base.OnDragEnd(e);
IsDragging.Value = false;
updateState();
}
protected override bool OnHover(HoverEvent e)
{
updateState();
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
updateState();
base.OnHoverLost(e);
}
private void updateState()
{
rightBox.Colour = colourProvider.Background6;
leftBox.Colour = IsHovered || IsDragged ? colourProvider.Highlight1.Opacity(0.5f) : colourProvider.Dark2;
nub.Colour = IsHovered || IsDragged ? colourProvider.Highlight1 : colourProvider.Light4;
}
protected override void UpdateValue(float value)
{
nub.MoveToX(value, 200, Easing.OutPow10);
}
}
}
}

View File

@ -59,8 +59,19 @@ namespace osu.Game.Graphics.UserInterfaceV2
private readonly BindableWithCurrent<string> current = new BindableWithCurrent<string>();
/// <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; }
/// <summary>
/// Text displayed in the text box when its contents are empty.
/// </summary>
public LocalisableString PlaceholderText { get; init; }
private Box background = null!;
@ -122,7 +133,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
if (!current.Disabled && !ReadOnly)
{
flashLayer.Colour = ColourInfo.GradientVertical(colourProvider.Dark1.Opacity(0), colourProvider.Dark2);
flashLayer.Colour = ColourInfo.GradientVertical(colourProvider.Dark2.Opacity(0), colourProvider.Dark2);
flashLayer.FadeOutFromOne(800, Easing.OutQuint);
}
};

View File

@ -1,41 +1,73 @@
// 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.UserInterface;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterfaceV2.FileSelection;
using osu.Game.Overlays;
namespace osu.Game.Graphics.UserInterfaceV2
{
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)
{
}
[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 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 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);
}

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.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.IO;
using System.Linq;
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.Containers;
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
{
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)
{
}
[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 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 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);
@ -51,19 +82,14 @@ namespace osu.Game.Graphics.UserInterfaceV2
}
[BackgroundDependencyLoader]
private void load()
private void load(OverlayColourProvider colourProvider)
{
Flow.AutoSizeAxes = Axes.X;
Flow.Height = OsuDirectorySelector.ITEM_HEIGHT;
AddRangeInternal(new Drawable[]
{
new OsuDirectorySelectorDirectory.Background
{
Depth = 1
},
new HoverClickSounds()
});
AddInternal(new BackgroundLayer());
Colour = colourProvider.Light3;
}
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>
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}";
}
}

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.
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())
Schedule(() => UpdateState(DownloadState.LocallyAvailable));

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Web;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.Edit;
@ -234,7 +235,7 @@ namespace osu.Game.Online.Chat
return new LinkDetails(LinkAction.External, url);
}
return new LinkDetails(linkType, args[2]);
return new LinkDetails(linkType, HttpUtility.UrlDecode(args[2]));
case "osump":
return new LinkDetails(LinkAction.JoinMultiplayerMatch, args[1]);

View File

@ -46,10 +46,15 @@ namespace osu.Game.Online
Downloader.DownloadBegan += downloadBegan;
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 =>
((s.OnlineID > 0 && s.OnlineID == TrackedItem.OnlineID)
|| (s.LegacyOnlineID > 0 && s.LegacyOnlineID == TrackedItem.LegacyOnlineID)
|| (!string.IsNullOrEmpty(s.Hash) && s.Hash == TrackedItem.Hash))
((s.OnlineID > 0 && s.OnlineID == onlineId)
|| (s.LegacyOnlineID > 0 && s.LegacyOnlineID == legacyOnlineId)
|| (!string.IsNullOrEmpty(s.Hash) && s.Hash == hash))
&& !s.DeletePending), (items, _) =>
{
if (items.Any())

View File

@ -445,10 +445,11 @@ namespace osu.Game
break;
case LinkAction.SearchBeatmapSet:
if (link.Argument is RomanisableString romanisable)
SearchBeatmapSet(romanisable.GetPreferred(Localisation.CurrentParameters.Value.PreferOriginalScript));
if (link.Argument is LocalisableString localisable)
SearchBeatmapSet(Localisation.GetLocalisedString(localisable));
else
SearchBeatmapSet(argString);
break;
case LinkAction.FilterBeatmapSetGenre:

View File

@ -242,7 +242,7 @@ namespace osu.Game.Overlays.BeatmapSet
title.Clear();
artist.Clear();
title.AddLink(titleText, LinkAction.SearchBeatmapSet, $@"title=""""{titleText}""""");
title.AddLink(titleText, LinkAction.SearchBeatmapSet, LocalisableString.Interpolate($@"title=""""{titleText}"""""));
title.AddArbitraryDrawable(Empty().With(d => d.Width = 5));
title.AddArbitraryDrawable(externalLink = new ExternalLinkButton());
@ -259,7 +259,7 @@ namespace osu.Game.Overlays.BeatmapSet
title.AddArbitraryDrawable(new SpotlightBeatmapBadge());
}
artist.AddLink(artistText, LinkAction.SearchBeatmapSet, $@"artist=""""{artistText}""""");
artist.AddLink(artistText, LinkAction.SearchBeatmapSet, LocalisableString.Interpolate($@"artist=""""{artistText}"""""));
if (setInfo.NewValue.TrackId != null)
{

View File

@ -314,6 +314,7 @@ namespace osu.Game.Overlays.FirstRunSetup
private partial class DirectoryChooserPopover : OsuPopover
{
public DirectoryChooserPopover(Bindable<DirectoryInfo?> currentDirectory)
: base(false)
{
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

@ -1,7 +1,6 @@
// 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.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
@ -20,18 +19,17 @@ using static osu.Game.Overlays.Mods.ModCustomisationPanel;
namespace osu.Game.Overlays.Mods
{
public partial class ModCustomisationHeader : OsuHoverContainer
public partial class ModCustomisationHeader : OsuClickableContainer
{
private Box background = null!;
private Box hoverBackground = null!;
private Box backgroundFlash = null!;
private SpriteIcon icon = null!;
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
protected override IEnumerable<Drawable> EffectTargets => new[] { background };
public readonly Bindable<ModCustomisationPanelState> ExpandedState = new Bindable<ModCustomisationPanelState>(ModCustomisationPanelState.Collapsed);
public readonly Bindable<ModCustomisationPanelState> ExpandedState = new Bindable<ModCustomisationPanelState>();
private readonly ModCustomisationPanel panel;
@ -53,6 +51,13 @@ namespace osu.Game.Overlays.Mods
{
RelativeSizeAxes = Axes.Both,
},
hoverBackground = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.Gray(50),
Blending = BlendingParameters.Additive,
Alpha = 0,
},
backgroundFlash = new Box
{
RelativeSizeAxes = Axes.Both,
@ -84,9 +89,6 @@ namespace osu.Game.Overlays.Mods
}
}
};
IdleColour = colourProvider.Dark3;
HoverColour = colourProvider.Light4;
}
protected override void LoadComplete()
@ -109,15 +111,37 @@ namespace osu.Game.Overlays.Mods
ExpandedState.BindValueChanged(v =>
{
icon.ScaleTo(v.NewValue > ModCustomisationPanelState.Collapsed ? new Vector2(1, -1) : Vector2.One, 300, Easing.OutQuint);
switch (v.NewValue)
{
case ModCustomisationPanelState.Collapsed:
background.FadeColour(colourProvider.Dark3, 500, Easing.OutQuint);
break;
case ModCustomisationPanelState.Expanded:
case ModCustomisationPanelState.ExpandedByMod:
background.FadeColour(colourProvider.Light4, 500, Easing.OutQuint);
break;
}
}, true);
}
protected override bool OnHover(HoverEvent e)
{
if (Enabled.Value && panel.ExpandedState.Value == ModCustomisationPanelState.Collapsed)
if (!Enabled.Value)
return base.OnHover(e);
if (panel.ExpandedState.Value == ModCustomisationPanelState.Collapsed)
panel.ExpandedState.Value = ModCustomisationPanelState.Expanded;
hoverBackground.FadeTo(0.4f, 200, Easing.OutQuint);
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
hoverBackground.FadeOut(200, Easing.OutQuint);
base.OnHoverLost(e);
}
}
}

View File

@ -48,8 +48,11 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
/// </summary>
protected virtual DirectoryInfo InitialPath => null;
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
[BackgroundDependencyLoader]
private void load(OsuColour colours)
private void load()
{
InternalChild = new Container
{
@ -64,7 +67,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colours.GreySeaFoamDark
Colour = colourProvider.Background4,
},
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.Menus;
using osu.Game.Skinning;
using osu.Framework.Graphics.Cursor;
namespace osu.Game.Overlays.SkinEditor
{
@ -118,107 +119,111 @@ namespace osu.Game.Overlays.SkinEditor
InternalChild = new OsuContextMenuContainer
{
RelativeSizeAxes = Axes.Both,
Child = new GridContainer
Child = new PopoverContainer
{
RelativeSizeAxes = Axes.Both,
RowDimensions = new[]
Child = new GridContainer
{
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
new Dimension(),
},
RelativeSizeAxes = Axes.Both,
RowDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
new Dimension(),
},
Content = new[]
{
new Drawable[]
Content = new[]
{
new Container
new Drawable[]
{
Name = @"Menu container",
RelativeSizeAxes = Axes.X,
Depth = float.MinValue,
Height = MENU_HEIGHT,
Children = new Drawable[]
new Container
{
new EditorMenuBar
Name = @"Menu container",
RelativeSizeAxes = Axes.X,
Depth = float.MinValue,
Height = MENU_HEIGHT,
Children = new Drawable[]
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.Both,
Items = new[]
new EditorMenuBar
{
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),
}
},
}
},
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,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
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

@ -46,7 +46,6 @@ namespace osu.Game.Overlays.SkinEditor
private Drawable[]? objectsInRotation;
private Vector2? defaultOrigin;
private Dictionary<Drawable, float>? originalRotations;
private Dictionary<Drawable, Vector2>? originalPositions;
@ -60,7 +59,7 @@ namespace osu.Game.Overlays.SkinEditor
objectsInRotation = selectedItems.Cast<Drawable>().ToArray();
originalRotations = objectsInRotation.ToDictionary(d => d, d => d.Rotation);
originalPositions = objectsInRotation.ToDictionary(d => d, d => d.ToScreenSpace(d.OriginPosition));
defaultOrigin = GeometryUtils.GetSurroundingQuad(objectsInRotation.SelectMany(d => d.ScreenSpaceDrawQuad.GetVertices().ToArray())).Centre;
DefaultOrigin = GeometryUtils.GetSurroundingQuad(objectsInRotation.SelectMany(d => d.ScreenSpaceDrawQuad.GetVertices().ToArray())).Centre;
base.Begin();
}
@ -70,7 +69,7 @@ namespace osu.Game.Overlays.SkinEditor
if (objectsInRotation == null)
throw new InvalidOperationException($"Cannot {nameof(Update)} a rotate operation without calling {nameof(Begin)} first!");
Debug.Assert(originalRotations != null && originalPositions != null && defaultOrigin != null);
Debug.Assert(originalRotations != null && originalPositions != null && DefaultOrigin != null);
if (objectsInRotation.Length == 1 && origin == null)
{
@ -79,7 +78,7 @@ namespace osu.Game.Overlays.SkinEditor
return;
}
var actualOrigin = origin ?? defaultOrigin.Value;
var actualOrigin = origin ?? DefaultOrigin.Value;
foreach (var drawableItem in objectsInRotation)
{
@ -100,7 +99,7 @@ namespace osu.Game.Overlays.SkinEditor
objectsInRotation = null;
originalPositions = null;
originalRotations = null;
defaultOrigin = null;
DefaultOrigin = null;
base.Commit();
}

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