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

Merge remote-tracking branch 'upstream/master' into alt-eagerselect

This commit is contained in:
Endrik Tombak 2018-04-13 11:55:35 +03:00
commit d9f587edff
45 changed files with 1140 additions and 954 deletions

@ -1 +1 @@
Subproject commit e4b0b57f5d3a80c09dcdfb6c8d30962e842b9fc3
Subproject commit 02d7a0fa4798d197cd08570ee48951edbb7c7860

View File

@ -2,11 +2,10 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Runtime.CompilerServices;
using osu.Framework.Testing;
// We publish our internal attributes to other sub-projects of the framework.
// Note, that we omit visual tests as they are meant to test the framework
// behavior "in the wild".
[assembly: InternalsVisibleTo("osu.Game.Rulesets.Catch.Tests")]
[assembly: InternalsVisibleTo(DynamicClassCompiler.DYNAMIC_ASSEMBLY_NAME)]
[assembly: InternalsVisibleTo("osu.Game.Rulesets.Catch.Tests.Dynamic")]

View File

@ -2,11 +2,10 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Runtime.CompilerServices;
using osu.Framework.Testing;
// We publish our internal attributes to other sub-projects of the framework.
// Note, that we omit visual tests as they are meant to test the framework
// behavior "in the wild".
[assembly: InternalsVisibleTo("osu.Game.Rulesets.Mania.Tests")]
[assembly: InternalsVisibleTo(DynamicClassCompiler.DYNAMIC_ASSEMBLY_NAME)]
[assembly: InternalsVisibleTo("osu.Game.Rulesets.Mania.Tests.Dynamic")]

View File

@ -21,6 +21,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Layers.Selection.Overlays
Size = hitCircle.Size;
Scale = hitCircle.Scale;
CornerRadius = Size.X / 2;
AddInternal(new RingPiece());
hitCircle.HitObject.PositionChanged += _ => Position = hitCircle.Position;

View File

@ -38,6 +38,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Layers.Selection.Overlays
Scale = slider.HeadCircle.Scale;
AddInternal(new RingPiece());
Select();
}
[BackgroundDependencyLoader]
@ -52,5 +54,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Layers.Selection.Overlays
RelativeAnchorPosition = hitObject.RelativeAnchorPosition;
}
// Todo: This is temporary, since the slider circle masks don't do anything special yet. In the future they will handle input.
public override bool HandleMouseInput => false;
}
}

View File

@ -3,6 +3,7 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives;
using osu.Game.Graphics;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu.Objects;
@ -59,5 +60,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Layers.Selection.Overlays
}
public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => body.ReceiveMouseInputAt(screenSpacePos);
public override Vector2 SelectionPoint => ToScreenSpace(OriginPosition);
public override Quad SelectionQuad => body.PathDrawQuad;
}
}

View File

@ -10,7 +10,6 @@ using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Framework.Graphics.Primitives;
using osu.Game.Configuration;
using osu.Game.Rulesets.Scoring;
using OpenTK.Graphics;
@ -177,8 +176,5 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public Drawable ProxiedLayer => HeadCircle.ApproachCircle;
public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => Body.ReceiveMouseInputAt(screenSpacePos);
public override Vector2 SelectionPoint => ToScreenSpace(OriginPosition);
public override Quad SelectionQuad => Body.PathDrawQuad;
}
}

View File

