mirror of
https://github.com/ppy/osu.git
synced 2025-01-13 22:22:55 +08:00
Merge pull request #28509 from bdach/slider-anchor-type-switching
Add ability to cycle slider control point types via keyboard
This commit is contained in:
commit
3f138489a1
@ -15,6 +15,7 @@ using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
{
|
||||
@ -177,6 +178,79 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
addAssertPointPositionChanged(points, i);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestChangingControlPointTypeViaTab()
|
||||
{
|
||||
createVisualiser(true);
|
||||
|
||||
addControlPointStep(new Vector2(200), PathType.LINEAR);
|
||||
addControlPointStep(new Vector2(300));
|
||||
addControlPointStep(new Vector2(500, 300));
|
||||
addControlPointStep(new Vector2(700, 200));
|
||||
addControlPointStep(new Vector2(500, 100));
|
||||
|
||||
AddStep("select first control point", () => visualiser.Pieces[0].IsSelected.Value = true);
|
||||
AddStep("press tab", () => InputManager.Key(Key.Tab));
|
||||
assertControlPointPathType(0, PathType.BEZIER);
|
||||
|
||||
AddStep("press shift-tab", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.LShift);
|
||||
InputManager.Key(Key.Tab);
|
||||
InputManager.ReleaseKey(Key.LShift);
|
||||
});
|
||||
assertControlPointPathType(0, PathType.LINEAR);
|
||||
|
||||
AddStep("press shift-tab", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.LShift);
|
||||
InputManager.Key(Key.Tab);
|
||||
InputManager.ReleaseKey(Key.LShift);
|
||||
});
|
||||
assertControlPointPathType(0, PathType.BSpline(4));
|
||||
|
||||
AddStep("press shift-tab", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.LShift);
|
||||
InputManager.Key(Key.Tab);
|
||||
InputManager.ReleaseKey(Key.LShift);
|
||||
});
|
||||
assertControlPointPathType(0, PathType.PERFECT_CURVE);
|
||||
assertControlPointPathType(2, PathType.BSpline(4));
|
||||
|
||||
AddStep("select third last control point", () =>
|
||||
{
|
||||
visualiser.Pieces[0].IsSelected.Value = false;
|
||||
visualiser.Pieces[2].IsSelected.Value = true;
|
||||
});
|
||||
|
||||
AddStep("press shift-tab", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.LShift);
|
||||
InputManager.Key(Key.Tab);
|
||||
InputManager.ReleaseKey(Key.LShift);
|
||||
});
|
||||
assertControlPointPathType(2, PathType.PERFECT_CURVE);
|
||||
|
||||
AddRepeatStep("press tab", () => InputManager.Key(Key.Tab), 2);
|
||||
assertControlPointPathType(0, PathType.BEZIER);
|
||||
assertControlPointPathType(2, null);
|
||||
|
||||
AddStep("select first and third control points", () =>
|
||||
{
|
||||
visualiser.Pieces[0].IsSelected.Value = true;
|
||||
visualiser.Pieces[2].IsSelected.Value = true;
|
||||
});
|
||||
AddStep("press alt-1", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.AltLeft);
|
||||
InputManager.Key(Key.Number1);
|
||||
InputManager.ReleaseKey(Key.AltLeft);
|
||||
});
|
||||
assertControlPointPathType(0, PathType.LINEAR);
|
||||
assertControlPointPathType(2, PathType.LINEAR);
|
||||
}
|
||||
|
||||
private void addAssertPointPositionChanged(Vector2[] points, int index)
|
||||
{
|
||||
AddAssert($"Point at {points.ElementAt(index)} changed",
|
||||
|
@ -2,13 +2,16 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders;
|
||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Tests.Visual;
|
||||
@ -57,7 +60,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
assertPlaced(true);
|
||||
assertLength(200);
|
||||
assertControlPointCount(2);
|
||||
assertControlPointType(0, PathType.LINEAR);
|
||||
assertFinalControlPointType(0, PathType.LINEAR);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -71,7 +74,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
|
||||
assertPlaced(true);
|
||||
assertControlPointCount(2);
|
||||
assertControlPointType(0, PathType.LINEAR);
|
||||
assertFinalControlPointType(0, PathType.LINEAR);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -89,7 +92,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
assertPlaced(true);
|
||||
assertControlPointCount(3);
|
||||
assertControlPointPosition(1, new Vector2(100, 0));
|
||||
assertControlPointType(0, PathType.PERFECT_CURVE);
|
||||
assertFinalControlPointType(0, PathType.PERFECT_CURVE);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -111,7 +114,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
assertControlPointCount(4);
|
||||
assertControlPointPosition(1, new Vector2(100, 0));
|
||||
assertControlPointPosition(2, new Vector2(100, 100));
|
||||
assertControlPointType(0, PathType.BEZIER);
|
||||
assertFinalControlPointType(0, PathType.BEZIER);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -130,8 +133,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
assertPlaced(true);
|
||||
assertControlPointCount(3);
|
||||
assertControlPointPosition(1, new Vector2(100, 0));
|
||||
assertControlPointType(0, PathType.LINEAR);
|
||||
assertControlPointType(1, PathType.LINEAR);
|
||||
assertFinalControlPointType(0, PathType.LINEAR);
|
||||
assertFinalControlPointType(1, PathType.LINEAR);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -149,7 +152,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
|
||||
assertPlaced(true);
|
||||
assertControlPointCount(2);
|
||||
assertControlPointType(0, PathType.LINEAR);
|
||||
assertFinalControlPointType(0, PathType.LINEAR);
|
||||
assertLength(100);
|
||||
}
|
||||
|
||||
@ -171,7 +174,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
|
||||
assertPlaced(true);
|
||||
assertControlPointCount(3);
|
||||
assertControlPointType(0, PathType.PERFECT_CURVE);
|
||||
assertFinalControlPointType(0, PathType.PERFECT_CURVE);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -195,7 +198,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
|
||||
assertPlaced(true);
|
||||
assertControlPointCount(4);
|
||||
assertControlPointType(0, PathType.BEZIER);
|
||||
assertFinalControlPointType(0, PathType.BEZIER);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -215,8 +218,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
assertControlPointCount(3);
|
||||
assertControlPointPosition(1, new Vector2(100, 0));
|
||||
assertControlPointPosition(2, new Vector2(100));
|
||||
assertControlPointType(0, PathType.LINEAR);
|
||||
assertControlPointType(1, PathType.LINEAR);
|
||||
assertFinalControlPointType(0, PathType.LINEAR);
|
||||
assertFinalControlPointType(1, PathType.LINEAR);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -239,8 +242,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
assertControlPointCount(4);
|
||||
assertControlPointPosition(1, new Vector2(100, 0));
|
||||
assertControlPointPosition(2, new Vector2(100));
|
||||
assertControlPointType(0, PathType.LINEAR);
|
||||
assertControlPointType(1, PathType.PERFECT_CURVE);
|
||||
assertFinalControlPointType(0, PathType.LINEAR);
|
||||
assertFinalControlPointType(1, PathType.PERFECT_CURVE);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -268,8 +271,46 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
assertControlPointPosition(2, new Vector2(100));
|
||||
assertControlPointPosition(3, new Vector2(200, 100));
|
||||
assertControlPointPosition(4, new Vector2(200));
|
||||
assertControlPointType(0, PathType.PERFECT_CURVE);
|
||||
assertControlPointType(2, PathType.PERFECT_CURVE);
|
||||
assertFinalControlPointType(0, PathType.PERFECT_CURVE);
|
||||
assertFinalControlPointType(2, PathType.PERFECT_CURVE);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestManualPathTypeControlViaKeyboard()
|
||||
{
|
||||
addMovementStep(new Vector2(200));
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
addMovementStep(new Vector2(300, 200));
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
addMovementStep(new Vector2(300));
|
||||
|
||||
assertControlPointTypeDuringPlacement(0, PathType.PERFECT_CURVE);
|
||||
|
||||
AddRepeatStep("press tab", () => InputManager.Key(Key.Tab), 2);
|
||||
assertControlPointTypeDuringPlacement(0, PathType.LINEAR);
|
||||
|
||||
AddStep("press shift-tab", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.ShiftLeft);
|
||||
InputManager.Key(Key.Tab);
|
||||
InputManager.ReleaseKey(Key.ShiftLeft);
|
||||
});
|
||||
assertControlPointTypeDuringPlacement(0, PathType.BSpline(4));
|
||||
|
||||
AddStep("start new segment via S", () => InputManager.Key(Key.S));
|
||||
assertControlPointTypeDuringPlacement(2, PathType.LINEAR);
|
||||
|
||||
addMovementStep(new Vector2(400, 300));
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
addMovementStep(new Vector2(400));
|
||||
addClickStep(MouseButton.Right);
|
||||
|
||||
assertPlaced(true);
|
||||
assertFinalControlPointType(0, PathType.BSpline(4));
|
||||
assertFinalControlPointType(2, PathType.PERFECT_CURVE);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -293,7 +334,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
addClickStep(MouseButton.Right);
|
||||
assertPlaced(true);
|
||||
|
||||
assertControlPointType(0, PathType.BEZIER);
|
||||
assertFinalControlPointType(0, PathType.BEZIER);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -312,11 +353,11 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
assertPlaced(true);
|
||||
assertLength(808, tolerance: 10);
|
||||
assertControlPointCount(5);
|
||||
assertControlPointType(0, PathType.BSpline(4));
|
||||
assertControlPointType(1, null);
|
||||
assertControlPointType(2, null);
|
||||
assertControlPointType(3, null);
|
||||
assertControlPointType(4, null);
|
||||
assertFinalControlPointType(0, PathType.BSpline(4));
|
||||
assertFinalControlPointType(1, null);
|
||||
assertFinalControlPointType(2, null);
|
||||
assertFinalControlPointType(3, null);
|
||||
assertFinalControlPointType(4, null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -337,10 +378,10 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
assertPlaced(true);
|
||||
assertLength(600, tolerance: 10);
|
||||
assertControlPointCount(4);
|
||||
assertControlPointType(0, PathType.BSpline(4));
|
||||
assertControlPointType(1, PathType.BSpline(4));
|
||||
assertControlPointType(2, PathType.BSpline(4));
|
||||
assertControlPointType(3, null);
|
||||
assertFinalControlPointType(0, PathType.BSpline(4));
|
||||
assertFinalControlPointType(1, PathType.BSpline(4));
|
||||
assertFinalControlPointType(2, PathType.BSpline(4));
|
||||
assertFinalControlPointType(3, null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -359,7 +400,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
|
||||
assertPlaced(true);
|
||||
assertControlPointCount(3);
|
||||
assertControlPointType(0, PathType.BEZIER);
|
||||
assertFinalControlPointType(0, PathType.BEZIER);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -379,7 +420,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
|
||||
assertPlaced(true);
|
||||
assertControlPointCount(3);
|
||||
assertControlPointType(0, PathType.PERFECT_CURVE);
|
||||
assertFinalControlPointType(0, PathType.PERFECT_CURVE);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -400,7 +441,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
|
||||
assertPlaced(true);
|
||||
assertControlPointCount(3);
|
||||
assertControlPointType(0, PathType.PERFECT_CURVE);
|
||||
assertFinalControlPointType(0, PathType.PERFECT_CURVE);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -421,7 +462,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
|
||||
assertPlaced(true);
|
||||
assertControlPointCount(3);
|
||||
assertControlPointType(0, PathType.BEZIER);
|
||||
assertFinalControlPointType(0, PathType.BEZIER);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -438,7 +479,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
|
||||
assertPlaced(true);
|
||||
assertControlPointCount(3);
|
||||
assertControlPointType(0, PathType.PERFECT_CURVE);
|
||||
assertFinalControlPointType(0, PathType.PERFECT_CURVE);
|
||||
}
|
||||
|
||||
private void addMovementStep(Vector2 position) => AddStep($"move mouse to {position}", () => InputManager.MoveMouseTo(InputManager.ToScreenSpace(position)));
|
||||
@ -454,7 +495,10 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
|
||||
private void assertControlPointCount(int expected) => AddAssert($"has {expected} control points", () => getSlider()!.Path.ControlPoints.Count, () => Is.EqualTo(expected));
|
||||
|
||||
private void assertControlPointType(int index, PathType? type) => AddAssert($"control point {index} is {type?.ToString() ?? "inherit"}", () => getSlider()!.Path.ControlPoints[index].Type, () => Is.EqualTo(type));
|
||||
private void assertControlPointTypeDuringPlacement(int index, PathType? type) => AddAssert($"control point {index} is {type?.ToString() ?? "inherit"}",
|
||||
() => this.ChildrenOfType<PathControlPointPiece<Slider>>().ElementAt(index).ControlPoint.Type, () => Is.EqualTo(type));
|
||||
|
||||
private void assertFinalControlPointType(int index, PathType? type) => AddAssert($"control point {index} is {type?.ToString() ?? "inherit"}", () => getSlider()!.Path.ControlPoints[index].Type, () => Is.EqualTo(type));
|
||||
|
||||
private void assertControlPointPosition(int index, Vector2 position) =>
|
||||
AddAssert($"control point {index} at {position}", () => Precision.AlmostEquals(position, getSlider()!.Path.ControlPoints[index].Position, 1));
|
||||
|
@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
public partial class PathControlPointVisualiser<T> : CompositeDrawable, IKeyBindingHandler<PlatformAction>, IHasContextMenu
|
||||
where T : OsuHitObject, IHasPath
|
||||
{
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; // allow context menu to appear outside of the playfield.
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; // allow context menu to appear outside the playfield.
|
||||
|
||||
internal readonly Container<PathControlPointPiece<T>> Pieces;
|
||||
|
||||
@ -196,6 +196,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
if (allowSelection)
|
||||
d.RequestSelection = selectionRequested;
|
||||
|
||||
d.ControlPoint.Changed += controlPointChanged;
|
||||
d.DragStarted = DragStarted;
|
||||
d.DragInProgress = DragInProgress;
|
||||
d.DragEnded = DragEnded;
|
||||
@ -209,6 +210,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
|
||||
foreach (var point in e.OldItems.Cast<PathControlPoint>())
|
||||
{
|
||||
point.Changed -= controlPointChanged;
|
||||
|
||||
foreach (var piece in Pieces.Where(p => p.ControlPoint == point).ToArray())
|
||||
piece.RemoveAndDisposeImmediately();
|
||||
}
|
||||
@ -217,6 +220,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
}
|
||||
}
|
||||
|
||||
private void controlPointChanged() => updateCurveMenuItems();
|
||||
|
||||
protected override bool OnClick(ClickEvent e)
|
||||
{
|
||||
if (Pieces.Any(piece => piece.IsHovered))
|
||||
@ -245,6 +250,86 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
{
|
||||
}
|
||||
|
||||
// ReSharper disable once StaticMemberInGenericType
|
||||
private static readonly PathType?[] path_types =
|
||||
[
|
||||
PathType.LINEAR,
|
||||
PathType.BEZIER,
|
||||
PathType.PERFECT_CURVE,
|
||||
PathType.BSpline(4),
|
||||
null,
|
||||
];
|
||||
|
||||
protected override bool OnKeyDown(KeyDownEvent e)
|
||||
{
|
||||
if (e.Repeat)
|
||||
return false;
|
||||
|
||||
switch (e.Key)
|
||||
{
|
||||
case Key.Tab:
|
||||
{
|
||||
var selectedPieces = Pieces.Where(p => p.IsSelected.Value).ToArray();
|
||||
if (selectedPieces.Length != 1)
|
||||
return false;
|
||||
|
||||
var selectedPiece = selectedPieces.Single();
|
||||
var selectedPoint = selectedPiece.ControlPoint;
|
||||
|
||||
var validTypes = path_types;
|
||||
|
||||
if (selectedPoint == controlPoints[0])
|
||||
validTypes = validTypes.Where(t => t != null).ToArray();
|
||||
|
||||
int currentTypeIndex = Array.IndexOf(validTypes, selectedPoint.Type);
|
||||
|
||||
if (currentTypeIndex < 0 && e.ShiftPressed)
|
||||
currentTypeIndex = 0;
|
||||
|
||||
changeHandler?.BeginChange();
|
||||
|
||||
do
|
||||
{
|
||||
currentTypeIndex = (validTypes.Length + currentTypeIndex + (e.ShiftPressed ? -1 : 1)) % validTypes.Length;
|
||||
|
||||
updatePathTypeOfSelectedPieces(validTypes[currentTypeIndex]);
|
||||
} while (selectedPoint.Type != validTypes[currentTypeIndex]);
|
||||
|
||||
changeHandler?.EndChange();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
case Key.Number1:
|
||||
case Key.Number2:
|
||||
case Key.Number3:
|
||||
case Key.Number4:
|
||||
case Key.Number5:
|
||||
{
|
||||
if (!e.AltPressed)
|
||||
return false;
|
||||
|
||||
var type = path_types[e.Key - Key.Number1];
|
||||
|
||||
if (Pieces[0].IsSelected.Value && type == null)
|
||||
return false;
|
||||
|
||||
updatePathTypeOfSelectedPieces(type);
|
||||
return true;
|
||||
}
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
foreach (var p in Pieces)
|
||||
p.ControlPoint.Changed -= controlPointChanged;
|
||||
}
|
||||
|
||||
private void selectionRequested(PathControlPointPiece<T> piece, MouseButtonEvent e)
|
||||
{
|
||||
if (e.Button == MouseButton.Left && inputManager.CurrentState.Keyboard.ControlPressed)
|
||||
@ -254,16 +339,19 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to set the given control point piece to the given path type.
|
||||
/// If that would fail, try to change the path such that it instead succeeds
|
||||
/// Attempts to set all selected control point pieces to the given path type.
|
||||
/// If that fails, try to change the path such that it instead succeeds
|
||||
/// in a UX-friendly way.
|
||||
/// </summary>
|
||||
/// <param name="piece">The control point piece that we want to change the path type of.</param>
|
||||
/// <param name="type">The path type we want to assign to the given control point piece.</param>
|
||||
private void updatePathType(PathControlPointPiece<T> piece, PathType? type)
|
||||
private void updatePathTypeOfSelectedPieces(PathType? type)
|
||||
{
|
||||
var pointsInSegment = hitObject.Path.PointsInSegment(piece.ControlPoint);
|
||||
int indexInSegment = pointsInSegment.IndexOf(piece.ControlPoint);
|
||||
changeHandler?.BeginChange();
|
||||
|
||||
foreach (var p in Pieces.Where(p => p.IsSelected.Value))
|
||||
{
|
||||
var pointsInSegment = hitObject.Path.PointsInSegment(p.ControlPoint);
|
||||
int indexInSegment = pointsInSegment.IndexOf(p.ControlPoint);
|
||||
|
||||
if (type?.Type == SplineType.PerfectCurve)
|
||||
{
|
||||
@ -277,7 +365,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
}
|
||||
|
||||
hitObject.Path.ExpectedDistance.Value = null;
|
||||
piece.ControlPoint.Type = type;
|
||||
p.ControlPoint.Type = type;
|
||||
}
|
||||
|
||||
EnsureValidPathTypes();
|
||||
|
||||
changeHandler?.EndChange();
|
||||
}
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
@ -290,6 +383,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
private int draggedControlPointIndex;
|
||||
private HashSet<PathControlPoint> selectedControlPoints;
|
||||
|
||||
private List<MenuItem> curveTypeItems;
|
||||
|
||||
public void DragStarted(PathControlPoint controlPoint)
|
||||
{
|
||||
dragStartPositions = hitObject.Path.ControlPoints.Select(point => point.Position).ToArray();
|
||||
@ -386,22 +481,27 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
var splittablePieces = selectedPieces.Where(isSplittable).ToList();
|
||||
int splittableCount = splittablePieces.Count;
|
||||
|
||||
List<MenuItem> curveTypeItems = new List<MenuItem>();
|
||||
curveTypeItems = new List<MenuItem>();
|
||||
|
||||
if (!selectedPieces.Contains(Pieces[0]))
|
||||
foreach (PathType? type in path_types)
|
||||
{
|
||||
curveTypeItems.Add(createMenuItemForPathType(null));
|
||||
// special inherit case
|
||||
if (type == null)
|
||||
{
|
||||
if (selectedPieces.Contains(Pieces[0]))
|
||||
continue;
|
||||
|
||||
curveTypeItems.Add(new OsuMenuItemSpacer());
|
||||
}
|
||||
|
||||
// todo: hide/disable items which aren't valid for selected points
|
||||
curveTypeItems.Add(createMenuItemForPathType(PathType.LINEAR));
|
||||
curveTypeItems.Add(createMenuItemForPathType(PathType.PERFECT_CURVE));
|
||||
curveTypeItems.Add(createMenuItemForPathType(PathType.BEZIER));
|
||||
curveTypeItems.Add(createMenuItemForPathType(PathType.BSpline(4)));
|
||||
curveTypeItems.Add(createMenuItemForPathType(type));
|
||||
}
|
||||
|
||||
if (selectedPieces.Any(piece => piece.ControlPoint.Type?.Type == SplineType.Catmull))
|
||||
{
|
||||
curveTypeItems.Add(new OsuMenuItemSpacer());
|
||||
curveTypeItems.Add(createMenuItemForPathType(PathType.CATMULL));
|
||||
}
|
||||
|
||||
var menuItems = new List<MenuItem>
|
||||
{
|
||||
@ -424,26 +524,23 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
() => DeleteSelected())
|
||||
);
|
||||
|
||||
updateCurveMenuItems();
|
||||
|
||||
return menuItems.ToArray();
|
||||
|
||||
CurveTypeMenuItem createMenuItemForPathType(PathType? type) => new CurveTypeMenuItem(type, _ => updatePathTypeOfSelectedPieces(type));
|
||||
}
|
||||
}
|
||||
|
||||
private MenuItem createMenuItemForPathType(PathType? type)
|
||||
private void updateCurveMenuItems()
|
||||
{
|
||||
if (curveTypeItems == null)
|
||||
return;
|
||||
|
||||
foreach (var item in curveTypeItems.OfType<CurveTypeMenuItem>())
|
||||
{
|
||||
int totalCount = Pieces.Count(p => p.IsSelected.Value);
|
||||
int countOfState = Pieces.Where(p => p.IsSelected.Value).Count(p => p.ControlPoint.Type == type);
|
||||
|
||||
var item = new TernaryStateRadioMenuItem(type?.Description ?? "Inherit", MenuItemType.Standard, _ =>
|
||||
{
|
||||
changeHandler?.BeginChange();
|
||||
|
||||
foreach (var p in Pieces.Where(p => p.IsSelected.Value))
|
||||
updatePathType(p, type);
|
||||
|
||||
EnsureValidPathTypes();
|
||||
|
||||
changeHandler?.EndChange();
|
||||
});
|
||||
int countOfState = Pieces.Where(p => p.IsSelected.Value).Count(p => p.ControlPoint.Type == item.PathType);
|
||||
|
||||
if (countOfState == totalCount)
|
||||
item.State.Value = TernaryState.True;
|
||||
@ -451,8 +548,18 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
item.State.Value = TernaryState.Indeterminate;
|
||||
else
|
||||
item.State.Value = TernaryState.False;
|
||||
}
|
||||
}
|
||||
|
||||
return item;
|
||||
private class CurveTypeMenuItem : TernaryStateRadioMenuItem
|
||||
{
|
||||
public readonly PathType? PathType;
|
||||
|
||||
public CurveTypeMenuItem(PathType? pathType, Action<TernaryState> action)
|
||||
: base(pathType?.Description ?? "Inherit", MenuItemType.Standard, action)
|
||||
{
|
||||
PathType = pathType;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -40,6 +40,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
private PathControlPoint segmentStart;
|
||||
private PathControlPoint cursor;
|
||||
private int currentSegmentLength;
|
||||
private bool usingCustomSegmentType;
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
[CanBeNull]
|
||||
@ -149,12 +150,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
|
||||
case SliderPlacementState.ControlPoints:
|
||||
if (canPlaceNewControlPoint(out var lastPoint))
|
||||
{
|
||||
// Place a new point by detatching the current cursor.
|
||||
updateCursor();
|
||||
cursor = null;
|
||||
}
|
||||
placeNewControlPoint();
|
||||
else
|
||||
beginNewSegment(lastPoint);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void beginNewSegment(PathControlPoint lastPoint)
|
||||
{
|
||||
// Transform the last point into a new segment.
|
||||
Debug.Assert(lastPoint != null);
|
||||
@ -163,12 +169,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
segmentStart.Type = PathType.LINEAR;
|
||||
|
||||
currentSegmentLength = 1;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
usingCustomSegmentType = false;
|
||||
}
|
||||
|
||||
protected override bool OnDragStart(DragStartEvent e)
|
||||
@ -223,6 +224,72 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
base.OnMouseUp(e);
|
||||
}
|
||||
|
||||
private static readonly PathType[] path_types =
|
||||
[
|
||||
PathType.LINEAR,
|
||||
PathType.BEZIER,
|
||||
PathType.PERFECT_CURVE,
|
||||
PathType.BSpline(4),
|
||||
];
|
||||
|
||||
protected override bool OnKeyDown(KeyDownEvent e)
|
||||
{
|
||||
if (e.Repeat)
|
||||
return false;
|
||||
|
||||
if (state != SliderPlacementState.ControlPoints)
|
||||
return false;
|
||||
|
||||
switch (e.Key)
|
||||
{
|
||||
case Key.S:
|
||||
{
|
||||
if (!canPlaceNewControlPoint(out _))
|
||||
return false;
|
||||
|
||||
placeNewControlPoint();
|
||||
var last = HitObject.Path.ControlPoints.Last(p => p != cursor);
|
||||
beginNewSegment(last);
|
||||
return true;
|
||||
}
|
||||
|
||||
case Key.Number1:
|
||||
case Key.Number2:
|
||||
case Key.Number3:
|
||||
case Key.Number4:
|
||||
{
|
||||
if (!e.AltPressed)
|
||||
return false;
|
||||
|
||||
usingCustomSegmentType = true;
|
||||
segmentStart.Type = path_types[e.Key - Key.Number1];
|
||||
controlPointVisualiser.EnsureValidPathTypes();
|
||||
return true;
|
||||
}
|
||||
|
||||
case Key.Tab:
|
||||
{
|
||||
usingCustomSegmentType = true;
|
||||
|
||||
int currentTypeIndex = segmentStart.Type.HasValue ? Array.IndexOf(path_types, segmentStart.Type.Value) : -1;
|
||||
|
||||
if (currentTypeIndex < 0 && e.ShiftPressed)
|
||||
currentTypeIndex = 0;
|
||||
|
||||
do
|
||||
{
|
||||
currentTypeIndex = (path_types.Length + currentTypeIndex + (e.ShiftPressed ? -1 : 1)) % path_types.Length;
|
||||
segmentStart.Type = path_types[currentTypeIndex];
|
||||
controlPointVisualiser.EnsureValidPathTypes();
|
||||
} while (segmentStart.Type != path_types[currentTypeIndex]);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
@ -246,6 +313,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
|
||||
private void updatePathType()
|
||||
{
|
||||
if (usingCustomSegmentType)
|
||||
{
|
||||
controlPointVisualiser.EnsureValidPathTypes();
|
||||
return;
|
||||
}
|
||||
|
||||
if (state == SliderPlacementState.Drawing)
|
||||
{
|
||||
segmentStart.Type = PathType.BSpline(4);
|
||||
@ -316,6 +389,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
return lastPiece.IsHovered != true;
|
||||
}
|
||||
|
||||
private void placeNewControlPoint()
|
||||
{
|
||||
// Place a new point by detatching the current cursor.
|
||||
updateCursor();
|
||||
cursor = null;
|
||||
}
|
||||
|
||||
private void updateSlider()
|
||||
{
|
||||
if (state == SliderPlacementState.Drawing)
|
||||
|
@ -15,6 +15,13 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
public SliderCompositionTool()
|
||||
: base(nameof(Slider))
|
||||
{
|
||||
TooltipText = """
|
||||
Left click for new point.
|
||||
Left click twice or S key for new segment.
|
||||
Tab, Shift-Tab, or Alt-1~4 to change current segment type.
|
||||
Right click to finish.
|
||||
Click and drag for drawing mode.
|
||||
""";
|
||||
}
|
||||
|
||||
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders);
|
||||
|
@ -215,14 +215,14 @@ namespace osu.Game.Rulesets.Edit
|
||||
|
||||
toolboxCollection.Items = CompositionTools
|
||||
.Prepend(new SelectTool())
|
||||
.Select(t => new RadioButton(t.Name, () => toolSelected(t), t.CreateIcon))
|
||||
.Select(t => new HitObjectCompositionToolButton(t, () => toolSelected(t)))
|
||||
.ToList();
|
||||
|
||||
foreach (var item in toolboxCollection.Items)
|
||||
{
|
||||
item.Selected.DisabledChanged += isDisabled =>
|
||||
{
|
||||
item.TooltipText = isDisabled ? "Add at least one timing point first!" : string.Empty;
|
||||
item.TooltipText = isDisabled ? "Add at least one timing point first!" : ((HitObjectCompositionToolButton)item).TooltipText;
|
||||
};
|
||||
}
|
||||
|
||||
|
22
osu.Game/Rulesets/Edit/HitObjectCompositionToolButton.cs
Normal file
22
osu.Game/Rulesets/Edit/HitObjectCompositionToolButton.cs
Normal file
@ -0,0 +1,22 @@
|
||||
// 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.Game.Rulesets.Edit.Tools;
|
||||
using osu.Game.Screens.Edit.Components.RadioButtons;
|
||||
|
||||
namespace osu.Game.Rulesets.Edit
|
||||
{
|
||||
public class HitObjectCompositionToolButton : RadioButton
|
||||
{
|
||||
public HitObjectCompositionTool Tool { get; }
|
||||
|
||||
public HitObjectCompositionToolButton(HitObjectCompositionTool tool, Action? action)
|
||||
: base(tool.Name, action, tool.CreateIcon)
|
||||
{
|
||||
Tool = tool;
|
||||
|
||||
TooltipText = tool.TooltipText;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,9 +1,8 @@
|
||||
// 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 osu.Framework.Graphics;
|
||||
using osu.Framework.Localisation;
|
||||
|
||||
namespace osu.Game.Rulesets.Edit.Tools
|
||||
{
|
||||
@ -11,14 +10,16 @@ namespace osu.Game.Rulesets.Edit.Tools
|
||||
{
|
||||
public readonly string Name;
|
||||
|
||||
public LocalisableString TooltipText { get; init; }
|
||||
|
||||
protected HitObjectCompositionTool(string name)
|
||||
{
|
||||
Name = name;
|
||||
}
|
||||
|
||||
public abstract PlacementBlueprint CreatePlacementBlueprint();
|
||||
public abstract PlacementBlueprint? CreatePlacementBlueprint();
|
||||
|
||||
public virtual Drawable CreateIcon() => null;
|
||||
public virtual Drawable? CreateIcon() => null;
|
||||
|
||||
public override string ToString() => Name;
|
||||
}
|
||||
|
@ -24,11 +24,11 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons
|
||||
/// <summary>
|
||||
/// A function which creates a drawable icon to represent this item. If null, a sane default should be used.
|
||||
/// </summary>
|
||||
public readonly Func<Drawable>? CreateIcon;
|
||||
public readonly Func<Drawable?>? CreateIcon;
|
||||
|
||||
private readonly Action? action;
|
||||
|
||||
public RadioButton(string label, Action? action, Func<Drawable>? createIcon = null)
|
||||
public RadioButton(string label, Action? action, Func<Drawable?>? createIcon = null)
|
||||
{
|
||||
Label = label;
|
||||
CreateIcon = createIcon;
|
||||
|
Loading…
Reference in New Issue
Block a user