@ -7,10 +7,11 @@ using osu.Game.Rulesets.Objects;
using OpenTK;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Edit.Types;
namespace osu.Game.Rulesets.Osu.Objects
{
public abstract class OsuHitObject : HitObject, IHasComboInformation, IHasPosition
public abstract class OsuHitObject : HitObject, IHasComboInformation, IHasEditablePosition
{
public const double OBJECT_RADIUS = 64;

View File

@ -2,11 +2,10 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Runtime.CompilerServices;
using osu.Framework.Testing;
// We publish our internal attributes to other sub-projects of the framework.
// Note, that we omit visual tests as they are meant to test the framework
// behavior "in the wild".
[assembly: InternalsVisibleTo("osu.Game.Rulesets.Osu.Tests")]
[assembly: InternalsVisibleTo(DynamicClassCompiler.DYNAMIC_ASSEMBLY_NAME)]
[assembly: InternalsVisibleTo("osu.Game.Rulesets.Osu.Tests.Dynamic")]

View File

@ -2,11 +2,10 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Runtime.CompilerServices;
using osu.Framework.Testing;
// We publish our internal attributes to other sub-projects of the framework.
// Note, that we omit visual tests as they are meant to test the framework
// behavior "in the wild".
[assembly: InternalsVisibleTo("osu.Game.Rulesets.Taiko.Tests")]
[assembly: InternalsVisibleTo(DynamicClassCompiler.DYNAMIC_ASSEMBLY_NAME)]
[assembly: InternalsVisibleTo("osu.Game.Rulesets.Taiko.Tests.Dynamic")]

View File

@ -9,7 +9,6 @@ using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input;
using osu.Framework.MathUtils;
using osu.Framework.Testing.Input;
using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.Sprites;
using OpenTK;
@ -18,88 +17,74 @@ using OpenTK.Graphics;
namespace osu.Game.Tests.Visual
{
[TestFixture]
public class TestCaseCursors : OsuTestCase
public class TestCaseCursors : ManualInputManagerTestCase
{
private readonly ManualInputManager inputManager;
private readonly CursorOverrideContainer cursorOverrideContainer;
private readonly CustomCursorBox[] cursorBoxes = new CustomCursorBox[6];
public TestCaseCursors()
{
Child = inputManager = new ManualInputManager
Child = cursorOverrideContainer = new CursorOverrideContainer
{
Child = cursorOverrideContainer = new CursorOverrideContainer
RelativeSizeAxes = Axes.Both,
Children = new[]
{
RelativeSizeAxes = Axes.Both,
Children = new[]
// Middle user
cursorBoxes[0] = new CustomCursorBox(Color4.Green)
{
// Middle user
cursorBoxes[0] = new CustomCursorBox(Color4.Green)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Size = new Vector2(0.5f),
},
// Top-left user
cursorBoxes[1] = new CustomCursorBox(Color4.Blue)
{
RelativeSizeAxes = Axes.Both,
Size = new Vector2(0.4f)
},
// Bottom-right user
cursorBoxes[2] = new CustomCursorBox(Color4.Red)
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
RelativeSizeAxes = Axes.Both,
Size = new Vector2(0.4f)
},
// Bottom-left local
cursorBoxes[3] = new CustomCursorBox(Color4.Magenta, false)
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
RelativeSizeAxes = Axes.Both,
Size = new Vector2(0.4f)
},
// Top-right local
cursorBoxes[4] = new CustomCursorBox(Color4.Cyan, false)
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
RelativeSizeAxes = Axes.Both,
Size = new Vector2(0.4f)
},
// Left-local
cursorBoxes[5] = new CustomCursorBox(Color4.Yellow, false)
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.Both,
Size = new Vector2(0.2f, 1),
},
}
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Size = new Vector2(0.5f),
},
// Top-left user
cursorBoxes[1] = new CustomCursorBox(Color4.Blue)
{
RelativeSizeAxes = Axes.Both,
Size = new Vector2(0.4f)
},
// Bottom-right user
cursorBoxes[2] = new CustomCursorBox(Color4.Red)
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
RelativeSizeAxes = Axes.Both,
Size = new Vector2(0.4f)
},
// Bottom-left local
cursorBoxes[3] = new CustomCursorBox(Color4.Magenta, false)
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
RelativeSizeAxes = Axes.Both,
Size = new Vector2(0.4f)
},
// Top-right local
cursorBoxes[4] = new CustomCursorBox(Color4.Cyan, false)
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
RelativeSizeAxes = Axes.Both,
Size = new Vector2(0.4f)
},
// Left-local
cursorBoxes[5] = new CustomCursorBox(Color4.Yellow, false)
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.Both,
Size = new Vector2(0.2f, 1),
},
}
};
returnUserInput();
AddToggleStep("Smooth transitions", b => cursorBoxes.ForEach(box => box.SmoothTransition = b));
testUserCursor();
testLocalCursor();
testUserCursorOverride();
testMultipleLocalCursors();
returnUserInput();
}
/// <summary>
/// Returns input back to the user.
/// </summary>
private void returnUserInput()
{
AddStep("Return user input", () => inputManager.UseParentState = true);
ReturnUserInput();
}
/// <summary>
@ -109,7 +94,7 @@ namespace osu.Game.Tests.Visual
/// </summary>
private void testUserCursor()
{
AddStep("Move to green area", () => inputManager.MoveMouseTo(cursorBoxes[0]));
AddStep("Move to green area", () => InputManager.MoveMouseTo(cursorBoxes[0]));
AddAssert("Check green cursor visible", () => checkVisible(cursorBoxes[0].Cursor));
AddAssert("Check green cursor at mouse", () => checkAtMouse(cursorBoxes[0].Cursor));
AddStep("Move out", moveOut);
@ -124,7 +109,7 @@ namespace osu.Game.Tests.Visual
/// </summary>
private void testLocalCursor()
{
AddStep("Move to purple area", () => inputManager.MoveMouseTo(cursorBoxes[3]));
AddStep("Move to purple area", () => InputManager.MoveMouseTo(cursorBoxes[3]));
AddAssert("Check purple cursor visible", () => checkVisible(cursorBoxes[3].Cursor));
AddAssert("Check purple cursor at mouse", () => checkAtMouse(cursorBoxes[3].Cursor));
AddAssert("Check global cursor visible", () => checkVisible(cursorOverrideContainer.Cursor));
@ -141,7 +126,7 @@ namespace osu.Game.Tests.Visual
/// </summary>
private void testUserCursorOverride()
{
AddStep("Move to blue-green boundary", () => inputManager.MoveMouseTo(cursorBoxes[1].ScreenSpaceDrawQuad.BottomRight - new Vector2(10)));
AddStep("Move to blue-green boundary", () => InputManager.MoveMouseTo(cursorBoxes[1].ScreenSpaceDrawQuad.BottomRight - new Vector2(10)));
AddAssert("Check blue cursor visible", () => checkVisible(cursorBoxes[1].Cursor));
AddAssert("Check green cursor invisible", () => !checkVisible(cursorBoxes[0].Cursor));
AddAssert("Check blue cursor at mouse", () => checkAtMouse(cursorBoxes[1].Cursor));
@ -156,7 +141,7 @@ namespace osu.Game.Tests.Visual
/// </summary>
private void testMultipleLocalCursors()
{
AddStep("Move to yellow-purple boundary", () => inputManager.MoveMouseTo(cursorBoxes[5].ScreenSpaceDrawQuad.BottomRight - new Vector2(10)));
AddStep("Move to yellow-purple boundary", () => InputManager.MoveMouseTo(cursorBoxes[5].ScreenSpaceDrawQuad.BottomRight - new Vector2(10)));
AddAssert("Check purple cursor visible", () => checkVisible(cursorBoxes[3].Cursor));
AddAssert("Check purple cursor at mouse", () => checkAtMouse(cursorBoxes[3].Cursor));
AddAssert("Check yellow cursor visible", () => checkVisible(cursorBoxes[5].Cursor));
@ -172,7 +157,7 @@ namespace osu.Game.Tests.Visual
/// </summary>
private void testUserOverrideWithLocal()
{
AddStep("Move to yellow-blue boundary", () => inputManager.MoveMouseTo(cursorBoxes[5].ScreenSpaceDrawQuad.TopRight - new Vector2(10)));
AddStep("Move to yellow-blue boundary", () => InputManager.MoveMouseTo(cursorBoxes[5].ScreenSpaceDrawQuad.TopRight - new Vector2(10)));
AddAssert("Check blue cursor visible", () => checkVisible(cursorBoxes[1].Cursor));
AddAssert("Check blue cursor at mouse", () => checkAtMouse(cursorBoxes[1].Cursor));
AddAssert("Check yellow cursor visible", () => checkVisible(cursorBoxes[5].Cursor));
@ -186,7 +171,7 @@ namespace osu.Game.Tests.Visual
/// Moves the cursor to a point not covered by any cursor containers.
/// </summary>
private void moveOut()
=> inputManager.MoveMouseTo(new Vector2(inputManager.ScreenSpaceDrawQuad.Centre.X, inputManager.ScreenSpaceDrawQuad.TopLeft.Y));
=> InputManager.MoveMouseTo(new Vector2(InputManager.ScreenSpaceDrawQuad.Centre.X, InputManager.ScreenSpaceDrawQuad.TopLeft.Y));
/// <summary>
/// Checks if a cursor is visible.
@ -199,7 +184,7 @@ namespace osu.Game.Tests.Visual
/// </summary>
/// <param name="cursorContainer">The cursor to check.</param>
private bool checkAtMouse(CursorContainer cursorContainer)
=> Precision.AlmostEquals(inputManager.CurrentState.Mouse.NativeState.Position, cursorContainer.ToScreenSpace(cursorContainer.ActiveCursor.DrawPosition));
=> Precision.AlmostEquals(InputManager.CurrentState.Mouse.NativeState.Position, cursorContainer.ToScreenSpace(cursorContainer.ActiveCursor.DrawPosition));
private class CustomCursorBox : Container, IProvideCursor
{

View File

@ -5,7 +5,6 @@ using System;
using System.Collections.Generic;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Timing;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Edit.Screens.Compose;
using osu.Game.Tests.Beatmaps;
@ -13,24 +12,15 @@ using osu.Game.Tests.Beatmaps;
namespace osu.Game.Tests.Visual
{
[TestFixture]
public class TestCaseEditorCompose : OsuTestCase
public class TestCaseEditorCompose : EditorClockTestCase
{
public override IReadOnlyList<Type> RequiredTypes => new[] { typeof(Compose) };
private DependencyContainer dependencies;
protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent)
=> dependencies = new DependencyContainer(parent);
[BackgroundDependencyLoader]
private void load(OsuGameBase osuGame)
{
osuGame.Beatmap.Value = new TestWorkingBeatmap(new OsuRuleset().RulesetInfo);
var clock = new DecoupleableInterpolatingFramedClock { IsCoupled = false };
dependencies.CacheAs<IAdjustableClock>(clock);
dependencies.CacheAs<IFrameBasedClock>(clock);
var compose = new Compose();
compose.Beatmap.BindTo(osuGame.Beatmap);

View File

@ -3,52 +3,35 @@
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit.Screens.Compose;
using osu.Game.Tests.Beatmaps;
using OpenTK;
using OpenTK.Graphics;
namespace osu.Game.Tests.Visual
{
public class TestCaseEditorSeekSnapping : OsuTestCase
public class TestCaseEditorSeekSnapping : EditorClockTestCase
{
public override IReadOnlyList<Type> RequiredTypes => new[] { typeof(HitObjectComposer) };
private Track track;
private HitObjectComposer composer;
private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor(4);
private DecoupleableInterpolatingFramedClock clock;
private DependencyContainer dependencies;
protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent)
=> dependencies = new DependencyContainer(parent);
public TestCaseEditorSeekSnapping()
{
BeatDivisor.Value = 4;
}
[BackgroundDependencyLoader]
private void load(OsuGameBase osuGame)
{
clock = new DecoupleableInterpolatingFramedClock { IsCoupled = false };
dependencies.CacheAs<IAdjustableClock>(clock);
dependencies.CacheAs<IFrameBasedClock>(clock);
dependencies.Cache(beatDivisor);
var testBeatmap = new Beatmap
{
ControlPointInfo = new ControlPointInfo
@ -71,22 +54,8 @@ namespace osu.Game.Tests.Visual
};
osuGame.Beatmap.Value = new TestWorkingBeatmap(testBeatmap);
track = osuGame.Beatmap.Value.Track;
Child = new GridContainer
{
RelativeSizeAxes = Axes.Both,
Content = new[]
{
new Drawable[] { composer = new TestHitObjectComposer(new OsuRuleset()) },
new Drawable[] { new TimingPointVisualiser(testBeatmap, track) { Clock = clock } },
},
RowDimensions = new[]
{
new Dimension(GridSizeMode.Distributed),
new Dimension(GridSizeMode.AutoSize),
}
};
Child = new TimingPointVisualiser(testBeatmap, 5000) { Clock = Clock };
testSeekNoSnapping();
testSeekSnappingOnBeat();
@ -107,18 +76,18 @@ namespace osu.Game.Tests.Visual
reset();
// Forwards
AddStep("Seek(0)", () => composer.SeekTo(0));
AddAssert("Time = 0", () => clock.CurrentTime == 0);
AddStep("Seek(33)", () => composer.SeekTo(33));
AddAssert("Time = 33", () => clock.CurrentTime == 33);
AddStep("Seek(89)", () => composer.SeekTo(89));
AddAssert("Time = 89", () => clock.CurrentTime == 89);
AddStep("Seek(0)", () => Clock.Seek(0));
AddAssert("Time = 0", () => Clock.CurrentTime == 0);
AddStep("Seek(33)", () => Clock.Seek(33));
AddAssert("Time = 33", () => Clock.CurrentTime == 33);
AddStep("Seek(89)", () => Clock.Seek(89));
AddAssert("Time = 89", () => Clock.CurrentTime == 89);
// Backwards
AddStep("Seek(25)", () => composer.SeekTo(25));
AddAssert("Time = 25", () => clock.CurrentTime == 25);
AddStep("Seek(0)", () => composer.SeekTo(0));
AddAssert("Time = 0", () => clock.CurrentTime == 0);
AddStep("Seek(25)", () => Clock.Seek(25));
AddAssert("Time = 25", () => Clock.CurrentTime == 25);
AddStep("Seek(0)", () => Clock.Seek(0));
AddAssert("Time = 0", () => Clock.CurrentTime == 0);
}
/// <summary>
@ -129,20 +98,20 @@ namespace osu.Game.Tests.Visual
{
reset();
AddStep("Seek(0), Snap", () => composer.SeekTo(0, true));
AddAssert("Time = 0", () => clock.CurrentTime == 0);
AddStep("Seek(50), Snap", () => composer.SeekTo(50, true));
AddAssert("Time = 50", () => clock.CurrentTime == 50);
AddStep("Seek(100), Snap", () => composer.SeekTo(100, true));
AddAssert("Time = 100", () => clock.CurrentTime == 100);
AddStep("Seek(175), Snap", () => composer.SeekTo(175, true));
AddAssert("Time = 175", () => clock.CurrentTime == 175);
AddStep("Seek(350), Snap", () => composer.SeekTo(350, true));
AddAssert("Time = 350", () => clock.CurrentTime == 350);
AddStep("Seek(400), Snap", () => composer.SeekTo(400, true));
AddAssert("Time = 400", () => clock.CurrentTime == 400);
AddStep("Seek(450), Snap", () => composer.SeekTo(450, true));
AddAssert("Time = 450", () => clock.CurrentTime == 450);
AddStep("Seek(0), Snap", () => Clock.SeekSnapped(0));
AddAssert("Time = 0", () => Clock.CurrentTime == 0);
AddStep("Seek(50), Snap", () => Clock.SeekSnapped(50));
AddAssert("Time = 50", () => Clock.CurrentTime == 50);
AddStep("Seek(100), Snap", () => Clock.SeekSnapped(100));
AddAssert("Time = 100", () => Clock.CurrentTime == 100);
AddStep("Seek(175), Snap", () => Clock.SeekSnapped(175));
AddAssert("Time = 175", () => Clock.CurrentTime == 175);
AddStep("Seek(350), Snap", () => Clock.SeekSnapped(350));
AddAssert("Time = 350", () => Clock.CurrentTime == 350);
AddStep("Seek(400), Snap", () => Clock.SeekSnapped(400));
AddAssert("Time = 400", () => Clock.CurrentTime == 400);
AddStep("Seek(450), Snap", () => Clock.SeekSnapped(450));
AddAssert("Time = 450", () => Clock.CurrentTime == 450);
}
/// <summary>
@ -154,18 +123,18 @@ namespace osu.Game.Tests.Visual
{
reset();
AddStep("Seek(24), Snap", () => composer.SeekTo(24, true));
AddAssert("Time = 0", () => clock.CurrentTime == 0);
AddStep("Seek(26), Snap", () => composer.SeekTo(26, true));
AddAssert("Time = 50", () => clock.CurrentTime == 50);
AddStep("Seek(150), Snap", () => composer.SeekTo(150, true));
AddAssert("Time = 100", () => clock.CurrentTime == 100);
AddStep("Seek(170), Snap", () => composer.SeekTo(170, true));
AddAssert("Time = 175", () => clock.CurrentTime == 175);
AddStep("Seek(274), Snap", () => composer.SeekTo(274, true));
AddAssert("Time = 175", () => clock.CurrentTime == 175);
AddStep("Seek(276), Snap", () => composer.SeekTo(276, true));
AddAssert("Time = 350", () => clock.CurrentTime == 350);
AddStep("Seek(24), Snap", () => Clock.SeekSnapped(24));
AddAssert("Time = 0", () => Clock.CurrentTime == 0);
AddStep("Seek(26), Snap", () => Clock.SeekSnapped(26));
AddAssert("Time = 50", () => Clock.CurrentTime == 50);
AddStep("Seek(150), Snap", () => Clock.SeekSnapped(150));
AddAssert("Time = 100", () => Clock.CurrentTime == 100);
AddStep("Seek(170), Snap", () => Clock.SeekSnapped(170));
AddAssert("Time = 175", () => Clock.CurrentTime == 175);
AddStep("Seek(274), Snap", () => Clock.SeekSnapped(274));
AddAssert("Time = 175", () => Clock.CurrentTime == 175);
AddStep("Seek(276), Snap", () => Clock.SeekSnapped(276));
AddAssert("Time = 350", () => Clock.CurrentTime == 350);
}
/// <summary>
@ -175,16 +144,16 @@ namespace osu.Game.Tests.Visual
{
reset();
AddStep("SeekForward", () => composer.SeekForward());
AddAssert("Time = 50", () => clock.CurrentTime == 50);
AddStep("SeekForward", () => composer.SeekForward());
AddAssert("Time = 100", () => clock.CurrentTime == 100);
AddStep("SeekForward", () => composer.SeekForward());
AddAssert("Time = 200", () => clock.CurrentTime == 200);
AddStep("SeekForward", () => composer.SeekForward());
AddAssert("Time = 400", () => clock.CurrentTime == 400);
AddStep("SeekForward", () => composer.SeekForward());
AddAssert("Time = 450", () => clock.CurrentTime == 450);
AddStep("SeekForward", () => Clock.SeekForward());
AddAssert("Time = 50", () => Clock.CurrentTime == 50);
AddStep("SeekForward", () => Clock.SeekForward());
AddAssert("Time = 100", () => Clock.CurrentTime == 100);
AddStep("SeekForward", () => Clock.SeekForward());
AddAssert("Time = 200", () => Clock.CurrentTime == 200);
AddStep("SeekForward", () => Clock.SeekForward());
AddAssert("Time = 400", () => Clock.CurrentTime == 400);
AddStep("SeekForward", () => Clock.SeekForward());
AddAssert("Time = 450", () => Clock.CurrentTime == 450);
}
/// <summary>
@ -194,18 +163,18 @@ namespace osu.Game.Tests.Visual
{
reset();
AddStep("SeekForward, Snap", () => composer.SeekForward(true));
AddAssert("Time = 50", () => clock.CurrentTime == 50);
AddStep("SeekForward, Snap", () => composer.SeekForward(true));
AddAssert("Time = 100", () => clock.CurrentTime == 100);
AddStep("SeekForward, Snap", () => composer.SeekForward(true));
AddAssert("Time = 175", () => clock.CurrentTime == 175);
AddStep("SeekForward, Snap", () => composer.SeekForward(true));
AddAssert("Time = 350", () => clock.CurrentTime == 350);
AddStep("SeekForward, Snap", () => composer.SeekForward(true));
AddAssert("Time = 400", () => clock.CurrentTime == 400);
AddStep("SeekForward, Snap", () => composer.SeekForward(true));
AddAssert("Time = 450", () => clock.CurrentTime == 450);
AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
AddAssert("Time = 50", () => Clock.CurrentTime == 50);
AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
AddAssert("Time = 100", () => Clock.CurrentTime == 100);
AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
AddAssert("Time = 175", () => Clock.CurrentTime == 175);
AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
AddAssert("Time = 350", () => Clock.CurrentTime == 350);
AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
AddAssert("Time = 400", () => Clock.CurrentTime == 400);
AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
AddAssert("Time = 450", () => Clock.CurrentTime == 450);
}
/// <summary>
@ -216,30 +185,30 @@ namespace osu.Game.Tests.Visual
{
reset();
AddStep("Seek(49)", () => composer.SeekTo(49));
AddStep("SeekForward, Snap", () => composer.SeekForward(true));
AddAssert("Time = 50", () => clock.CurrentTime == 50);
AddStep("Seek(49.999)", () => composer.SeekTo(49.999));
AddStep("SeekForward, Snap", () => composer.SeekForward(true));
AddAssert("Time = 50", () => clock.CurrentTime == 50);
AddStep("Seek(99)", () => composer.SeekTo(99));
AddStep("SeekForward, Snap", () => composer.SeekForward(true));
AddAssert("Time = 100", () => clock.CurrentTime == 100);
AddStep("Seek(99.999)", () => composer.SeekTo(99.999));
AddStep("SeekForward, Snap", () => composer.SeekForward(true));
AddAssert("Time = 100", () => clock.CurrentTime == 100);
AddStep("Seek(174)", () => composer.SeekTo(174));
AddStep("SeekForward, Snap", () => composer.SeekForward(true));
AddAssert("Time = 175", () => clock.CurrentTime == 175);
AddStep("Seek(349)", () => composer.SeekTo(349));
AddStep("SeekForward, Snap", () => composer.SeekForward(true));
AddAssert("Time = 350", () => clock.CurrentTime == 350);
AddStep("Seek(399)", () => composer.SeekTo(399));
AddStep("SeekForward, Snap", () => composer.SeekForward(true));
AddAssert("Time = 400", () => clock.CurrentTime == 400);
AddStep("Seek(449)", () => composer.SeekTo(449));
AddStep("SeekForward, Snap", () => composer.SeekForward(true));
AddAssert("Time = 450", () => clock.CurrentTime == 450);
AddStep("Seek(49)", () => Clock.Seek(49));
AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
AddAssert("Time = 50", () => Clock.CurrentTime == 50);
AddStep("Seek(49.999)", () => Clock.Seek(49.999));
AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
AddAssert("Time = 50", () => Clock.CurrentTime == 50);
AddStep("Seek(99)", () => Clock.Seek(99));
AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
AddAssert("Time = 100", () => Clock.CurrentTime == 100);
AddStep("Seek(99.999)", () => Clock.Seek(99.999));
AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
AddAssert("Time = 100", () => Clock.CurrentTime == 100);
AddStep("Seek(174)", () => Clock.Seek(174));
AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
AddAssert("Time = 175", () => Clock.CurrentTime == 175);
AddStep("Seek(349)", () => Clock.Seek(349));
AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
AddAssert("Time = 350", () => Clock.CurrentTime == 350);
AddStep("Seek(399)", () => Clock.Seek(399));
AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
AddAssert("Time = 400", () => Clock.CurrentTime == 400);
AddStep("Seek(449)", () => Clock.Seek(449));
AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
AddAssert("Time = 450", () => Clock.CurrentTime == 450);
}
/// <summary>
@ -249,19 +218,19 @@ namespace osu.Game.Tests.Visual
{
reset();
AddStep("Seek(450)", () => composer.SeekTo(450));
AddStep("SeekBackward", () => composer.SeekBackward());
AddAssert("Time = 425", () => clock.CurrentTime == 425);
AddStep("SeekBackward", () => composer.SeekBackward());
AddAssert("Time = 375", () => clock.CurrentTime == 375);
AddStep("SeekBackward", () => composer.SeekBackward());
AddAssert("Time = 325", () => clock.CurrentTime == 325);
AddStep("SeekBackward", () => composer.SeekBackward());
AddAssert("Time = 125", () => clock.CurrentTime == 125);
AddStep("SeekBackward", () => composer.SeekBackward());
AddAssert("Time = 25", () => clock.CurrentTime == 25);
AddStep("SeekBackward", () => composer.SeekBackward());
AddAssert("Time = 0", () => clock.CurrentTime == 0);
AddStep("Seek(450)", () => Clock.Seek(450));
AddStep("SeekBackward", () => Clock.SeekBackward());
AddAssert("Time = 425", () => Clock.CurrentTime == 425);
AddStep("SeekBackward", () => Clock.SeekBackward());
AddAssert("Time = 375", () => Clock.CurrentTime == 375);
AddStep("SeekBackward", () => Clock.SeekBackward());
AddAssert("Time = 325", () => Clock.CurrentTime == 325);
AddStep("SeekBackward", () => Clock.SeekBackward());
AddAssert("Time = 125", () => Clock.CurrentTime == 125);
AddStep("SeekBackward", () => Clock.SeekBackward());
AddAssert("Time = 25", () => Clock.CurrentTime == 25);
AddStep("SeekBackward", () => Clock.SeekBackward());
AddAssert("Time = 0", () => Clock.CurrentTime == 0);
}
/// <summary>
@ -271,19 +240,19 @@ namespace osu.Game.Tests.Visual
{
reset();
AddStep("Seek(450)", () => composer.SeekTo(450));
AddStep("SeekBackward, Snap", () => composer.SeekBackward(true));
AddAssert("Time = 400", () => clock.CurrentTime == 400);
AddStep("SeekBackward, Snap", () => composer.SeekBackward(true));
AddAssert("Time = 350", () => clock.CurrentTime == 350);
AddStep("SeekBackward, Snap", () => composer.SeekBackward(true));
AddAssert("Time = 175", () => clock.CurrentTime == 175);
AddStep("SeekBackward, Snap", () => composer.SeekBackward(true));
AddAssert("Time = 100", () => clock.CurrentTime == 100);
AddStep("SeekBackward, Snap", () => composer.SeekBackward(true));
AddAssert("Time = 50", () => clock.CurrentTime == 50);
AddStep("SeekBackward, Snap", () => composer.SeekBackward(true));
AddAssert("Time = 0", () => clock.CurrentTime == 0);
AddStep("Seek(450)", () => Clock.Seek(450));
AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true));
AddAssert("Time = 400", () => Clock.CurrentTime == 400);
AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true));
AddAssert("Time = 350", () => Clock.CurrentTime == 350);
AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true));
AddAssert("Time = 175", () => Clock.CurrentTime == 175);
AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true));
AddAssert("Time = 100", () => Clock.CurrentTime == 100);
AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true));
AddAssert("Time = 50", () => Clock.CurrentTime == 50);
AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true));
AddAssert("Time = 0", () => Clock.CurrentTime == 0);
}
/// <summary>
@ -294,18 +263,18 @@ namespace osu.Game.Tests.Visual
{
reset();
AddStep("Seek(451)", () => composer.SeekTo(451));
AddStep("SeekBackward, Snap", () => composer.SeekBackward(true));
AddAssert("Time = 450", () => clock.CurrentTime == 450);
AddStep("Seek(450.999)", () => composer.SeekTo(450.999));
AddStep("SeekBackward, Snap", () => composer.SeekBackward(true));
AddAssert("Time = 450", () => clock.CurrentTime == 450);
AddStep("Seek(401)", () => composer.SeekTo(401));
AddStep("SeekBackward, Snap", () => composer.SeekBackward(true));
AddAssert("Time = 400", () => clock.CurrentTime == 400);
AddStep("Seek(401.999)", () => composer.SeekTo(401.999));
AddStep("SeekBackward, Snap", () => composer.SeekBackward(true));
AddAssert("Time = 400", () => clock.CurrentTime == 400);
AddStep("Seek(451)", () => Clock.Seek(451));
AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true));
AddAssert("Time = 450", () => Clock.CurrentTime == 450);
AddStep("Seek(450.999)", () => Clock.Seek(450.999));
AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true));
AddAssert("Time = 450", () => Clock.CurrentTime == 450);
AddStep("Seek(401)", () => Clock.Seek(401));
AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true));
AddAssert("Time = 400", () => Clock.CurrentTime == 400);
AddStep("Seek(401.999)", () => Clock.Seek(401.999));
AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true));
AddAssert("Time = 400", () => Clock.CurrentTime == 400);
}
/// <summary>
@ -317,34 +286,34 @@ namespace osu.Game.Tests.Visual
double lastTime = 0;
AddStep("Seek(0)", () => composer.SeekTo(0));
AddStep("Seek(0)", () => Clock.Seek(0));
for (int i = 0; i < 20; i++)
{
AddStep("SeekForward, Snap", () =>
{
lastTime = clock.CurrentTime;
composer.SeekForward(true);
lastTime = Clock.CurrentTime;
Clock.SeekForward(true);
});
AddAssert("Time > lastTime", () => clock.CurrentTime > lastTime);
AddAssert("Time > lastTime", () => Clock.CurrentTime > lastTime);
}
for (int i = 0; i < 20; i++)
{
AddStep("SeekBackward, Snap", () =>
{
lastTime = clock.CurrentTime;
composer.SeekBackward(true);
lastTime = Clock.CurrentTime;
Clock.SeekBackward(true);
});
AddAssert("Time < lastTime", () => clock.CurrentTime < lastTime);
AddAssert("Time < lastTime", () => Clock.CurrentTime < lastTime);
}
AddAssert("Time = 0", () => clock.CurrentTime == 0);
AddAssert("Time = 0", () => Clock.CurrentTime == 0);
}
private void reset()
{
AddStep("Reset", () => composer.SeekTo(0));
AddStep("Reset", () => Clock.Seek(0));
}
private class TestHitObjectComposer : HitObjectComposer
@ -359,13 +328,13 @@ namespace osu.Game.Tests.Visual
private class TimingPointVisualiser : CompositeDrawable
{
private readonly Track track;
private readonly double length;
private readonly Drawable tracker;
public TimingPointVisualiser(Beatmap beatmap, Track track)
public TimingPointVisualiser(Beatmap beatmap, double length)
{
this.track = track;
this.length = length;
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
@ -417,7 +386,7 @@ namespace osu.Game.Tests.Visual
for (int i = 0; i < timingPoints.Count; i++)
{
TimingControlPoint next = i == timingPoints.Count - 1 ? null : timingPoints[i + 1];
timelineContainer.Add(new TimingPointTimeline(timingPoints[i], next?.Time ?? beatmap.HitObjects.Last().StartTime, track.Length));
timelineContainer.Add(new TimingPointTimeline(timingPoints[i], next?.Time ?? length, length));
}
}
@ -425,7 +394,7 @@ namespace osu.Game.Tests.Visual
{
base.Update();
tracker.X = (float)(Time.Current / track.Length);
tracker.X = (float)(Time.Current / length);
}
private class TimingPointTimeline : CompositeDrawable

View File

@ -6,36 +6,22 @@ using System.Collections.Generic;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using OpenTK;
using osu.Game.Screens.Edit.Components.Timelines.Summary;
using osu.Framework.Configuration;
using osu.Framework.Timing;
using osu.Game.Rulesets.Osu;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Tests.Visual
{
[TestFixture]
public class TestCaseEditorSummaryTimeline : OsuTestCase
public class TestCaseEditorSummaryTimeline : EditorClockTestCase
{
public override IReadOnlyList<Type> RequiredTypes => new[] { typeof(SummaryTimeline) };
private readonly Bindable<WorkingBeatmap> beatmap = new Bindable<WorkingBeatmap>();
private DependencyContainer dependencies;
protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent)
=> dependencies = new DependencyContainer(parent);
[BackgroundDependencyLoader]
private void load()
private void load(OsuGameBase osuGame)
{
beatmap.Value = new TestWorkingBeatmap(new OsuRuleset().RulesetInfo);
var clock = new DecoupleableInterpolatingFramedClock { IsCoupled = false };
dependencies.CacheAs<IAdjustableClock>(clock);
dependencies.CacheAs<IFrameBasedClock>(clock);
osuGame.Beatmap.Value = new TestWorkingBeatmap(new OsuRuleset().RulesetInfo);
SummaryTimeline summaryTimeline;
Add(summaryTimeline = new SummaryTimeline
@ -45,7 +31,7 @@ namespace osu.Game.Tests.Visual
Size = new Vector2(500, 50)
});
summaryTimeline.Beatmap.BindTo(beatmap);
summaryTimeline.Beatmap.BindTo(osuGame.Beatmap);
}
}
}

View File

@ -1,90 +0,0 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using NUnit.Framework;
using osu.Game.Beatmaps.ControlPoints;
namespace osu.Game.Tests.Visual
{
[TestFixture]
public class TestCaseGamefield : OsuTestCase
{
protected override void LoadComplete()
{
base.LoadComplete();
/*int time = 500;
for (int i = 0; i < 100; i++)
{
objects.Add(new HitCircle
{
StartTime = time,
Position = new Vector2(RNG.Next(0, (int)OsuPlayfield.BASE_SIZE.X), RNG.Next(0, (int)OsuPlayfield.BASE_SIZE.Y)),
Scale = RNG.NextSingle(0.5f, 1.0f),
});
time += RNG.Next(50, 500);
}*/
var controlPointInfo = new ControlPointInfo();
controlPointInfo.TimingPoints.Add(new TimingControlPoint
{
BeatLength = 200
});
/*WorkingBeatmap beatmap = new TestWorkingBeatmap(new Beatmap
{
HitObjects = objects,
BeatmapInfo = new BeatmapInfo
{
Difficulty = new BeatmapDifficulty(),
Ruleset = rulesets.Query<RulesetInfo>().First(),
Metadata = new BeatmapMetadata
{
Artist = @"Unknown",
Title = @"Sample Beatmap",
Author = @"peppy",
},
},
ControlPointInfo = controlPointInfo
});
AddRange(new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
//ensure we are at offset 0
Clock = new FramedClock(),
Children = new Drawable[]
{
new OsuRulesetContainer(new OsuRuleset(), beatmap, false)
{
Scale = new Vector2(0.5f),
Anchor = Anchor.TopLeft,
Origin = Anchor.TopLeft
},
new TaikoRulesetContainer(new TaikoRuleset(),beatmap, false)
{
Scale = new Vector2(0.5f),
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight
},
new CatchRulesetContainer(new CatchRuleset(),beatmap, false)
{
Scale = new Vector2(0.5f),
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft
},
new ManiaRulesetContainer(new ManiaRuleset(),beatmap, false)
{
Scale = new Vector2(0.5f),
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight
}
}
}
});*/
}
}
}

View File

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Timing;
@ -12,7 +13,6 @@ using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Edit;
using osu.Game.Rulesets.Osu.Edit.Layers.Selection.Overlays;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit.Screens.Compose.Layers;
using osu.Game.Tests.Beatmaps;
@ -20,19 +20,16 @@ using osu.Game.Tests.Beatmaps;
namespace osu.Game.Tests.Visual
{
[TestFixture]
public class TestCaseEditorSelectionLayer : OsuTestCase
public class TestCaseHitObjectComposer : OsuTestCase
{
public override IReadOnlyList<Type> RequiredTypes => new[]
{
typeof(SelectionLayer),
typeof(SelectionBox),
typeof(MaskSelection),
typeof(DragLayer),
typeof(HitObjectComposer),
typeof(OsuHitObjectComposer),
typeof(HitObjectMaskLayer),
typeof(HitObjectMask),
typeof(HitCircleMask),
typeof(SliderMask),
typeof(SliderCircleMask)
typeof(NotNullAttribute)
};
private DependencyContainer dependencies;

View File

@ -35,11 +35,13 @@ namespace osu.Game.Database
/// <summary>
/// Fired when a new <see cref="TModel"/> becomes available in the database.
/// This is not guaranteed to run on the update thread.
/// </summary>
public event Action<TModel> ItemAdded;
/// <summary>
/// Fired when a <see cref="TModel"/> is removed from the database.
/// This is not guaranteed to run on the update thread.
/// </summary>
public event Action<TModel> ItemRemoved;

View File

@ -10,7 +10,7 @@ namespace osu.Game.Graphics.UserInterface
{
public BackButton()
{
Text = @"Back";
Text = @"back";
Icon = FontAwesome.fa_osu_left_o;
Anchor = Anchor.BottomLeft;
Origin = Anchor.BottomLeft;

View File

@ -37,13 +37,7 @@ namespace osu.Game.Online.API
public Bindable<User> LocalUser { get; } = new Bindable<User>(createGuestUser());
public string Token
{
get { return authentication.Token?.ToString(); }
set { authentication.Token = string.IsNullOrEmpty(value) ? null : OAuthToken.Parse(value); }
}
protected bool HasLogin => Token != null || !string.IsNullOrEmpty(ProvidedUsername) && !string.IsNullOrEmpty(password);
protected bool HasLogin => authentication.Token.Value != null || !string.IsNullOrEmpty(ProvidedUsername) && !string.IsNullOrEmpty(password);
private readonly CancellationTokenSource cancellationToken = new CancellationTokenSource();
@ -57,11 +51,15 @@ namespace osu.Game.Online.API
log = Logger.GetLogger(LoggingTarget.Network);
ProvidedUsername = config.Get<string>(OsuSetting.Username);
Token = config.Get<string>(OsuSetting.Token);
authentication.TokenString = config.Get<string>(OsuSetting.Token);
authentication.Token.ValueChanged += onTokenChanged;
Task.Factory.StartNew(run, cancellationToken.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default);
}
private void onTokenChanged(OAuthToken token) => config.Set(OsuSetting.Token, config.Get<bool>(OsuSetting.SavePassword) ? authentication.TokenString : string.Empty);
private readonly List<IOnlineComponent> components = new List<IOnlineComponent>();
internal new void Schedule(Action action) => base.Schedule(action);
@ -306,9 +304,6 @@ namespace osu.Game.Online.API
{
base.Dispose(isDisposing);
config.Set(OsuSetting.Token, config.Get<bool>(OsuSetting.SavePassword) ? Token : string.Empty);
config.Save();
flushQueue();
cancellationToken.Cancel();
}

View File

@ -2,6 +2,7 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Diagnostics;
using osu.Framework.Configuration;
using osu.Framework.IO.Network;
namespace osu.Game.Online.API
@ -12,7 +13,13 @@ namespace osu.Game.Online.API
private readonly string clientSecret;
private readonly string endpoint;
public OAuthToken Token;
public readonly Bindable<OAuthToken> Token = new Bindable<OAuthToken>();
public string TokenString
{
get => Token.Value?.ToString();
set => Token.Value = string.IsNullOrEmpty(value) ? null : OAuthToken.Parse(value);
}
internal OAuth(string clientId, string clientSecret, string endpoint)
{
@ -47,7 +54,7 @@ namespace osu.Game.Online.API
return false;
}
Token = req.ResponseObject;
Token.Value = req.ResponseObject;
return true;
}
}
@ -66,14 +73,14 @@ namespace osu.Game.Online.API
{
req.Perform();
Token = req.ResponseObject;
Token.Value = req.ResponseObject;
return true;
}
}
catch
{
//todo: potentially only kill the refresh token on certain exception types.
Token = null;
Token.Value = null;
return false;
}
}
@ -95,15 +102,15 @@ namespace osu.Game.Online.API
if (accessTokenValid) return true;
// if not, let's try using our refresh token to request a new access token.
if (!string.IsNullOrEmpty(Token?.RefreshToken))
if (!string.IsNullOrEmpty(Token.Value?.RefreshToken))
// ReSharper disable once PossibleNullReferenceException
AuthenticateWithRefresh(Token.RefreshToken);
AuthenticateWithRefresh(Token.Value.RefreshToken);
return accessTokenValid;
}
}
private bool accessTokenValid => Token?.IsValid ?? false;
private bool accessTokenValid => Token.Value?.IsValid ?? false;
internal bool HasValidAccessToken => RequestAccessToken() != null;
@ -111,12 +118,12 @@ namespace osu.Game.Online.API
{
if (!ensureAccessToken()) return null;
return Token.AccessToken;
return Token.Value.AccessToken;
}
internal void Clear()
{
Token = null;
Token.Value = null;
}
private class AccessTokenRequestRefresh : AccessTokenRequest

View File

@ -36,6 +36,10 @@ using osu.Game.Overlays.Volume;
namespace osu.Game
{
/// <summary>
/// The full osu! experience. Builds on top of <see cref="OsuGameBase"/> to add menus and binding logic
/// for initial components that are generally retrieved via DI.
/// </summary>
public class OsuGame : OsuGameBase, IKeyBindingHandler<GlobalAction>
{
public Toolbar Toolbar;

View File

@ -34,6 +34,11 @@ using osu.Game.Skinning;
namespace osu.Game
{
/// <summary>
/// The most basic <see cref="Game"/> that can be used to host osu! components and systems.
/// Unlike <see cref="OsuGame"/>, this class will not load any kind of UI, allowing it to be used
/// for provide dependencies to test cases without interfering with them.
/// </summary>
public class OsuGameBase : Framework.Game, ICanAcceptFiles
{
protected OsuConfigManager LocalConfig;

View File

@ -44,7 +44,7 @@ namespace osu.Game.Overlays.BeatmapSet
Beatmap.Value = BeatmapSet.Beatmaps.First();
plays.Value = BeatmapSet.OnlineInfo.PlayCount;
favourites.Value = BeatmapSet.OnlineInfo.FavouriteCount;
difficulties.ChildrenEnumerable = BeatmapSet.Beatmaps.Select(b => new DifficultySelectorButton(b)
difficulties.ChildrenEnumerable = BeatmapSet.Beatmaps.OrderBy(beatmap => beatmap.StarDifficulty).Select(b => new DifficultySelectorButton(b)
{
State = DifficultySelectorState.NotSelected,
OnHovered = beatmap =>

View File

@ -246,11 +246,11 @@ namespace osu.Game.Overlays.BeatmapSet
if (beatmaps != null) beatmaps.ItemAdded -= handleBeatmapAdd;
}
private void handleBeatmapAdd(BeatmapSetInfo beatmap)
private void handleBeatmapAdd(BeatmapSetInfo beatmap) => Schedule(() =>
{
if (beatmap.OnlineBeatmapSetID == BeatmapSet?.OnlineBeatmapSetID)
downloadButtonsContainer.FadeOut(transition_duration);
}
});
private void download(bool noVideo)
{

View File

@ -2,6 +2,7 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using OpenTK;
@ -208,7 +209,7 @@ namespace osu.Game.Overlays.Direct
{
var icons = new List<DifficultyIcon>();
foreach (var b in SetInfo.Beatmaps)
foreach (var b in SetInfo.Beatmaps.OrderBy(beatmap => beatmap.StarDifficulty))
icons.Add(new DifficultyIcon(b));
return icons;

View File

@ -188,12 +188,12 @@ namespace osu.Game.Overlays
beatmaps.ItemAdded += setAdded;
}
private void setAdded(BeatmapSetInfo set)
private void setAdded(BeatmapSetInfo set) => Schedule(() =>
{
// if a new map was imported, we should remove it from search results (download completed etc.)
panels?.FirstOrDefault(p => p.SetInfo.OnlineBeatmapSetID == set.OnlineBeatmapSetID)?.FadeOut(400).Expire();
BeatmapSets = BeatmapSets?.Where(b => b.OnlineBeatmapSetID != set.OnlineBeatmapSetID);
}
});
private void updateResultCounts()
{
@ -323,6 +323,14 @@ namespace osu.Game.Overlays
private int distinctCount(List<string> list) => list.Distinct().ToArray().Length;
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (beatmaps != null)
beatmaps.ItemAdded -= setAdded;
}
public class ResultCounts
{
public readonly int Artists;

View File

@ -74,8 +74,8 @@ namespace osu.Game.Overlays.Music
},
};
beatmaps.ItemAdded += list.AddBeatmapSet;
beatmaps.ItemRemoved += list.RemoveBeatmapSet;
beatmaps.ItemAdded += handleBeatmapAdded;
beatmaps.ItemRemoved += handleBeatmapRemoved;
list.BeatmapSets = beatmaps.GetAllUsableBeatmapSets();
@ -95,6 +95,9 @@ namespace osu.Game.Overlays.Music
beatmapBacking.TriggerChange();
}
private void handleBeatmapAdded(BeatmapSetInfo setInfo) => Schedule(() => list.AddBeatmapSet(setInfo));
private void handleBeatmapRemoved(BeatmapSetInfo setInfo) => Schedule(() => list.RemoveBeatmapSet(setInfo));
protected override void PopIn()
{
filter.Search.HoldFocus = true;
@ -153,6 +156,17 @@ namespace osu.Game.Overlays.Music
track.Restart();
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (beatmaps != null)
{
beatmaps.ItemAdded -= handleBeatmapAdded;
beatmaps.ItemRemoved -= handleBeatmapRemoved;
}
}
}
//todo: placeholder

View File

@ -21,9 +21,13 @@ namespace osu.Game.Overlays.Settings.Sections
public override FontAwesome Icon => FontAwesome.fa_paint_brush;
private SkinManager skins;
[BackgroundDependencyLoader]
private void load(OsuConfigManager config, SkinManager skins)
{
this.skins = skins;
FlowContent.Spacing = new Vector2(0, 5);
Children = new Drawable[]
{
@ -47,15 +51,29 @@ namespace osu.Game.Overlays.Settings.Sections
},
};
void reloadSkins() => skinDropdown.Items = skins.GetAllUsableSkins().Select(s => new KeyValuePair<string, int>(s.ToString(), s.ID));
skins.ItemAdded += _ => reloadSkins();
skins.ItemRemoved += _ => reloadSkins();
skins.ItemAdded += onItemsChanged;
skins.ItemRemoved += onItemsChanged;
reloadSkins();
skinDropdown.Bindable = config.GetBindable<int>(OsuSetting.Skin);
}
private void reloadSkins() => skinDropdown.Items = skins.GetAllUsableSkins().Select(s => new KeyValuePair<string, int>(s.ToString(), s.ID));
private void onItemsChanged(SkinInfo _) => Schedule(reloadSkins);
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (skins != null)
{
skins.ItemAdded -= onItemsChanged;
skins.ItemRemoved -= onItemsChanged;
}
}
private class SizeSlider : OsuSliderBar<double>
{
public override string TooltipText => Current.Value.ToString(@"0.##x");

View File

@ -0,0 +1,11 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Runtime.CompilerServices;
// We publish our internal attributes to other sub-projects of the framework.
// Note, that we omit visual tests as they are meant to test the framework
// behavior "in the wild".
[assembly: InternalsVisibleTo("osu.Game.Tests")]
[assembly: InternalsVisibleTo("osu.Game.Tests.Dynamic")]

View File

@ -4,20 +4,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Configuration;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
using osu.Framework.Logging;
using osu.Framework.MathUtils;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Edit.Screens.Compose;
using osu.Game.Screens.Edit.Screens.Compose.Layers;
using osu.Game.Screens.Edit.Screens.Compose.RadioButtons;
@ -33,9 +29,6 @@ namespace osu.Game.Rulesets.Edit
private readonly List<Container> layerContainers = new List<Container>();
private readonly Bindable<WorkingBeatmap> beatmap = new Bindable<WorkingBeatmap>();
private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor();
private IAdjustableClock adjustableClock;
protected HitObjectComposer(Ruleset ruleset)
{
@ -44,14 +37,9 @@ namespace osu.Game.Rulesets.Edit
RelativeSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader(true)]
private void load([NotNull] OsuGameBase osuGame, [NotNull] IAdjustableClock adjustableClock, [NotNull] IFrameBasedClock framedClock, [CanBeNull] BindableBeatDivisor beatDivisor)
[BackgroundDependencyLoader]
private void load(OsuGameBase osuGame, IFrameBasedClock framedClock)
{
this.adjustableClock = adjustableClock;
if (beatDivisor != null)
this.beatDivisor.BindTo(beatDivisor);
beatmap.BindTo(osuGame.Beatmap);
try
@ -65,9 +53,6 @@ namespace osu.Game.Rulesets.Edit
return;
}
HitObjectMaskLayer hitObjectMaskLayer = new HitObjectMaskLayer(this);
SelectionLayer selectionLayer = new SelectionLayer(rulesetContainer.Playfield);
var layerBelowRuleset = new BorderLayer
{
RelativeSizeAxes = Axes.Both,
@ -75,12 +60,7 @@ namespace osu.Game.Rulesets.Edit
};
var layerAboveRuleset = CreateLayerContainer();
layerAboveRuleset.Children = new Drawable[]
{
selectionLayer, // Below object overlays for input
hitObjectMaskLayer,
selectionLayer.CreateProxy() // Proxy above object overlays for selections
};
layerAboveRuleset.Child = new HitObjectMaskLayer(rulesetContainer.Playfield, this);
layerContainers.Add(layerBelowRuleset);
layerContainers.Add(layerAboveRuleset);
@ -122,11 +102,6 @@ namespace osu.Game.Rulesets.Edit
}
};
selectionLayer.ObjectSelected += hitObjectMaskLayer.AddOverlay;
selectionLayer.ObjectDeselected += hitObjectMaskLayer.RemoveOverlay;
selectionLayer.SelectionCleared += hitObjectMaskLayer.RemoveSelectionOverlay;
selectionLayer.SelectionFinished += hitObjectMaskLayer.AddSelectionOverlay;
toolboxCollection.Items =
CompositionTools.Select(t => new RadioButton(t.Name, () => setCompositionTool(t)))
.Prepend(new RadioButton("Select", () => setCompositionTool(null)))
@ -148,106 +123,6 @@ namespace osu.Game.Rulesets.Edit
});
}
protected override bool OnWheel(InputState state)
{
if (state.Mouse.WheelDelta > 0)
SeekBackward(true);
else
SeekForward(true);
return true;
}
/// <summary>
/// Seeks the current time one beat-snapped beat-length backwards.
/// </summary>
/// <param name="snapped">Whether to snap to the closest beat.</param>
public void SeekBackward(bool snapped = false) => seek(-1, snapped);
/// <summary>
/// Seeks the current time one beat-snapped beat-length forwards.
/// </summary>
/// <param name="snapped">Whether to snap to the closest beat.</param>
public void SeekForward(bool snapped = false) => seek(1, snapped);
private void seek(int direction, bool snapped)
{
var cpi = beatmap.Value.Beatmap.ControlPointInfo;
var timingPoint = cpi.TimingPointAt(adjustableClock.CurrentTime);
if (direction < 0 && timingPoint.Time == adjustableClock.CurrentTime)
{
// When going backwards and we're at the boundary of two timing points, we compute the seek distance with the timing point which we are seeking into
int activeIndex = cpi.TimingPoints.IndexOf(timingPoint);
while (activeIndex > 0 && adjustableClock.CurrentTime == timingPoint.Time)
timingPoint = cpi.TimingPoints[--activeIndex];
}
double seekAmount = timingPoint.BeatLength / beatDivisor;
double seekTime = adjustableClock.CurrentTime + seekAmount * direction;
if (!snapped || cpi.TimingPoints.Count == 0)
{
adjustableClock.Seek(seekTime);
return;
}
// We will be snapping to beats within timingPoint
seekTime -= timingPoint.Time;
// Determine the index from timingPoint of the closest beat to seekTime, accounting for scrolling direction
int closestBeat;
if (direction > 0)
closestBeat = (int)Math.Floor(seekTime / seekAmount);
else
closestBeat = (int)Math.Ceiling(seekTime / seekAmount);
seekTime = timingPoint.Time + closestBeat * seekAmount;
// Due to the rounding above, we may end up on the current beat. This will effectively cause 0 seeking to happen, but we don't want this.
// Instead, we'll go to the next beat in the direction when this is the case
if (Precision.AlmostEquals(adjustableClock.CurrentTime, seekTime))
{
closestBeat += direction > 0 ? 1 : -1;
seekTime = timingPoint.Time + closestBeat * seekAmount;
}
if (seekTime < timingPoint.Time && timingPoint != cpi.TimingPoints.First())
seekTime = timingPoint.Time;
var nextTimingPoint = cpi.TimingPoints.FirstOrDefault(t => t.Time > timingPoint.Time);
if (seekTime > nextTimingPoint?.Time)
seekTime = nextTimingPoint.Time;
adjustableClock.Seek(seekTime);
}
public void SeekTo(double seekTime, bool snapped = false)
{
if (!snapped)
{
adjustableClock.Seek(seekTime);
return;
}
var timingPoint = beatmap.Value.Beatmap.ControlPointInfo.TimingPointAt(seekTime);
double beatSnapLength = timingPoint.BeatLength / beatDivisor;
// We will be snapping to beats within the timing point
seekTime -= timingPoint.Time;
// Determine the index from the current timing point of the closest beat to seekTime
int closestBeat = (int)Math.Round(seekTime / beatSnapLength);
seekTime = timingPoint.Time + closestBeat * beatSnapLength;
// Depending on beatSnapLength, we may snap to a beat that is beyond timingPoint's end time, but we want to instead snap to
// the next timing point's start time
var nextTimingPoint = beatmap.Value.Beatmap.ControlPointInfo.TimingPoints.FirstOrDefault(t => t.Time > timingPoint.Time);
if (seekTime > nextTimingPoint?.Time)
seekTime = nextTimingPoint.Time;
adjustableClock.Seek(seekTime);
}
private void setCompositionTool(ICompositionTool tool) => CurrentTool = tool;
protected virtual RulesetContainer CreateRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap) => ruleset.CreateRulesetContainerWith(beatmap, true);
@ -261,11 +136,10 @@ namespace osu.Game.Rulesets.Edit
public virtual HitObjectMask CreateMaskFor(DrawableHitObject hitObject) => null;
/// <summary>
/// Creates a <see cref="SelectionBox"/> which outlines <see cref="DrawableHitObject"/>s
/// and handles all hitobject movement/pattern adjustments.
/// Creates a <see cref="MaskSelection"/> which outlines <see cref="DrawableHitObject"/>s
/// and handles hitobject pattern adjustments.
/// </summary>
/// <param name="overlays">The <see cref="DrawableHitObject"/> overlays.</param>
public virtual SelectionBox CreateSelectionOverlay(IReadOnlyList<HitObjectMask> overlays) => new SelectionBox(overlays);
public virtual MaskSelection CreateMaskSelection() => new MaskSelection();
/// <summary>
/// Creates a <see cref="ScalableContainer"/> which provides a layer above or below the <see cref="Playfield"/>.

View File

@ -1,21 +1,146 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using osu.Framework;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Input;
using osu.Game.Rulesets.Objects.Drawables;
using OpenTK;
namespace osu.Game.Rulesets.Edit
{
/// <summary>
/// A mask placed above a <see cref="DrawableHitObject"/> adding editing functionality.
/// </summary>
public class HitObjectMask : Container
public class HitObjectMask : CompositeDrawable, IStateful<SelectionState>
{
/// <summary>
/// Invoked when this <see cref="HitObjectMask"/> has been selected.
/// </summary>
public event Action<HitObjectMask> Selected;
/// <summary>
/// Invoked when this <see cref="HitObjectMask"/> has been deselected.
/// </summary>
public event Action<HitObjectMask> Deselected;
/// <summary>
/// Invoked when this <see cref="HitObjectMask"/> has requested selection.
/// Will fire even if already selected. Does not actually perform selection.
/// </summary>
public event Action<HitObjectMask, InputState> SelectionRequested;
/// <summary>
/// Invoked when this <see cref="HitObjectMask"/> has requested drag.
/// </summary>
public event Action<HitObjectMask, InputState> DragRequested;
/// <summary>
/// The <see cref="DrawableHitObject"/> which this <see cref="HitObjectMask"/> applies to.
/// </summary>
public readonly DrawableHitObject HitObject;
protected override bool ShouldBeAlive => HitObject.IsAlive && HitObject.IsPresent || State == SelectionState.Selected;
public override bool HandleMouseInput => ShouldBeAlive;
public override bool RemoveWhenNotAlive => false;
public HitObjectMask(DrawableHitObject hitObject)
{
HitObject = hitObject;
AlwaysPresent = true;
Alpha = 0;
}
private SelectionState state;
public event Action<SelectionState> StateChanged;
public SelectionState State
{
get => state;
set
{
if (state == value) return;
state = value;
switch (state)
{
case SelectionState.Selected:
Show();
Selected?.Invoke(this);
break;
case SelectionState.NotSelected:
Hide();
Deselected?.Invoke(this);
break;
}
}
}
/// <summary>
/// Selects this <see cref="HitObjectMask"/>, causing it to become visible.
/// </summary>
public void Select() => State = SelectionState.Selected;
/// <summary>
/// Deselects this <see cref="HitObjectMask"/>, causing it to become invisible.
/// </summary>
public void Deselect() => State = SelectionState.NotSelected;
public bool IsSelected => State == SelectionState.Selected;
private bool selectionRequested;
protected override bool OnMouseDown(InputState state, MouseDownEventArgs args)
{
selectionRequested = false;
if (State == SelectionState.NotSelected)
{
SelectionRequested?.Invoke(this, state);
selectionRequested = true;
}
return IsSelected;
}
protected override bool OnClick(InputState state)
{
if (State == SelectionState.Selected && !selectionRequested)
{
selectionRequested = true;
SelectionRequested?.Invoke(this, state);
return true;
}
return base.OnClick(state);
}
protected override bool OnDragStart(InputState state) => true;
protected override bool OnDrag(InputState state)
{
DragRequested?.Invoke(this, state);
return true;
}
/// <summary>
/// The screen-space point that causes this <see cref="HitObjectMask"/> to be selected.
/// </summary>
public virtual Vector2 SelectionPoint => ScreenSpaceDrawQuad.Centre;
/// <summary>
/// The screen-space quad that outlines this <see cref="HitObjectMask"/> for selections.
/// </summary>
public virtual Quad SelectionQuad => ScreenSpaceDrawQuad;
}
public enum SelectionState
{
NotSelected,
Selected
}
}

View File

@ -6,14 +6,12 @@ using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Configuration;
using osu.Framework.Graphics.Primitives;
using osu.Game.Audio;
using osu.Game.Graphics;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning;
using OpenTK;
using OpenTK.Graphics;
namespace osu.Game.Rulesets.Objects.Drawables
@ -231,16 +229,6 @@ namespace osu.Game.Rulesets.Objects.Drawables
protected virtual void CheckForJudgements(bool userTriggered, double timeOffset)
{
}
/// <summary>
/// The screen-space point that causes this <see cref="DrawableHitObject"/> to be selected in the Editor.
/// </summary>
public virtual Vector2 SelectionPoint => ScreenSpaceDrawQuad.Centre;
/// <summary>
/// The screen-space quad that outlines this <see cref="DrawableHitObject"/> for selections in the Editor.
/// </summary>
public virtual Quad SelectionQuad => ScreenSpaceDrawQuad;
}
public abstract class DrawableHitObject<TObject> : DrawableHitObject

View File

@ -55,11 +55,9 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
if (Beatmap.Value.Track.Length == double.PositiveInfinity) return;
float markerPos = MathHelper.Clamp(ToLocalSpace(screenPosition).X, 0, DrawWidth);
seekTo(markerPos / DrawWidth * Beatmap.Value.Track.Length);
adjustableClock.Seek(markerPos / DrawWidth * Beatmap.Value.Track.Length);
}
private void seekTo(double time) => adjustableClock.Seek(time);
protected override void Update()
{
base.Update();

View File

@ -12,6 +12,7 @@ using osu.Game.Screens.Edit.Menus;
using osu.Game.Screens.Edit.Components.Timelines.Summary;
using osu.Framework.Allocation;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input;
using osu.Framework.Timing;
using osu.Game.Graphics.UserInterface;
using osu.Game.Screens.Edit.Screens;
@ -32,6 +33,10 @@ namespace osu.Game.Screens.Edit
private EditorScreen currentScreen;
private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor();
private EditorClock clock;
private DependencyContainer dependencies;
protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent)
@ -42,11 +47,12 @@ namespace osu.Game.Screens.Edit
{
// TODO: should probably be done at a RulesetContainer level to share logic with Player.
var sourceClock = (IAdjustableClock)Beatmap.Value.Track ?? new StopwatchClock();
var adjustableClock = new DecoupleableInterpolatingFramedClock { IsCoupled = false };
adjustableClock.ChangeSource(sourceClock);
clock = new EditorClock(Beatmap.Value.Beatmap.ControlPointInfo, beatDivisor) { IsCoupled = false };
clock.ChangeSource(sourceClock);
dependencies.CacheAs<IAdjustableClock>(adjustableClock);
dependencies.CacheAs<IFrameBasedClock>(adjustableClock);
dependencies.CacheAs<IFrameBasedClock>(clock);
dependencies.CacheAs<IAdjustableClock>(clock);
dependencies.Cache(beatDivisor);
EditorMenuBar menuBar;
TimeInfoContainer timeInfo;
@ -147,7 +153,6 @@ namespace osu.Game.Screens.Edit
menuBar.Mode.ValueChanged += onModeChanged;
bottomBackground.Colour = colours.Gray2;
}
private void exportBeatmap()
@ -176,6 +181,15 @@ namespace osu.Game.Screens.Edit
screenContainer.Add(currentScreen);
}
protected override bool OnWheel(InputState state)
{
if (state.Mouse.WheelDelta > 0)
clock.SeekBackward(true);
else
clock.SeekForward(true);
return true;
}
protected override void OnResuming(Screen last)
{
Beatmap.Value.Track?.Stop();

View File

@ -0,0 +1,117 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using System.Linq;
using osu.Framework.MathUtils;
using osu.Framework.Timing;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Screens.Edit.Screens.Compose;
namespace osu.Game.Screens.Edit
{
/// <summary>
/// A decoupled clock which adds editor-specific functionality, such as snapping to a user-defined beat divisor.
/// </summary>
public class EditorClock : DecoupleableInterpolatingFramedClock
{
public ControlPointInfo ControlPointInfo;
private readonly BindableBeatDivisor beatDivisor;
public EditorClock(ControlPointInfo controlPointInfo, BindableBeatDivisor beatDivisor)
{
this.beatDivisor = beatDivisor;
ControlPointInfo = controlPointInfo;
}
/// <summary>
/// Seek to the closest snappable beat from a time.
/// </summary>
/// <param name="position">The raw position which should be seeked around.</param>
/// <returns>Whether the seek could be performed.</returns>
public bool SeekSnapped(double position)
{
var timingPoint = ControlPointInfo.TimingPointAt(position);
double beatSnapLength = timingPoint.BeatLength / beatDivisor;
// We will be snapping to beats within the timing point
position -= timingPoint.Time;
// Determine the index from the current timing point of the closest beat to position
int closestBeat = (int)Math.Round(position / beatSnapLength);
position = timingPoint.Time + closestBeat * beatSnapLength;
// Depending on beatSnapLength, we may snap to a beat that is beyond timingPoint's end time, but we want to instead snap to
// the next timing point's start time
var nextTimingPoint = ControlPointInfo.TimingPoints.FirstOrDefault(t => t.Time > timingPoint.Time);
if (position > nextTimingPoint?.Time)
position = nextTimingPoint.Time;
return Seek(position);
}
/// <summary>
/// Seeks backwards by one beat length.
/// </summary>
/// <param name="snapped">Whether to snap to the closest beat after seeking.</param>
public void SeekBackward(bool snapped = false) => seek(-1, snapped);
/// <summary>
/// Seeks forwards by one beat length.
/// </summary>
/// <param name="snapped">Whether to snap to the closest beat after seeking.</param>
public void SeekForward(bool snapped = false) => seek(1, snapped);
private void seek(int direction, bool snapped)
{
var timingPoint = ControlPointInfo.TimingPointAt(CurrentTime);
if (direction < 0 && timingPoint.Time == CurrentTime)
{
// When going backwards and we're at the boundary of two timing points, we compute the seek distance with the timing point which we are seeking into
int activeIndex = ControlPointInfo.TimingPoints.IndexOf(timingPoint);
while (activeIndex > 0 && CurrentTime == timingPoint.Time)
timingPoint = ControlPointInfo.TimingPoints[--activeIndex];
}
double seekAmount = timingPoint.BeatLength / beatDivisor;
double seekTime = CurrentTime + seekAmount * direction;
if (!snapped || ControlPointInfo.TimingPoints.Count == 0)
{
Seek(seekTime);
return;
}
// We will be snapping to beats within timingPoint
seekTime -= timingPoint.Time;
// Determine the index from timingPoint of the closest beat to seekTime, accounting for scrolling direction
int closestBeat;
if (direction > 0)
closestBeat = (int)Math.Floor(seekTime / seekAmount);
else
closestBeat = (int)Math.Ceiling(seekTime / seekAmount);
seekTime = timingPoint.Time + closestBeat * seekAmount;
// Due to the rounding above, we may end up on the current beat. This will effectively cause 0 seeking to happen, but we don't want this.
// Instead, we'll go to the next beat in the direction when this is the case
if (Precision.AlmostEquals(CurrentTime, seekTime))
{
closestBeat += direction > 0 ? 1 : -1;
seekTime = timingPoint.Time + closestBeat * seekAmount;
}
if (seekTime < timingPoint.Time && timingPoint != ControlPointInfo.TimingPoints.First())
seekTime = timingPoint.Time;
var nextTimingPoint = ControlPointInfo.TimingPoints.FirstOrDefault(t => t.Time > timingPoint.Time);
if (seekTime > nextTimingPoint?.Time)
seekTime = nextTimingPoint.Time;
Seek(seekTime);
}
}
}

View File

@ -1,6 +1,7 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using JetBrains.Annotations;
using osu.Framework.Allocation;
using OpenTK.Graphics;
using osu.Framework.Extensions.Color4Extensions;
@ -21,15 +22,11 @@ namespace osu.Game.Screens.Edit.Screens.Compose
private Container composerContainer;
private DependencyContainer dependencies;
protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent)
=> dependencies = new DependencyContainer(parent);
[BackgroundDependencyLoader]
private void load()
[BackgroundDependencyLoader(true)]
private void load([CanBeNull] BindableBeatDivisor beatDivisor)
{
dependencies.Cache(beatDivisor);
if (beatDivisor != null)
this.beatDivisor.BindTo(beatDivisor);
ScrollableTimeline timeline;
Children = new Drawable[]

View File

@ -0,0 +1,92 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input;
using osu.Game.Rulesets.Edit;
using OpenTK.Graphics;
namespace osu.Game.Screens.Edit.Screens.Compose.Layers
{
/// <summary>
/// A layer that handles and displays drag selection for a collection of <see cref="HitObjectMask"/>s.
/// </summary>
public class DragLayer : CompositeDrawable
{
private readonly Action<RectangleF> performSelection;
/// <summary>
/// Invoked when the drag selection has finished.
/// </summary>
public event Action DragEnd;
private Drawable box;
/// <summary>
/// Creates a new <see cref="DragLayer"/>.
/// </summary>
/// <param name="maskContainer">The selectable <see cref="HitObjectMask"/>s.</param>
public DragLayer(Action<RectangleF> performSelection)
{
this.performSelection = performSelection;
RelativeSizeAxes = Axes.Both;
AlwaysPresent = true;
Alpha = 0;
}
[BackgroundDependencyLoader]
private void load()
{
InternalChild = box = new Container
{
Masking = true,
BorderColour = Color4.White,
BorderThickness = MaskSelection.BORDER_RADIUS,
Child = new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0.1f
}
};
}
protected override bool OnDragStart(InputState state)
{
this.FadeIn(250, Easing.OutQuint);
return true;
}
protected override bool OnDrag(InputState state)
{
var dragPosition = state.Mouse.NativeState.Position;
var dragStartPosition = state.Mouse.NativeState.PositionMouseDown ?? dragPosition;
var dragQuad = new Quad(dragStartPosition.X, dragStartPosition.Y, dragPosition.X - dragStartPosition.X, dragPosition.Y - dragStartPosition.Y);
// We use AABBFloat instead of RectangleF since it handles negative sizes for us
var dragRectangle = dragQuad.AABBFloat;
var topLeft = ToLocalSpace(dragRectangle.TopLeft);
var bottomRight = ToLocalSpace(dragRectangle.BottomRight);
box.Position = topLeft;
box.Size = bottomRight - topLeft;
performSelection?.Invoke(dragRectangle);
return true;
}
protected override bool OnDragEnd(InputState state)
{
this.FadeOut(250, Easing.OutQuint);
DragEnd?.Invoke();
return true;
}
}
}

View File

@ -2,65 +2,92 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI;
namespace osu.Game.Screens.Edit.Screens.Compose.Layers
{
public class HitObjectMaskLayer : CompositeDrawable
{
private readonly Playfield playfield;
private readonly HitObjectComposer composer;
private readonly Container<HitObjectMask> overlayContainer;
public HitObjectMaskLayer(HitObjectComposer composer)
private MaskContainer maskContainer;
public HitObjectMaskLayer(Playfield playfield, HitObjectComposer composer)
{
// we need the playfield as HitObjects may not be initialised until its BDL.
this.playfield = playfield;
this.composer = composer;
RelativeSizeAxes = Axes.Both;
}
InternalChild = overlayContainer = new Container<HitObjectMask> { RelativeSizeAxes = Axes.Both };
[BackgroundDependencyLoader]
private void load()
{
maskContainer = new MaskContainer();
var maskSelection = composer.CreateMaskSelection();
maskContainer.MaskSelected += maskSelection.HandleSelected;
maskContainer.MaskDeselected += maskSelection.HandleDeselected;
maskContainer.MaskSelectionRequested += maskSelection.HandleSelectionRequested;
maskContainer.MaskDragRequested += maskSelection.HandleDrag;
maskSelection.DeselectAll = maskContainer.DeselectAll;
var dragLayer = new DragLayer(maskContainer.Select);
dragLayer.DragEnd += () => maskSelection.UpdateVisibility();
InternalChildren = new Drawable[]
{
dragLayer,
maskSelection,
maskContainer,
dragLayer.CreateProxy()
};
foreach (var obj in playfield.HitObjects.Objects)
addMask(obj);
}
protected override bool OnMouseDown(InputState state, MouseDownEventArgs args)
{
maskContainer.DeselectAll();
return true;
}
/// <summary>
/// Adds an overlay for a <see cref="DrawableHitObject"/> which adds movement support.
/// Adds a mask for a <see cref="DrawableHitObject"/> which adds movement support.
/// </summary>
/// <param name="hitObject">The <see cref="DrawableHitObject"/> to create an overlay for.</param>
public void AddOverlay(DrawableHitObject hitObject)
/// <param name="hitObject">The <see cref="DrawableHitObject"/> to create a mask for.</param>
private void addMask(DrawableHitObject hitObject)
{
var overlay = composer.CreateMaskFor(hitObject);
if (overlay == null)
var mask = composer.CreateMaskFor(hitObject);
if (mask == null)
return;
overlayContainer.Add(overlay);
maskContainer.Add(mask);
}
/// <summary>
/// Removes the overlay for a <see cref="DrawableHitObject"/>.
/// Removes the mask for a <see cref="DrawableHitObject"/>.
/// </summary>
/// <param name="hitObject">The <see cref="DrawableHitObject"/> to remove the overlay for.</param>
public void RemoveOverlay(DrawableHitObject hitObject)
/// <param name="hitObject">The <see cref="DrawableHitObject"/> to remove the mask for.</param>
private void removeMask(DrawableHitObject hitObject)
{
var existing = overlayContainer.FirstOrDefault(h => h.HitObject == hitObject);
if (existing == null)
var mask = maskContainer.FirstOrDefault(h => h.HitObject == hitObject);
if (mask == null)
return;
existing.Hide();
existing.Expire();
}
private SelectionBox currentSelectionBox;
public void AddSelectionOverlay()
{
if (overlayContainer.Count > 0)
AddInternal(currentSelectionBox = composer.CreateSelectionOverlay(overlayContainer));
}
public void RemoveSelectionOverlay()
{
currentSelectionBox?.Hide();
currentSelectionBox?.Expire();
maskContainer.Remove(mask);
}
}
}

View File

@ -0,0 +1,123 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
using osu.Game.Rulesets.Edit;
using RectangleF = osu.Framework.Graphics.Primitives.RectangleF;
namespace osu.Game.Screens.Edit.Screens.Compose.Layers
{
public class MaskContainer : Container<HitObjectMask>
{
/// <summary>
/// Invoked when any <see cref="HitObjectMask"/> is selected.
/// </summary>
public event Action<HitObjectMask> MaskSelected;
/// <summary>
/// Invoked when any <see cref="HitObjectMask"/> is deselected.
/// </summary>
public event Action<HitObjectMask> MaskDeselected;
/// <summary>
/// Invoked when any <see cref="HitObjectMask"/> requests selection.
/// </summary>
public event Action<HitObjectMask, InputState> MaskSelectionRequested;
/// <summary>
/// Invoked when any <see cref="HitObjectMask"/> requests drag.
/// </summary>
public event Action<HitObjectMask, InputState> MaskDragRequested;
private IEnumerable<HitObjectMask> aliveMasks => AliveInternalChildren.Cast<HitObjectMask>();
public MaskContainer()
{
RelativeSizeAxes = Axes.Both;
}
public override void Add(HitObjectMask drawable)
{
base.Add(drawable);
drawable.Selected += onMaskSelected;
drawable.Deselected += onMaskDeselected;
drawable.SelectionRequested += onSelectionRequested;
drawable.DragRequested += onDragRequested;
}
public override bool Remove(HitObjectMask drawable)
{
var result = base.Remove(drawable);
if (result)
{
drawable.Selected -= onMaskSelected;
drawable.Deselected -= onMaskDeselected;
drawable.SelectionRequested -= onSelectionRequested;
drawable.DragRequested -= onDragRequested;
}
return result;
}
/// <summary>
/// Select all masks in a given rectangle selection area.
/// </summary>
/// <param name="rect">The rectangle to perform a selection on in screen-space coordinates.</param>
public void Select(RectangleF rect)
{
foreach (var mask in aliveMasks.ToList())
{
if (mask.IsPresent && rect.Contains(mask.SelectionPoint))
mask.Select();
else
mask.Deselect();
}
}
/// <summary>
/// Deselects all selected <see cref="HitObjectMask"/>s.
/// </summary>
public void DeselectAll() => aliveMasks.ToList().ForEach(m => m.Deselect());
private void onMaskSelected(HitObjectMask mask)
{
MaskSelected?.Invoke(mask);
ChangeChildDepth(mask, 1);
}
private void onMaskDeselected(HitObjectMask mask)
{
MaskDeselected?.Invoke(mask);
ChangeChildDepth(mask, 0);
}
private void onSelectionRequested(HitObjectMask mask, InputState state) => MaskSelectionRequested?.Invoke(mask, state);
private void onDragRequested(HitObjectMask mask, InputState state) => MaskDragRequested?.Invoke(mask, state);
protected override int Compare(Drawable x, Drawable y)
{
if (!(x is HitObjectMask xMask) || !(y is HitObjectMask yMask))
return base.Compare(x, y);
return Compare(xMask, yMask);
}
public int Compare(HitObjectMask x, HitObjectMask y)
{
// dpeth is used to denote selected status (we always want selected masks to handle input first).
int d = x.Depth.CompareTo(y.Depth);
if (d != 0)
return d;
// Put earlier hitobjects towards the end of the list, so they handle input first
int i = y.HitObject.HitObject.StartTime.CompareTo(x.HitObject.HitObject.StartTime);
return i == 0 ? CompareReverseChildID(x, y) : i;
}
}
}

View File

@ -0,0 +1,164 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input;
using osu.Game.Graphics;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Types;
using OpenTK;
namespace osu.Game.Screens.Edit.Screens.Compose.Layers
{
/// <summary>
/// A box which surrounds <see cref="HitObjectMask"/>s and provides interactive handles, context menus etc.
/// </summary>
public class MaskSelection : CompositeDrawable
{
public const float BORDER_RADIUS = 2;
private readonly List<HitObjectMask> selectedMasks;
private Drawable outline;
public MaskSelection()
{
selectedMasks = new List<HitObjectMask>();
RelativeSizeAxes = Axes.Both;
AlwaysPresent = true;
Alpha = 0;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
InternalChild = outline = new Container
{
Masking = true,
BorderThickness = BORDER_RADIUS,
BorderColour = colours.Yellow,
Child = new Box
{
RelativeSizeAxes = Axes.Both,
AlwaysPresent = true,
Alpha = 0
}
};
}
#region User Input Handling
public void HandleDrag(HitObjectMask m, InputState state)
{
// Todo: Various forms of snapping
foreach (var mask in selectedMasks)
{
switch (mask.HitObject.HitObject)
{
case IHasEditablePosition editablePosition:
editablePosition.OffsetPosition(state.Mouse.Delta);
break;
}
}
}
#endregion
#region Selection Handling
/// <summary>
/// Bind an action to deselect all selected masks.
/// </summary>
public Action DeselectAll { private get; set; }
/// <summary>
/// Handle a mask becoming selected.
/// </summary>
/// <param name="mask">The mask.</param>
public void HandleSelected(HitObjectMask mask) => selectedMasks.Add(mask);
/// <summary>
/// Handle a mask becoming deselected.
/// </summary>
/// <param name="mask">The mask.</param>
public void HandleDeselected(HitObjectMask mask)
{
selectedMasks.Remove(mask);
// We don't want to update visibility if > 0, since we may be deselecting masks during drag-selection
if (selectedMasks.Count == 0)
UpdateVisibility();
}
/// <summary>
/// Handle a mask requesting selection.
/// </summary>
/// <param name="mask">The mask.</param>
public void HandleSelectionRequested(HitObjectMask mask, InputState state)
{
if (state.Keyboard.ControlPressed)
{
if (mask.IsSelected)
mask.Deselect();
else
mask.Select();
}
else
{
if (mask.IsSelected)
return;
DeselectAll?.Invoke();
mask.Select();
}
UpdateVisibility();
}
#endregion
/// <summary>
/// Updates whether this <see cref="MaskSelection"/> is visible.
/// </summary>
internal void UpdateVisibility()
{
if (selectedMasks.Count > 0)
Show();
else
Hide();
}
protected override void Update()
{
base.Update();
if (selectedMasks.Count == 0)
return;
// Move the rectangle to cover the hitobjects
var topLeft = new Vector2(float.MaxValue, float.MaxValue);
var bottomRight = new Vector2(float.MinValue, float.MinValue);
bool hasSelection = false;
foreach (var mask in selectedMasks)
{
topLeft = Vector2.ComponentMin(topLeft, ToLocalSpace(mask.SelectionQuad.TopLeft));
bottomRight = Vector2.ComponentMax(bottomRight, ToLocalSpace(mask.SelectionQuad.BottomRight));
}
topLeft -= new Vector2(5);
bottomRight += new Vector2(5);
outline.Size = bottomRight - topLeft;
outline.Position = topLeft;
}
}
}

View File

@ -1,102 +0,0 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input;
using osu.Game.Graphics;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Types;
using osu.Game.Rulesets.Objects.Drawables;
using OpenTK;
namespace osu.Game.Screens.Edit.Screens.Compose.Layers
{
/// <summary>
/// A box which surrounds <see cref="DrawableHitObject"/>s and provides interactive handles, context menus etc.
/// </summary>
public class SelectionBox : VisibilityContainer
{
private readonly IReadOnlyList<HitObjectMask> overlays;
public const float BORDER_RADIUS = 2;
public SelectionBox(IReadOnlyList<HitObjectMask> overlays)
{
this.overlays = overlays;
Masking = true;
BorderThickness = BORDER_RADIUS;
InternalChild = new Box
{
RelativeSizeAxes = Axes.Both,
AlwaysPresent = true,
Alpha = 0
};
State = Visibility.Visible;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
BorderColour = colours.Yellow;
}
protected override void Update()
{
base.Update();
// Todo: We might need to optimise this
// Move the rectangle to cover the hitobjects
var topLeft = new Vector2(float.MaxValue, float.MaxValue);
var bottomRight = new Vector2(float.MinValue, float.MinValue);
foreach (var obj in overlays)
{
topLeft = Vector2.ComponentMin(topLeft, Parent.ToLocalSpace(obj.HitObject.SelectionQuad.TopLeft));
bottomRight = Vector2.ComponentMax(bottomRight, Parent.ToLocalSpace(obj.HitObject.SelectionQuad.BottomRight));
}
topLeft -= new Vector2(5);
bottomRight += new Vector2(5);
Size = bottomRight - topLeft;
Position = topLeft;
}
public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => overlays.Any(o => o.ReceiveMouseInputAt(screenSpacePos));
protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) => true;
protected override bool OnDragStart(InputState state) => true;
protected override bool OnDrag(InputState state)
{
// Todo: Various forms of snapping
foreach (var hitObject in overlays.Select(o => o.HitObject.HitObject))
{
switch (hitObject)
{
case IHasEditablePosition editablePosition:
editablePosition.OffsetPosition(state.Mouse.Delta);
break;
}
}
return true;
}
protected override bool OnDragEnd(InputState state) => true;
public override bool DisposeOnDeathRemoval => true;
protected override void PopIn() => this.FadeIn();
protected override void PopOut() => this.FadeOut();
}
}

View File

@ -1,240 +0,0 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI;
using OpenTK;
using OpenTK.Graphics;
using RectangleF = osu.Framework.Graphics.Primitives.RectangleF;
namespace osu.Game.Screens.Edit.Screens.Compose.Layers
{
public class SelectionLayer : CompositeDrawable
{
/// <summary>
/// Invoked when a <see cref="DrawableHitObject"/> is selected.
/// </summary>
public event Action<DrawableHitObject> ObjectSelected;
/// <summary>
/// Invoked when a <see cref="DrawableHitObject"/> is deselected.
/// </summary>
public event Action<DrawableHitObject> ObjectDeselected;
/// <summary>
/// Invoked when the selection has been cleared.
/// </summary>
public event Action SelectionCleared;
/// <summary>
/// Invoked when the user has finished selecting all <see cref="DrawableHitObject"/>s.
/// </summary>
public event Action SelectionFinished;
private readonly Playfield playfield;
public SelectionLayer(Playfield playfield)
{
this.playfield = playfield;
RelativeSizeAxes = Axes.Both;
}
private DragBox dragBox;
private readonly HashSet<DrawableHitObject> selectedHitObjects = new HashSet<DrawableHitObject>();
protected override bool OnMouseDown(InputState state, MouseDownEventArgs args)
{
DeselectAll();
return true;
}
protected override bool OnDragStart(InputState state)
{
AddInternal(dragBox = new DragBox());
return true;
}
protected override bool OnDrag(InputState state)
{
dragBox.Show();
var dragPosition = state.Mouse.NativeState.Position;
var dragStartPosition = state.Mouse.NativeState.PositionMouseDown ?? dragPosition;
var screenSpaceDragQuad = new Quad(dragStartPosition.X, dragStartPosition.Y, dragPosition.X - dragStartPosition.X, dragPosition.Y - dragStartPosition.Y);
dragBox.SetDragRectangle(screenSpaceDragQuad.AABBFloat);
selectQuad(screenSpaceDragQuad);
return true;
}
protected override bool OnDragEnd(InputState state)
{
dragBox.Hide();
dragBox.Expire();
finishSelection();
return true;
}
protected override bool OnClick(InputState state)
{
selectPoint(state.Mouse.NativeState.Position);
finishSelection();
return true;
}
/// <summary>
/// Selects a <see cref="DrawableHitObject"/>.
/// </summary>
/// <param name="hitObject">The <see cref="DrawableHitObject"/> to select.</param>
public void Select(DrawableHitObject hitObject)
{
if (!select(hitObject))
return;
clearSelection();
finishSelection();
}
/// <summary>
/// Selects a <see cref="DrawableHitObject"/> without performing capture updates.
/// </summary>
/// <param name="hitObject">The <see cref="DrawableHitObject"/> to select.</param>
/// <returns>Whether <paramref name="hitObject"/> was selected.</returns>
private bool select(DrawableHitObject hitObject)
{
if (!selectedHitObjects.Add(hitObject))
return false;
ObjectSelected?.Invoke(hitObject);
return true;
}
/// <summary>
/// Deselects a <see cref="DrawableHitObject"/>.
/// </summary>
/// <param name="hitObject">The <see cref="DrawableHitObject"/> to deselect.</param>
public void Deselect(DrawableHitObject hitObject)
{
if (!deselect(hitObject))
return;
clearSelection();
finishSelection();
}
/// <summary>
/// Deselects a <see cref="DrawableHitObject"/> without performing capture updates.
/// </summary>
/// <param name="hitObject">The <see cref="DrawableHitObject"/> to deselect.</param>
/// <returns>Whether the <see cref="DrawableHitObject"/> was deselected.</returns>
private bool deselect(DrawableHitObject hitObject)
{
if (!selectedHitObjects.Remove(hitObject))
return false;
ObjectDeselected?.Invoke(hitObject);
return true;
}
/// <summary>
/// Deselects all selected <see cref="DrawableHitObject"/>s.
/// </summary>
public void DeselectAll()
{
selectedHitObjects.ForEach(h => ObjectDeselected?.Invoke(h));
selectedHitObjects.Clear();
clearSelection();
}
/// <summary>
/// Selects all hitobjects that are present within the area of a <see cref="Quad"/>.
/// </summary>
/// <param name="screenSpaceQuad">The selection <see cref="Quad"/>.</param>
// Todo: If needed we can severely reduce allocations in this method
private void selectQuad(Quad screenSpaceQuad)
{
var expectedSelection = playfield.HitObjects.Objects.Where(h => h.IsAlive && h.IsPresent && screenSpaceQuad.Contains(h.SelectionPoint)).ToList();
var toRemove = selectedHitObjects.Except(expectedSelection).ToList();
foreach (var obj in toRemove)
deselect(obj);
expectedSelection.ForEach(h => select(h));
}
/// <summary>
/// Selects the top-most hitobject that is present under a specific point.
/// </summary>
/// <param name="screenSpacePoint">The <see cref="Vector2"/> to select at.</param>
private void selectPoint(Vector2 screenSpacePoint)
{
var target = playfield.HitObjects.Objects.Reverse().Where(h => h.IsAlive && h.IsPresent).FirstOrDefault(h => h.ReceiveMouseInputAt(screenSpacePoint));
if (target == null)
return;
select(target);
}
private void clearSelection() => SelectionCleared?.Invoke();
private void finishSelection()
{
if (selectedHitObjects.Count == 0)
return;
SelectionFinished?.Invoke();
}
/// <summary>
/// A box that represents a drag selection.
/// </summary>
private class DragBox : VisibilityContainer
{
/// <summary>
/// Creates a new <see cref="DragBox"/>.
/// </summary>
public DragBox()
{
Masking = true;
BorderColour = Color4.White;
BorderThickness = SelectionBox.BORDER_RADIUS;
Child = new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0.1f
};
}
public void SetDragRectangle(RectangleF rectangle)
{
var topLeft = Parent.ToLocalSpace(rectangle.TopLeft);
var bottomRight = Parent.ToLocalSpace(rectangle.BottomRight);
Position = topLeft;
Size = bottomRight - topLeft;
}
public override bool DisposeOnDeathRemoval => true;
protected override void PopIn() => this.FadeIn(250, Easing.OutQuint);
protected override void PopOut() => this.FadeOut(250, Easing.OutQuint);
}
}
}

View File

@ -309,11 +309,6 @@ namespace osu.Game.Screens.Play
Origin = Anchor.TopCentre,
Anchor = Anchor.TopCentre,
},
new MetadataLine("Composer", string.Empty)
{
Origin = Anchor.TopCentre,
Anchor = Anchor.TopCentre,
},
new MetadataLine("Mapper", metadata.AuthorString)
{
Origin = Anchor.TopCentre,

View File

@ -0,0 +1,79 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Allocation;
using osu.Framework.Input;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Screens.Compose;
namespace osu.Game.Tests.Visual
{
/// <summary>
/// Provides a clock, beat-divisor, and scrolling capability for test cases of editor components that
/// are preferrably tested within the presence of a clock and seek controls.
/// </summary>
public abstract class EditorClockTestCase : OsuTestCase
{
protected readonly BindableBeatDivisor BeatDivisor = new BindableBeatDivisor();
protected readonly EditorClock Clock;
private DependencyContainer dependencies;
protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent)
=> dependencies = new DependencyContainer(parent);
private OsuGameBase osuGame;
protected EditorClockTestCase()
{
Clock = new EditorClock(new ControlPointInfo(), BeatDivisor) { IsCoupled = false };
}
[BackgroundDependencyLoader]
private void load(OsuGameBase osuGame)
{
this.osuGame = osuGame;
dependencies.Cache(BeatDivisor);
dependencies.CacheAs<IFrameBasedClock>(Clock);
dependencies.CacheAs<IAdjustableClock>(Clock);
osuGame.Beatmap.ValueChanged += beatmapChanged;
beatmapChanged(osuGame.Beatmap.Value);
}
private void beatmapChanged(WorkingBeatmap working)
{
Clock.ControlPointInfo = working.Beatmap.ControlPointInfo;
Clock.ChangeSource((IAdjustableClock)working.Track ?? new StopwatchClock());
Clock.ProcessFrame();
}
protected override void Update()
{
base.Update();
Clock.ProcessFrame();
}
protected override bool OnWheel(InputState state)
{
if (state.Mouse.WheelDelta > 0)
Clock.SeekBackward(true);
else
Clock.SeekForward(true);
return true;
}
protected override void Dispose(bool isDisposing)
{
osuGame.Beatmap.ValueChanged -= beatmapChanged;
base.Dispose(isDisposing);
}
}
}

View File

@ -0,0 +1,29 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing.Input;
namespace osu.Game.Tests.Visual
{
public abstract class ManualInputManagerTestCase : OsuTestCase
{
protected override Container<Drawable> Content => InputManager;
protected readonly ManualInputManager InputManager;
protected ManualInputManagerTestCase()
{
base.Content.Add(InputManager = new ManualInputManager());
ReturnUserInput();
}
/// <summary>
/// Returns input back to the user.
/// </summary>
protected void ReturnUserInput()
{
AddStep("Return user input", () => InputManager.UseParentState = true);
}
}
}