mirror of
https://github.com/ppy/osu.git
synced 2024-12-05 10:33:22 +08:00
Merge branch 'feature/command-handler' of https://github.com/minetoblend/osu into command-pattern-real-2
This commit is contained in:
commit
05a87c9a6e
@ -24,8 +24,10 @@ using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Edit.Commands;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.Commands;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
@ -42,6 +44,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
private readonly T hitObject;
|
||||
private readonly bool allowSelection;
|
||||
|
||||
private SliderPathCommandProxy pathProxy = null;
|
||||
|
||||
private InputManager inputManager;
|
||||
|
||||
public Action<List<PathControlPoint>> RemoveControlPointsRequested;
|
||||
@ -113,7 +117,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
return;
|
||||
|
||||
if (segment.Count > 3)
|
||||
first.Type = PathType.BEZIER;
|
||||
commandHandler.SafeSubmit(new UpdateControlPointCommand(first) { Type = PathType.BEZIER });
|
||||
|
||||
if (segment.Count != 3)
|
||||
return;
|
||||
@ -121,7 +125,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
ReadOnlySpan<Vector2> points = segment.Select(p => p.Position).ToArray();
|
||||
RectangleF boundingBox = PathApproximator.CircularArcBoundingBox(points);
|
||||
if (boundingBox.Width >= 640 || boundingBox.Height >= 480)
|
||||
first.Type = PathType.BEZIER;
|
||||
commandHandler.SafeSubmit(new UpdateControlPointCommand(first) { Type = PathType.BEZIER });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -146,9 +150,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
if (toRemove.Count == 0)
|
||||
return false;
|
||||
|
||||
changeHandler?.BeginChange();
|
||||
RemoveControlPointsRequested?.Invoke(toRemove);
|
||||
changeHandler?.EndChange();
|
||||
commandHandler?.Commit();
|
||||
|
||||
// Since pieces are re-used, they will not point to the deleted control points while remaining selected
|
||||
foreach (var piece in Pieces)
|
||||
@ -165,9 +168,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
if (controlPointsToSplitAt.Count == 0)
|
||||
return false;
|
||||
|
||||
changeHandler?.BeginChange();
|
||||
SplitControlPointsRequested?.Invoke(controlPointsToSplitAt);
|
||||
changeHandler?.EndChange();
|
||||
commandHandler?.Commit();
|
||||
|
||||
// Since pieces are re-used, they will not point to the deleted control points while remaining selected
|
||||
foreach (var piece in Pieces)
|
||||
@ -286,8 +288,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
if (currentTypeIndex < 0 && e.ShiftPressed)
|
||||
currentTypeIndex = 0;
|
||||
|
||||
changeHandler?.BeginChange();
|
||||
|
||||
do
|
||||
{
|
||||
currentTypeIndex = (validTypes.Length + currentTypeIndex + (e.ShiftPressed ? -1 : 1)) % validTypes.Length;
|
||||
@ -295,7 +295,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
updatePathTypeOfSelectedPieces(validTypes[currentTypeIndex]);
|
||||
} while (selectedPoint.Type != validTypes[currentTypeIndex]);
|
||||
|
||||
changeHandler?.EndChange();
|
||||
commandHandler?.Commit();
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -351,8 +351,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
/// <param name="type">The path type we want to assign to the given control point piece.</param>
|
||||
private void updatePathTypeOfSelectedPieces(PathType? type)
|
||||
{
|
||||
changeHandler?.BeginChange();
|
||||
|
||||
double originalDistance = hitObject.Path.Distance;
|
||||
|
||||
foreach (var p in Pieces.Where(p => p.IsSelected.Value))
|
||||
@ -382,12 +380,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
else
|
||||
hitObject.Path.ExpectedDistance.Value = originalDistance;
|
||||
|
||||
changeHandler?.EndChange();
|
||||
commandHandler?.Commit();
|
||||
}
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private IEditorChangeHandler changeHandler { get; set; }
|
||||
|
||||
#region Drag handling
|
||||
|
||||
private Vector2[] dragStartPositions;
|
||||
@ -406,9 +401,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
|
||||
Debug.Assert(draggedControlPointIndex >= 0);
|
||||
|
||||
changeHandler?.BeginChange();
|
||||
commandHandler?.Commit();
|
||||
}
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private EditorCommandHandler commandHandler { get; set; }
|
||||
|
||||
public void DragInProgress(DragEvent e)
|
||||
{
|
||||
Vector2[] oldControlPoints = hitObject.Path.ControlPoints.Select(cp => cp.Position).ToArray();
|
||||
@ -423,8 +421,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
|
||||
Vector2 movementDelta = Parent!.ToLocalSpace(result?.ScreenSpacePosition ?? newHeadPosition) - hitObject.Position;
|
||||
|
||||
hitObject.Position += movementDelta;
|
||||
hitObject.StartTime = result?.Time ?? hitObject.StartTime;
|
||||
commandHandler.SafeSubmit(new MoveCommand(hitObject, hitObject.Position + movementDelta));
|
||||
commandHandler.SafeSubmit(new SetStartTimeCommand(hitObject, result?.Time ?? hitObject.StartTime));
|
||||
|
||||
for (int i = 1; i < hitObject.Path.ControlPoints.Count; i++)
|
||||
{
|
||||
@ -434,7 +432,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
// All other selected control points (if any) will move together with the head point
|
||||
// (and so they will not move at all, relative to each other).
|
||||
if (!selectedControlPoints.Contains(controlPoint))
|
||||
controlPoint.Position -= movementDelta;
|
||||
commandHandler.SafeSubmit(new UpdateControlPointCommand(controlPoint) { Position = controlPoint.Position - movementDelta });
|
||||
}
|
||||
}
|
||||
else
|
||||
@ -447,33 +445,33 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
{
|
||||
PathControlPoint controlPoint = controlPoints[i];
|
||||
if (selectedControlPoints.Contains(controlPoint))
|
||||
controlPoint.Position = dragStartPositions[i] + movementDelta;
|
||||
commandHandler.SafeSubmit(new UpdateControlPointCommand(controlPoint) { Position = dragStartPositions[i] + movementDelta });
|
||||
}
|
||||
}
|
||||
|
||||
// Snap the path to the current beat divisor before checking length validity.
|
||||
hitObject.SnapTo(distanceSnapProvider);
|
||||
hitObject.SnapTo(distanceSnapProvider, commandHandler);
|
||||
|
||||
if (!hitObject.Path.HasValidLength)
|
||||
{
|
||||
for (int i = 0; i < hitObject.Path.ControlPoints.Count; i++)
|
||||
hitObject.Path.ControlPoints[i].Position = oldControlPoints[i];
|
||||
commandHandler.SafeSubmit(new UpdateControlPointCommand(hitObject.Path.ControlPoints[i]) { Position = oldControlPoints[i] });
|
||||
|
||||
hitObject.Position = oldPosition;
|
||||
hitObject.StartTime = oldStartTime;
|
||||
commandHandler.SafeSubmit(new MoveCommand(hitObject, oldPosition));
|
||||
commandHandler.SafeSubmit(new SetStartTimeCommand(hitObject, oldStartTime));
|
||||
// Snap the path length again to undo the invalid length.
|
||||
hitObject.SnapTo(distanceSnapProvider);
|
||||
hitObject.SnapTo(distanceSnapProvider, commandHandler);
|
||||
return;
|
||||
}
|
||||
|
||||
// Maintain the path types in case they got defaulted to bezier at some point during the drag.
|
||||
for (int i = 0; i < hitObject.Path.ControlPoints.Count; i++)
|
||||
hitObject.Path.ControlPoints[i].Type = dragPathTypes[i];
|
||||
commandHandler.SafeSubmit(new UpdateControlPointCommand(hitObject.Path.ControlPoints[i]) { Type = dragPathTypes[i] });
|
||||
|
||||
EnsureValidPathTypes();
|
||||
}
|
||||
|
||||
public void DragEnded() => changeHandler?.EndChange();
|
||||
public void DragEnded() => commandHandler?.Commit();
|
||||
|
||||
#endregion
|
||||
|
||||
|
@ -21,9 +21,11 @@ using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
|
||||
using osu.Game.Rulesets.Osu.Edit.Commands;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.Commands;
|
||||
using osu.Game.Screens.Edit.Compose;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
@ -34,6 +36,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
{
|
||||
protected new DrawableSlider DrawableObject => (DrawableSlider)base.DrawableObject;
|
||||
|
||||
protected SliderCommandProxy Proxy;
|
||||
|
||||
protected SliderBodyPiece BodyPiece { get; private set; } = null!;
|
||||
protected SliderCircleOverlay HeadOverlay { get; private set; } = null!;
|
||||
protected SliderCircleOverlay TailOverlay { get; private set; } = null!;
|
||||
@ -50,7 +54,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
private EditorBeatmap? editorBeatmap { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private IEditorChangeHandler? changeHandler { get; set; }
|
||||
private EditorCommandHandler? commandHandler { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private BindableBeatDivisor? beatDivisor { get; set; }
|
||||
@ -94,6 +98,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuConfigManager config)
|
||||
{
|
||||
Proxy = new SliderCommandProxy(commandHandler, HitObject);
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
BodyPiece = new SliderBodyPiece(),
|
||||
@ -216,7 +222,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
// If there's more than two objects selected, ctrl+click should deselect
|
||||
if (e.ControlPressed && IsSelected && selectedObjects.Count < 2)
|
||||
{
|
||||
changeHandler?.BeginChange();
|
||||
placementControlPoint = addControlPoint(e.MousePosition);
|
||||
ControlPointVisualiser?.SetSelectionTo(placementControlPoint);
|
||||
return true; // Stop input from being handled and modifying the selection
|
||||
@ -244,13 +249,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
lengthAdjustMouseOffset = ToLocalSpace(e.ScreenSpaceMouseDownPosition) - HitObject.Position - HitObject.Path.PositionAt(1);
|
||||
oldDuration = HitObject.Path.Distance / HitObject.SliderVelocityMultiplier;
|
||||
oldVelocityMultiplier = HitObject.SliderVelocityMultiplier;
|
||||
changeHandler?.BeginChange();
|
||||
}
|
||||
|
||||
private void endAdjustLength()
|
||||
{
|
||||
trimExcessControlPoints(HitObject.Path);
|
||||
changeHandler?.EndChange();
|
||||
trimExcessControlPoints(Proxy.Path);
|
||||
commandHandler?.Commit();
|
||||
isAdjustingLength = false;
|
||||
}
|
||||
|
||||
@ -277,8 +281,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
if (Precision.AlmostEquals(proposedDistance, HitObject.Path.Distance) && Precision.AlmostEquals(proposedVelocity, HitObject.SliderVelocityMultiplier))
|
||||
return;
|
||||
|
||||
HitObject.SliderVelocityMultiplier = proposedVelocity;
|
||||
HitObject.Path.ExpectedDistance.Value = proposedDistance;
|
||||
Proxy.SliderVelocityMultiplier = proposedVelocity;
|
||||
Proxy.Path.ExpectedDistance = proposedDistance;
|
||||
editorBeatmap?.Update(HitObject);
|
||||
}
|
||||
|
||||
@ -286,9 +290,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
/// Trims control points from the end of the slider path which are not required to reach the expected end of the slider.
|
||||
/// </summary>
|
||||
/// <param name="sliderPath">The slider path to trim control points of.</param>
|
||||
private void trimExcessControlPoints(SliderPath sliderPath)
|
||||
private void trimExcessControlPoints(SliderPathCommandProxy sliderPath)
|
||||
{
|
||||
if (!sliderPath.ExpectedDistance.Value.HasValue)
|
||||
if (!sliderPath.ExpectedDistance.HasValue)
|
||||
return;
|
||||
|
||||
double[] segmentEnds = sliderPath.GetSegmentEnds().ToArray();
|
||||
@ -382,7 +386,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
ControlPointVisualiser?.DragEnded();
|
||||
|
||||
placementControlPoint = null;
|
||||
changeHandler?.EndChange();
|
||||
commandHandler?.Commit();
|
||||
}
|
||||
}
|
||||
|
||||
@ -435,12 +439,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
|
||||
var pathControlPoint = new PathControlPoint { Position = position };
|
||||
|
||||
// Move the control points from the insertion index onwards to make room for the insertion
|
||||
controlPoints.Insert(insertionIndex, pathControlPoint);
|
||||
Proxy.Path.ControlPoints.Insert(insertionIndex, pathControlPoint);
|
||||
|
||||
ControlPointVisualiser?.EnsureValidPathTypes();
|
||||
|
||||
HitObject.SnapTo(distanceSnapProvider);
|
||||
HitObject.SnapTo(distanceSnapProvider, commandHandler);
|
||||
|
||||
return pathControlPoint;
|
||||
}
|
||||
@ -456,15 +459,15 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
// The first control point in the slider must have a type, so take it from the previous "first" one
|
||||
// Todo: Should be handled within SliderPath itself
|
||||
if (c == controlPoints[0] && controlPoints.Count > 1 && controlPoints[1].Type == null)
|
||||
controlPoints[1].Type = controlPoints[0].Type;
|
||||
new PathControlPointCommandProxy(commandHandler, c).Type = controlPoints[0].Type;
|
||||
|
||||
controlPoints.Remove(c);
|
||||
Proxy.Path.ControlPoints.Remove(c);
|
||||
}
|
||||
|
||||
ControlPointVisualiser?.EnsureValidPathTypes();
|
||||
|
||||
// Snap the slider to the current beat divisor before checking length validity.
|
||||
HitObject.SnapTo(distanceSnapProvider);
|
||||
HitObject.SnapTo(distanceSnapProvider, commandHandler);
|
||||
|
||||
// If there are 0 or 1 remaining control points, or the slider has an invalid length, it is in a degenerate form and should be deleted
|
||||
if (controlPoints.Count <= 1 || !HitObject.Path.HasValidLength)
|
||||
@ -477,8 +480,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
// So the slider needs to be offset by this amount instead, and all control points offset backwards such that the path is re-positioned at (0, 0)
|
||||
Vector2 first = controlPoints[0].Position;
|
||||
foreach (var c in controlPoints)
|
||||
c.Position -= first;
|
||||
HitObject.Position += first;
|
||||
commandHandler.SafeSubmit(new UpdateControlPointCommand(c) { Position = c.Position - first });
|
||||
|
||||
commandHandler.SafeSubmit(new MoveCommand(HitObject, HitObject.Position + first));
|
||||
}
|
||||
|
||||
private void splitControlPoints(List<PathControlPoint> controlPointsToSplitAt)
|
||||
@ -495,6 +499,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
|
||||
editorBeatmap.SelectedHitObjects.Clear();
|
||||
|
||||
var controlPointsProxy = new PathControlPointsCommandProxy(commandHandler, controlPoints);
|
||||
|
||||
foreach (var splitPoint in controlPointsToSplitAt)
|
||||
{
|
||||
if (splitPoint == controlPoints[0] || splitPoint == controlPoints[^1] || splitPoint.Type == null)
|
||||
@ -508,7 +514,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
|
||||
// Extract the split portion and remove from the original slider.
|
||||
var splitControlPoints = controlPoints.Take(index + 1).ToList();
|
||||
controlPoints.RemoveRange(0, index);
|
||||
controlPointsProxy.RemoveRange(0, index);
|
||||
|
||||
var newSlider = new Slider
|
||||
{
|
||||
@ -521,28 +527,29 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
Path = new SliderPath(splitControlPoints.Select(o => new PathControlPoint(o.Position - splitControlPoints[0].Position, o == splitControlPoints[^1] ? null : o.Type)).ToArray())
|
||||
};
|
||||
|
||||
Proxy.StartTime += split_gap;
|
||||
|
||||
// Increase the start time of the slider before adding the new slider so the new slider is immediately inserted at the correct index and internal state remains valid.
|
||||
HitObject.StartTime += split_gap;
|
||||
commandHandler.SafeSubmit(new AddHitObjectCommand(editorBeatmap, newSlider));
|
||||
|
||||
editorBeatmap.Add(newSlider);
|
||||
|
||||
HitObject.NewCombo = false;
|
||||
HitObject.Path.ExpectedDistance.Value -= newSlider.Path.CalculatedDistance;
|
||||
HitObject.StartTime += newSlider.SpanDuration;
|
||||
Proxy.NewCombo = false;
|
||||
Proxy.Path.ExpectedDistance -= newSlider.Path.CalculatedDistance;
|
||||
Proxy.StartTime += newSlider.SpanDuration;
|
||||
|
||||
// In case the remainder of the slider has no length left over, give it length anyways so we don't get a 0 length slider.
|
||||
if (HitObject.Path.ExpectedDistance.Value <= Precision.DOUBLE_EPSILON)
|
||||
{
|
||||
HitObject.Path.ExpectedDistance.Value = null;
|
||||
Proxy.Path.ExpectedDistance = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Once all required pieces have been split off, the original slider has the final split.
|
||||
// As a final step, we must reset its control points to have an origin of (0,0).
|
||||
Vector2 first = controlPoints[0].Position;
|
||||
foreach (var c in controlPoints)
|
||||
foreach (var c in controlPointsProxy)
|
||||
c.Position -= first;
|
||||
HitObject.Position += first;
|
||||
|
||||
Proxy.Position += first;
|
||||
}
|
||||
|
||||
private void convertToStream()
|
||||
@ -553,8 +560,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
var timingPoint = editorBeatmap.ControlPointInfo.TimingPointAt(HitObject.StartTime);
|
||||
double streamSpacing = timingPoint.BeatLength / beatDivisor.Value;
|
||||
|
||||
changeHandler?.BeginChange();
|
||||
|
||||
int i = 0;
|
||||
double time = HitObject.StartTime;
|
||||
|
||||
@ -570,30 +575,29 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
|
||||
Vector2 position = HitObject.Position + HitObject.Path.PositionAt(pathPosition);
|
||||
|
||||
editorBeatmap.Add(new HitCircle
|
||||
commandHandler.SafeSubmit(new AddHitObjectCommand(editorBeatmap, new HitCircle
|
||||
{
|
||||
StartTime = time,
|
||||
Position = position,
|
||||
NewCombo = i == 0 && HitObject.NewCombo,
|
||||
Samples = HitObject.HeadCircle.Samples.Select(s => s.With()).ToList()
|
||||
});
|
||||
}));
|
||||
|
||||
i += 1;
|
||||
time = HitObject.StartTime + i * streamSpacing;
|
||||
}
|
||||
|
||||
editorBeatmap.Remove(HitObject);
|
||||
commandHandler.SafeSubmit(new RemoveHitObjectCommand(editorBeatmap, HitObject));
|
||||
|
||||
changeHandler?.EndChange();
|
||||
commandHandler?.Commit();
|
||||
}
|
||||
|
||||
public override MenuItem[] ContextMenuItems => new MenuItem[]
|
||||
{
|
||||
new OsuMenuItem("Add control point", MenuItemType.Standard, () =>
|
||||
{
|
||||
changeHandler?.BeginChange();
|
||||
addControlPoint(lastRightClickPosition);
|
||||
changeHandler?.EndChange();
|
||||
commandHandler?.Commit();
|
||||
})
|
||||
{
|
||||
Hotkey = new Hotkey(new KeyCombination(InputKey.Control, InputKey.MouseLeft))
|
||||
|
@ -0,0 +1,29 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Screens.Edit.Commands;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit.Commands
|
||||
{
|
||||
public class AddControlPointCommand : IEditorCommand
|
||||
{
|
||||
public readonly BindableList<PathControlPoint> ControlPoints;
|
||||
|
||||
public readonly int InsertionIndex;
|
||||
|
||||
public readonly PathControlPoint ControlPoint;
|
||||
|
||||
public AddControlPointCommand(BindableList<PathControlPoint> controlPoints, int insertionIndex, PathControlPoint controlPoint)
|
||||
{
|
||||
ControlPoints = controlPoints;
|
||||
InsertionIndex = insertionIndex;
|
||||
ControlPoint = controlPoint;
|
||||
}
|
||||
|
||||
public void Apply() => ControlPoints.Insert(InsertionIndex, ControlPoint);
|
||||
|
||||
public IEditorCommand CreateUndo() => new RemoveControlPointCommand(ControlPoints, InsertionIndex);
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.Commands;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit.Commands
|
||||
{
|
||||
public class OsuHitObjectCommandProxy : HitObjectCommandProxy
|
||||
{
|
||||
public OsuHitObjectCommandProxy(EditorCommandHandler? commandHandler, OsuHitObject hitObject)
|
||||
: base(commandHandler, hitObject)
|
||||
{
|
||||
}
|
||||
|
||||
protected new OsuHitObject HitObject => (OsuHitObject)base.HitObject;
|
||||
|
||||
public Vector2 Position
|
||||
{
|
||||
get => HitObject.Position;
|
||||
set => Submit(new MoveCommand(HitObject, value));
|
||||
}
|
||||
|
||||
public bool NewCombo
|
||||
{
|
||||
get => HitObject.NewCombo;
|
||||
set => Submit(new SetNewComboCommand(HitObject, value));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.Commands;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit.Commands
|
||||
{
|
||||
public class PathControlPointCommandProxy : CommandProxy
|
||||
{
|
||||
public PathControlPointCommandProxy(EditorCommandHandler? commandHandler, PathControlPoint controlPoint)
|
||||
: base(commandHandler)
|
||||
{
|
||||
ControlPoint = controlPoint;
|
||||
}
|
||||
|
||||
public readonly PathControlPoint ControlPoint;
|
||||
|
||||
public Vector2 Position
|
||||
{
|
||||
get => ControlPoint.Position;
|
||||
set => Submit(new UpdateControlPointCommand(ControlPoint) { Position = Position });
|
||||
}
|
||||
|
||||
public PathType? Type
|
||||
{
|
||||
get => ControlPoint.Type;
|
||||
set => Submit(new UpdateControlPointCommand(ControlPoint) { Type = Type });
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,113 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.Commands;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit.Commands
|
||||
{
|
||||
public class PathControlPointsCommandProxy : CommandProxy, IList<PathControlPointCommandProxy>
|
||||
{
|
||||
public PathControlPointsCommandProxy(EditorCommandHandler? commandHandler, BindableList<PathControlPoint> controlPoints)
|
||||
: base(commandHandler)
|
||||
{
|
||||
ControlPoints = controlPoints;
|
||||
}
|
||||
|
||||
public readonly BindableList<PathControlPoint> ControlPoints;
|
||||
|
||||
public int IndexOf(PathControlPointCommandProxy item)
|
||||
{
|
||||
return ControlPoints.IndexOf(item.ControlPoint);
|
||||
}
|
||||
|
||||
public void Insert(int index, PathControlPointCommandProxy item) => Insert(index, item.ControlPoint);
|
||||
|
||||
public void Insert(int index, PathControlPoint controlPoint) => Submit(new AddControlPointCommand(ControlPoints, index, controlPoint));
|
||||
|
||||
public void RemoveAt(int index) => Submit(new RemoveControlPointCommand(ControlPoints, index));
|
||||
|
||||
public PathControlPointCommandProxy this[int index]
|
||||
{
|
||||
get => new PathControlPointCommandProxy(CommandHandler, ControlPoints[index]);
|
||||
set => Submit(new AddControlPointCommand(ControlPoints, index, value.ControlPoint));
|
||||
}
|
||||
|
||||
public void RemoveRange(int index, int count)
|
||||
{
|
||||
for (int i = 0; i < count; i++)
|
||||
Submit(new RemoveControlPointCommand(ControlPoints, index));
|
||||
}
|
||||
|
||||
public void Add(PathControlPointCommandProxy item) => Add(item.ControlPoint);
|
||||
|
||||
public void Add(PathControlPoint controlPoint) => Submit(new AddControlPointCommand(ControlPoints, ControlPoints.Count, controlPoint));
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
while (ControlPoints.Count > 0)
|
||||
Remove(ControlPoints[0]);
|
||||
}
|
||||
|
||||
public bool Contains(PathControlPointCommandProxy item)
|
||||
{
|
||||
return ControlPoints.Any(c => c.Equals(item.ControlPoint));
|
||||
}
|
||||
|
||||
public void CopyTo(PathControlPointCommandProxy[] array, int arrayIndex)
|
||||
{
|
||||
for (int i = 0; i < ControlPoints.Count; i++)
|
||||
array[arrayIndex + i] = new PathControlPointCommandProxy(CommandHandler, ControlPoints[i]);
|
||||
}
|
||||
|
||||
public bool Remove(PathControlPointCommandProxy item) => Remove(item.ControlPoint);
|
||||
|
||||
public bool Remove(PathControlPoint controlPoint)
|
||||
{
|
||||
if (!ControlPoints.Contains(controlPoint))
|
||||
return false;
|
||||
|
||||
Submit(new RemoveControlPointCommand(ControlPoints, controlPoint));
|
||||
return true;
|
||||
}
|
||||
|
||||
public int Count => ControlPoints.Count;
|
||||
|
||||
public bool IsReadOnly => ControlPoints.IsReadOnly;
|
||||
|
||||
public IEnumerator<PathControlPointCommandProxy> GetEnumerator() => new PathControlPointsCommandProxyEnumerator(CommandHandler, ControlPoints.GetEnumerator());
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
|
||||
private readonly struct PathControlPointsCommandProxyEnumerator : IEnumerator<PathControlPointCommandProxy>
|
||||
{
|
||||
public PathControlPointsCommandProxyEnumerator(
|
||||
EditorCommandHandler? commandHandler,
|
||||
IEnumerator<PathControlPoint> enumerator
|
||||
)
|
||||
{
|
||||
this.commandHandler = commandHandler;
|
||||
this.enumerator = enumerator;
|
||||
}
|
||||
|
||||
private readonly EditorCommandHandler? commandHandler;
|
||||
|
||||
private readonly IEnumerator<PathControlPoint> enumerator;
|
||||
|
||||
public bool MoveNext() => enumerator.MoveNext();
|
||||
|
||||
public void Reset() => enumerator.Reset();
|
||||
|
||||
public PathControlPointCommandProxy Current => new PathControlPointCommandProxy(commandHandler, enumerator.Current);
|
||||
|
||||
object IEnumerator.Current => Current;
|
||||
|
||||
public void Dispose() => enumerator.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Screens.Edit.Commands;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit.Commands
|
||||
{
|
||||
public class RemoveControlPointCommand : IEditorCommand
|
||||
{
|
||||
public readonly BindableList<PathControlPoint> ControlPoints;
|
||||
|
||||
public readonly int Index;
|
||||
|
||||
public RemoveControlPointCommand(BindableList<PathControlPoint> controlPoints, int index)
|
||||
{
|
||||
ControlPoints = controlPoints;
|
||||
Index = index;
|
||||
}
|
||||
|
||||
public RemoveControlPointCommand(BindableList<PathControlPoint> controlPoints, PathControlPoint controlPoint)
|
||||
{
|
||||
ControlPoints = controlPoints;
|
||||
Index = controlPoints.IndexOf(controlPoint);
|
||||
}
|
||||
|
||||
public void Apply() => ControlPoints.RemoveAt(Index);
|
||||
|
||||
public IEditorCommand CreateUndo() => new AddControlPointCommand(ControlPoints, Index, ControlPoints[Index]);
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Screens.Edit.Commands;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit.Commands
|
||||
{
|
||||
public class SetSliderVelocityMultiplierCommand : IEditorCommand
|
||||
{
|
||||
public readonly Slider Slider;
|
||||
|
||||
public readonly double SliderVelocityMultiplier;
|
||||
|
||||
public SetSliderVelocityMultiplierCommand(Slider slider, double sliderVelocityMultiplier)
|
||||
{
|
||||
Slider = slider;
|
||||
SliderVelocityMultiplier = sliderVelocityMultiplier;
|
||||
}
|
||||
|
||||
public void Apply() => Slider.SliderVelocityMultiplier = SliderVelocityMultiplier;
|
||||
|
||||
public IEditorCommand CreateUndo() => new SetSliderVelocityMultiplierCommand(Slider, Slider.SliderVelocityMultiplier);
|
||||
}
|
||||
}
|
27
osu.Game.Rulesets.Osu/Edit/Commands/SetNewComboCommand.cs
Normal file
27
osu.Game.Rulesets.Osu/Edit/Commands/SetNewComboCommand.cs
Normal file
@ -0,0 +1,27 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Screens.Edit.Commands;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit.Commands
|
||||
{
|
||||
public class SetNewComboCommand : IEditorCommand
|
||||
{
|
||||
public OsuHitObject Target;
|
||||
|
||||
public bool NewCombo;
|
||||
|
||||
public SetNewComboCommand(OsuHitObject target, bool newCombo)
|
||||
{
|
||||
Target = target;
|
||||
NewCombo = newCombo;
|
||||
}
|
||||
|
||||
public void Apply() => Target.NewCombo = NewCombo;
|
||||
|
||||
public IEditorCommand CreateUndo() => new SetNewComboCommand(Target, Target.NewCombo);
|
||||
|
||||
public bool IsRedundant => NewCombo == Target.NewCombo;
|
||||
}
|
||||
}
|
26
osu.Game.Rulesets.Osu/Edit/Commands/SliderCommandProxy.cs
Normal file
26
osu.Game.Rulesets.Osu/Edit/Commands/SliderCommandProxy.cs
Normal file
@ -0,0 +1,26 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Screens.Edit;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit.Commands
|
||||
{
|
||||
public class SliderCommandProxy : OsuHitObjectCommandProxy
|
||||
{
|
||||
public SliderCommandProxy(EditorCommandHandler? commandHandler, Slider hitObject)
|
||||
: base(commandHandler, hitObject)
|
||||
{
|
||||
}
|
||||
|
||||
protected new Slider HitObject => (Slider)base.HitObject;
|
||||
|
||||
public SliderPathCommandProxy Path => new SliderPathCommandProxy(CommandHandler, HitObject.Path);
|
||||
|
||||
public double SliderVelocityMultiplier
|
||||
{
|
||||
get => HitObject.SliderVelocityMultiplier;
|
||||
set => Submit(new SetSliderVelocityMultiplierCommand(HitObject, value));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.Commands;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit.Commands
|
||||
{
|
||||
public class SliderPathCommandProxy : CommandProxy
|
||||
{
|
||||
public SliderPathCommandProxy(EditorCommandHandler? commandHandler, SliderPath path)
|
||||
: base(commandHandler)
|
||||
{
|
||||
Path = path;
|
||||
}
|
||||
|
||||
public readonly SliderPath Path;
|
||||
|
||||
public double? ExpectedDistance
|
||||
{
|
||||
get => Path.ExpectedDistance.Value;
|
||||
set => Submit(new SetExpectedDistanceCommand(Path, value));
|
||||
}
|
||||
|
||||
public PathControlPointsCommandProxy ControlPoints => new PathControlPointsCommandProxy(CommandHandler, Path.ControlPoints);
|
||||
|
||||
public IEnumerable<double> GetSegmentEnds() => Path.GetSegmentEnds();
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Screens.Edit.Commands;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit.Commands
|
||||
{
|
||||
public class UpdateControlPointCommand : IEditorCommand
|
||||
{
|
||||
public PathControlPoint ControlPoint;
|
||||
|
||||
public Vector2 Position;
|
||||
|
||||
public PathType? Type;
|
||||
|
||||
public UpdateControlPointCommand(PathControlPoint controlPoint)
|
||||
{
|
||||
ControlPoint = controlPoint;
|
||||
Position = controlPoint.Position;
|
||||
Type = controlPoint.Type;
|
||||
}
|
||||
|
||||
public UpdateControlPointCommand(PathControlPoint controlPoint, Vector2 position, PathType? type)
|
||||
{
|
||||
ControlPoint = controlPoint;
|
||||
Position = position;
|
||||
Type = type;
|
||||
}
|
||||
|
||||
public void Apply()
|
||||
{
|
||||
ControlPoint.Position = Position;
|
||||
ControlPoint.Type = Type;
|
||||
}
|
||||
|
||||
public IEditorCommand CreateUndo()
|
||||
{
|
||||
return new UpdateControlPointCommand(ControlPoint, ControlPoint.Position, ControlPoint.Type);
|
||||
}
|
||||
}
|
||||
}
|
@ -17,6 +17,8 @@ using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Beatmaps;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.Commands;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osu.Game.Utils;
|
||||
using osuTK;
|
||||
@ -51,6 +53,9 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
return false;
|
||||
}
|
||||
|
||||
[Resolved(canBeNull: true)]
|
||||
private EditorCommandHandler? commandManager { get; set; }
|
||||
|
||||
public override bool HandleMovement(MoveSelectionEvent<HitObject> moveEvent)
|
||||
{
|
||||
var hitObjects = selectedMovableObjects;
|
||||
@ -70,12 +75,10 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
if (hitObjects.Any(h => Precision.AlmostEquals(localDelta, -h.StackOffset)))
|
||||
return true;
|
||||
|
||||
// this will potentially move the selection out of bounds...
|
||||
foreach (var h in hitObjects)
|
||||
h.Position += localDelta;
|
||||
localDelta = moveSelectionInBounds(localDelta);
|
||||
|
||||
// but this will be corrected.
|
||||
moveSelectionInBounds();
|
||||
foreach (var h in hitObjects)
|
||||
commandManager.SafeSubmit(new MoveCommand(h, h.Position + localDelta));
|
||||
|
||||
// manually update stacking.
|
||||
// this intentionally bypasses the editor `UpdateState()` / beatmap processor flow for performance reasons,
|
||||
@ -105,7 +108,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
foreach (var h in hitObjects)
|
||||
{
|
||||
if (moreThanOneObject)
|
||||
h.StartTime = endTime - (h.GetEndTime() - startTime);
|
||||
commandManager.SafeSubmit(new SetStartTimeCommand(h, endTime - (h.GetEndTime() - startTime)));
|
||||
|
||||
if (h is Slider slider)
|
||||
{
|
||||
@ -187,26 +190,23 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
public override SelectionScaleHandler CreateScaleHandler() => new OsuSelectionScaleHandler();
|
||||
|
||||
private void moveSelectionInBounds()
|
||||
private Vector2 moveSelectionInBounds(Vector2 delta)
|
||||
{
|
||||
var hitObjects = selectedMovableObjects;
|
||||
|
||||
Quad quad = GeometryUtils.GetSurroundingQuad(hitObjects);
|
||||
|
||||
Vector2 delta = Vector2.Zero;
|
||||
if (quad.TopLeft.X + delta.X < 0)
|
||||
delta.X -= quad.TopLeft.X + delta.X;
|
||||
if (quad.TopLeft.Y + delta.Y < 0)
|
||||
delta.Y -= quad.TopLeft.Y + delta.Y;
|
||||
|
||||
if (quad.TopLeft.X < 0)
|
||||
delta.X -= quad.TopLeft.X;
|
||||
if (quad.TopLeft.Y < 0)
|
||||
delta.Y -= quad.TopLeft.Y;
|
||||
if (quad.BottomRight.X + delta.X > DrawWidth)
|
||||
delta.X -= quad.BottomRight.X + delta.X - DrawWidth;
|
||||
if (quad.BottomRight.Y + delta.Y > DrawHeight)
|
||||
delta.Y -= quad.BottomRight.Y + delta.Y - DrawHeight;
|
||||
|
||||
if (quad.BottomRight.X > DrawWidth)
|
||||
delta.X -= quad.BottomRight.X - DrawWidth;
|
||||
if (quad.BottomRight.Y > DrawHeight)
|
||||
delta.Y -= quad.BottomRight.Y - DrawHeight;
|
||||
|
||||
foreach (var h in hitObjects)
|
||||
h.Position += delta;
|
||||
return delta;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -309,6 +309,13 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
EditorBeatmap.EndChange();
|
||||
}
|
||||
|
||||
protected override void OnOperationEnded()
|
||||
{
|
||||
base.OnOperationEnded();
|
||||
|
||||
commandManager?.Commit();
|
||||
}
|
||||
|
||||
protected override IEnumerable<MenuItem> GetContextMenuItemsForSelection(IEnumerable<SelectionBlueprint<HitObject>> selection)
|
||||
{
|
||||
foreach (var item in base.GetContextMenuItemsForSelection(selection))
|
||||
|
@ -14,7 +14,7 @@ using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Objects
|
||||
{
|
||||
public abstract class OsuHitObject : HitObject, IHasComboInformation, IHasPosition, IHasTimePreempt
|
||||
public abstract class OsuHitObject : HitObject, IHasComboInformation, IHasMutablePosition, IHasTimePreempt
|
||||
{
|
||||
/// <summary>
|
||||
/// The radius of hit objects (ie. the radius of a <see cref="HitCircle"/>).
|
||||
@ -59,8 +59,17 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
set => position.Value = value;
|
||||
}
|
||||
|
||||
public float X => Position.X;
|
||||
public float Y => Position.Y;
|
||||
public float X
|
||||
{
|
||||
get => Position.X;
|
||||
set => Position = Position with { X = value };
|
||||
}
|
||||
|
||||
public float Y
|
||||
{
|
||||
get => Position.Y;
|
||||
set => Position = Position with { Y = value };
|
||||
}
|
||||
|
||||
public Vector2 StackedPosition => Position + StackOffset;
|
||||
|
||||
|
@ -27,6 +27,7 @@ using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.Commands;
|
||||
using osu.Game.Screens.Edit.Components.RadioButtons;
|
||||
using osu.Game.Screens.Edit.Components.TernaryButtons;
|
||||
using osu.Game.Screens.Edit.Compose;
|
||||
@ -489,20 +490,23 @@ namespace osu.Game.Rulesets.Edit
|
||||
EditorBeatmap.PlacementObject.Value = hitObject;
|
||||
}
|
||||
|
||||
[Resolved(canBeNull: true)]
|
||||
private EditorCommandHandler commandHandler { get; set; }
|
||||
|
||||
public void EndPlacement(HitObject hitObject, bool commit)
|
||||
{
|
||||
EditorBeatmap.PlacementObject.Value = null;
|
||||
|
||||
if (commit)
|
||||
{
|
||||
EditorBeatmap.Add(hitObject);
|
||||
commandHandler.SafeSubmit(new AddHitObjectCommand(EditorBeatmap, hitObject), true);
|
||||
|
||||
if (autoSeekOnPlacement.Value && EditorClock.CurrentTime < hitObject.StartTime)
|
||||
EditorClock.SeekSmoothlyTo(hitObject.StartTime);
|
||||
}
|
||||
}
|
||||
|
||||
public void Delete(HitObject hitObject) => EditorBeatmap.Remove(hitObject);
|
||||
public void Delete(HitObject hitObject) => commandHandler.SafeSubmit(new RemoveHitObjectCommand(EditorBeatmap, hitObject));
|
||||
|
||||
#endregion
|
||||
|
||||
|
@ -5,6 +5,8 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.Commands;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Objects
|
||||
@ -14,10 +16,12 @@ namespace osu.Game.Rulesets.Objects
|
||||
/// <summary>
|
||||
/// Snaps the provided <paramref name="hitObject"/>'s duration using the <paramref name="snapProvider"/>.
|
||||
/// </summary>
|
||||
public static void SnapTo<THitObject>(this THitObject hitObject, IDistanceSnapProvider? snapProvider)
|
||||
public static void SnapTo<THitObject>(this THitObject hitObject, IDistanceSnapProvider? snapProvider, EditorCommandHandler? commandHandler = null)
|
||||
where THitObject : HitObject, IHasPath
|
||||
{
|
||||
hitObject.Path.ExpectedDistance.Value = snapProvider?.FindSnappedDistance(hitObject, (float)hitObject.Path.CalculatedDistance, DistanceSnapTarget.Start) ?? hitObject.Path.CalculatedDistance;
|
||||
double distance = snapProvider?.FindSnappedDistance(hitObject, (float)hitObject.Path.CalculatedDistance, DistanceSnapTarget.Start) ?? hitObject.Path.CalculatedDistance;
|
||||
|
||||
commandHandler.SafeSubmit(new SetExpectedDistanceCommand(hitObject.Path, distance));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
18
osu.Game/Rulesets/Objects/Types/IHasMutablePosition.cs
Normal file
18
osu.Game/Rulesets/Objects/Types/IHasMutablePosition.cs
Normal file
@ -0,0 +1,18 @@
|
||||
// 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 osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Objects.Types
|
||||
{
|
||||
/// <summary>
|
||||
/// A HitObject that has a starting position that can be mutated.
|
||||
/// </summary>
|
||||
public interface IHasMutablePosition : IHasPosition, IHasMutableXPosition, IHasMutableYPosition
|
||||
{
|
||||
/// <summary>
|
||||
/// The starting position of the HitObject.
|
||||
/// </summary>
|
||||
new Vector2 Position { get; set; }
|
||||
}
|
||||
}
|
16
osu.Game/Rulesets/Objects/Types/IHasMutableXPosition.cs
Normal file
16
osu.Game/Rulesets/Objects/Types/IHasMutableXPosition.cs
Normal file
@ -0,0 +1,16 @@
|
||||
// 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.
|
||||
|
||||
namespace osu.Game.Rulesets.Objects.Types
|
||||
{
|
||||
/// <summary>
|
||||
/// A HitObject that has a starting X-position that can be mutated.
|
||||
/// </summary>
|
||||
public interface IHasMutableXPosition : IHasXPosition
|
||||
{
|
||||
/// <summary>
|
||||
/// The starting X-position of this HitObject.
|
||||
/// </summary>
|
||||
new float X { get; set; }
|
||||
}
|
||||
}
|
16
osu.Game/Rulesets/Objects/Types/IHasMutableYPosition.cs
Normal file
16
osu.Game/Rulesets/Objects/Types/IHasMutableYPosition.cs
Normal file
@ -0,0 +1,16 @@
|
||||
// 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.
|
||||
|
||||
namespace osu.Game.Rulesets.Objects.Types
|
||||
{
|
||||
/// <summary>
|
||||
/// A HitObject that has a starting Y-position that can be mutated.
|
||||
/// </summary>
|
||||
public interface IHasMutableYPosition : IHasYPosition
|
||||
{
|
||||
/// <summary>
|
||||
/// The starting Y-position of this HitObject.
|
||||
/// </summary>
|
||||
new float Y { get; set; }
|
||||
}
|
||||
}
|
24
osu.Game/Screens/Edit/Commands/AddHitObjectCommand.cs
Normal file
24
osu.Game/Screens/Edit/Commands/AddHitObjectCommand.cs
Normal file
@ -0,0 +1,24 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Rulesets.Objects;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Commands
|
||||
{
|
||||
public class AddHitObjectCommand : IEditorCommand
|
||||
{
|
||||
public EditorBeatmap Beatmap;
|
||||
|
||||
public HitObject HitObject;
|
||||
|
||||
public AddHitObjectCommand(EditorBeatmap beatmap, HitObject hitObject)
|
||||
{
|
||||
Beatmap = beatmap;
|
||||
HitObject = hitObject;
|
||||
}
|
||||
|
||||
public void Apply() => Beatmap.Add(HitObject);
|
||||
|
||||
public IEditorCommand CreateUndo() => new RemoveHitObjectCommand(Beatmap, HitObject);
|
||||
}
|
||||
}
|
21
osu.Game/Screens/Edit/Commands/CommandProxy.cs
Normal file
21
osu.Game/Screens/Edit/Commands/CommandProxy.cs
Normal file
@ -0,0 +1,21 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Commands
|
||||
{
|
||||
public abstract class CommandProxy
|
||||
{
|
||||
protected EditorCommandHandler? CommandHandler;
|
||||
|
||||
protected CommandProxy(EditorCommandHandler? commandHandler)
|
||||
{
|
||||
CommandHandler = commandHandler;
|
||||
}
|
||||
|
||||
protected void Submit(IEditorCommand command) => CommandHandler.SafeSubmit(command);
|
||||
|
||||
protected void Submit(IEnumerable<IEditorCommand> command) => CommandHandler.SafeSubmit(command);
|
||||
}
|
||||
}
|
24
osu.Game/Screens/Edit/Commands/HitObjectCommandProxy.cs
Normal file
24
osu.Game/Screens/Edit/Commands/HitObjectCommandProxy.cs
Normal file
@ -0,0 +1,24 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Rulesets.Objects;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Commands
|
||||
{
|
||||
public class HitObjectCommandProxy : CommandProxy
|
||||
{
|
||||
public HitObjectCommandProxy(EditorCommandHandler? commandHandler, HitObject hitObject)
|
||||
: base(commandHandler)
|
||||
{
|
||||
HitObject = hitObject;
|
||||
}
|
||||
|
||||
protected HitObject HitObject;
|
||||
|
||||
public double StartTime
|
||||
{
|
||||
get => HitObject.StartTime;
|
||||
set => Submit(new SetStartTimeCommand(HitObject, value));
|
||||
}
|
||||
}
|
||||
}
|
14
osu.Game/Screens/Edit/Commands/IEditorCommand.cs
Normal file
14
osu.Game/Screens/Edit/Commands/IEditorCommand.cs
Normal file
@ -0,0 +1,14 @@
|
||||
// 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.
|
||||
|
||||
namespace osu.Game.Screens.Edit.Commands
|
||||
{
|
||||
public interface IEditorCommand
|
||||
{
|
||||
public void Apply();
|
||||
|
||||
public IEditorCommand CreateUndo();
|
||||
|
||||
public virtual bool IsRedundant => false;
|
||||
}
|
||||
}
|
27
osu.Game/Screens/Edit/Commands/MoveCommand.cs
Normal file
27
osu.Game/Screens/Edit/Commands/MoveCommand.cs
Normal file
@ -0,0 +1,27 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Commands
|
||||
{
|
||||
public class MoveCommand : IEditorCommand
|
||||
{
|
||||
public readonly IHasMutablePosition Target;
|
||||
|
||||
public readonly Vector2 Position;
|
||||
|
||||
public MoveCommand(IHasMutablePosition target, Vector2 position)
|
||||
{
|
||||
Target = target;
|
||||
Position = position;
|
||||
}
|
||||
|
||||
public void Apply() => Target.Position = Position;
|
||||
|
||||
public IEditorCommand CreateUndo() => new MoveCommand(Target, Target.Position);
|
||||
|
||||
public bool IsRedundant => Position == Target.Position;
|
||||
}
|
||||
}
|
27
osu.Game/Screens/Edit/Commands/MoveXCommand.cs
Normal file
27
osu.Game/Screens/Edit/Commands/MoveXCommand.cs
Normal file
@ -0,0 +1,27 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Commands
|
||||
{
|
||||
public class MoveXCommand : IEditorCommand
|
||||
{
|
||||
public readonly IHasMutablePosition Target;
|
||||
|
||||
public readonly float X;
|
||||
|
||||
public MoveXCommand(IHasMutablePosition target, float x)
|
||||
{
|
||||
Target = target;
|
||||
X = x;
|
||||
}
|
||||
|
||||
public void Apply() => Target.X = X;
|
||||
|
||||
public IEditorCommand CreateUndo() => new MoveXCommand(Target, Target.X);
|
||||
|
||||
public bool IsRedundant => Precision.AlmostEquals(X, Target.X);
|
||||
}
|
||||
}
|
27
osu.Game/Screens/Edit/Commands/MoveYCommand.cs
Normal file
27
osu.Game/Screens/Edit/Commands/MoveYCommand.cs
Normal file
@ -0,0 +1,27 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Commands
|
||||
{
|
||||
public class MoveYCommand : IEditorCommand
|
||||
{
|
||||
public readonly IHasMutablePosition Target;
|
||||
|
||||
public readonly float Y;
|
||||
|
||||
public MoveYCommand(IHasMutablePosition target, float y)
|
||||
{
|
||||
Target = target;
|
||||
Y = y;
|
||||
}
|
||||
|
||||
public void Apply() => Target.Y = Y;
|
||||
|
||||
public IEditorCommand CreateUndo() => new MoveXCommand(Target, Target.Y);
|
||||
|
||||
public bool IsRedundant => Precision.AlmostEquals(Y, Target.Y);
|
||||
}
|
||||
}
|
24
osu.Game/Screens/Edit/Commands/RemoveHitObjectCommand.cs
Normal file
24
osu.Game/Screens/Edit/Commands/RemoveHitObjectCommand.cs
Normal file
@ -0,0 +1,24 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Rulesets.Objects;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Commands
|
||||
{
|
||||
public class RemoveHitObjectCommand : IEditorCommand
|
||||
{
|
||||
public EditorBeatmap Beatmap;
|
||||
|
||||
public HitObject HitObject;
|
||||
|
||||
public RemoveHitObjectCommand(EditorBeatmap beatmap, HitObject hitObject)
|
||||
{
|
||||
Beatmap = beatmap;
|
||||
HitObject = hitObject;
|
||||
}
|
||||
|
||||
public void Apply() => Beatmap.Remove(HitObject);
|
||||
|
||||
public IEditorCommand CreateUndo() => new AddHitObjectCommand(Beatmap, HitObject);
|
||||
}
|
||||
}
|
24
osu.Game/Screens/Edit/Commands/SetExpectedDistanceCommand.cs
Normal file
24
osu.Game/Screens/Edit/Commands/SetExpectedDistanceCommand.cs
Normal file
@ -0,0 +1,24 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Rulesets.Objects;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Commands
|
||||
{
|
||||
public class SetExpectedDistanceCommand : IEditorCommand
|
||||
{
|
||||
public readonly SliderPath Path;
|
||||
|
||||
public readonly double? ExpectedDistance;
|
||||
|
||||
public SetExpectedDistanceCommand(SliderPath path, double? expectedDistance)
|
||||
{
|
||||
Path = path;
|
||||
ExpectedDistance = expectedDistance;
|
||||
}
|
||||
|
||||
public void Apply() => Path.ExpectedDistance.Value = ExpectedDistance;
|
||||
|
||||
public IEditorCommand CreateUndo() => new SetExpectedDistanceCommand(Path, Path.ExpectedDistance.Value);
|
||||
}
|
||||
}
|
27
osu.Game/Screens/Edit/Commands/SetStartTimeCommand.cs
Normal file
27
osu.Game/Screens/Edit/Commands/SetStartTimeCommand.cs
Normal file
@ -0,0 +1,27 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Commands
|
||||
{
|
||||
public class SetStartTimeCommand : IEditorCommand
|
||||
{
|
||||
public readonly HitObject Target;
|
||||
|
||||
public readonly double StartTime;
|
||||
|
||||
public SetStartTimeCommand(HitObject target, double startTime)
|
||||
{
|
||||
Target = target;
|
||||
StartTime = startTime;
|
||||
}
|
||||
|
||||
public void Apply() => Target.StartTime = StartTime;
|
||||
|
||||
public IEditorCommand CreateUndo() => new SetStartTimeCommand(Target, Target.StartTime);
|
||||
|
||||
public bool IsRedundant => Precision.AlmostEquals(StartTime, Target.StartTime);
|
||||
}
|
||||
}
|
@ -42,7 +42,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
private IPositionSnapProvider snapProvider { get; set; }
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private IEditorChangeHandler changeHandler { get; set; }
|
||||
private EditorCommandHandler commandHandler { get; set; }
|
||||
|
||||
protected readonly BindableList<T> SelectedItems = new BindableList<T>();
|
||||
|
||||
@ -190,7 +190,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
if (movementBlueprints != null)
|
||||
{
|
||||
isDraggingBlueprint = true;
|
||||
changeHandler?.BeginChange();
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -218,7 +217,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
if (isDraggingBlueprint)
|
||||
{
|
||||
DragOperationCompleted();
|
||||
changeHandler?.EndChange();
|
||||
commandHandler?.Commit();
|
||||
}
|
||||
|
||||
DragBox.Hide();
|
||||
|
@ -52,7 +52,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
protected SelectionBox SelectionBox { get; private set; } = null!;
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
protected IEditorChangeHandler? ChangeHandler { get; private set; }
|
||||
protected EditorCommandHandler? CommandHandler { get; private set; }
|
||||
|
||||
public SelectionRotationHandler RotationHandler { get; private set; } = null!;
|
||||
|
||||
@ -105,7 +105,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
/// </summary>
|
||||
protected virtual void OnOperationBegan()
|
||||
{
|
||||
ChangeHandler?.BeginChange();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -113,7 +112,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
/// </summary>
|
||||
protected virtual void OnOperationEnded()
|
||||
{
|
||||
ChangeHandler?.EndChange();
|
||||
CommandHandler?.Commit();
|
||||
}
|
||||
|
||||
#region User Input Handling
|
||||
@ -188,16 +187,14 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
switch (e.Action)
|
||||
{
|
||||
case GlobalAction.EditorFlipHorizontally:
|
||||
ChangeHandler?.BeginChange();
|
||||
handled = HandleFlip(Direction.Horizontal, true);
|
||||
ChangeHandler?.EndChange();
|
||||
CommandHandler?.Commit();
|
||||
|
||||
return handled;
|
||||
|
||||
case GlobalAction.EditorFlipVertically:
|
||||
ChangeHandler?.BeginChange();
|
||||
handled = HandleFlip(Direction.Vertical, true);
|
||||
ChangeHandler?.EndChange();
|
||||
CommandHandler?.Commit();
|
||||
|
||||
return handled;
|
||||
}
|
||||
|
@ -179,6 +179,8 @@ namespace osu.Game.Screens.Edit
|
||||
[CanBeNull] // Should be non-null once it can support custom rulesets.
|
||||
private EditorChangeHandler changeHandler;
|
||||
|
||||
private EditorCommandHandler commandHandler;
|
||||
|
||||
private DependencyContainer dependencies;
|
||||
|
||||
private bool isNewBeatmap;
|
||||
@ -300,6 +302,9 @@ namespace osu.Game.Screens.Edit
|
||||
dependencies.CacheAs<IEditorChangeHandler>(changeHandler);
|
||||
}
|
||||
|
||||
commandHandler = new EditorCommandHandler();
|
||||
dependencies.CacheAs(commandHandler);
|
||||
|
||||
beatDivisor.SetArbitraryDivisor(editorBeatmap.BeatmapInfo.BeatDivisor);
|
||||
beatDivisor.BindValueChanged(divisor => editorBeatmap.BeatmapInfo.BeatDivisor = divisor.NewValue);
|
||||
|
||||
@ -428,8 +433,8 @@ namespace osu.Game.Screens.Edit
|
||||
}
|
||||
});
|
||||
|
||||
changeHandler?.CanUndo.BindValueChanged(v => undoMenuItem.Action.Disabled = !v.NewValue, true);
|
||||
changeHandler?.CanRedo.BindValueChanged(v => redoMenuItem.Action.Disabled = !v.NewValue, true);
|
||||
commandHandler.CanUndo.BindValueChanged(v => undoMenuItem.Action.Disabled = !v.NewValue, true);
|
||||
commandHandler.CanRedo.BindValueChanged(v => redoMenuItem.Action.Disabled = !v.NewValue, true);
|
||||
|
||||
editorBackgroundDim.BindValueChanged(_ => dimBackground());
|
||||
}
|
||||
@ -964,9 +969,9 @@ namespace osu.Game.Screens.Edit
|
||||
|
||||
#endregion
|
||||
|
||||
protected void Undo() => changeHandler?.RestoreState(-1);
|
||||
protected void Undo() => commandHandler.Undo();
|
||||
|
||||
protected void Redo() => changeHandler?.RestoreState(1);
|
||||
protected void Redo() => commandHandler.Redo();
|
||||
|
||||
protected void SetPreviewPointToCurrentTime()
|
||||
{
|
||||
|
159
osu.Game/Screens/Edit/EditorCommandHandler.cs
Normal file
159
osu.Game/Screens/Edit/EditorCommandHandler.cs
Normal file
@ -0,0 +1,159 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Game.Screens.Edit.Commands;
|
||||
|
||||
namespace osu.Game.Screens.Edit
|
||||
{
|
||||
public partial class EditorCommandHandler
|
||||
{
|
||||
public EditorCommandHandler()
|
||||
{
|
||||
}
|
||||
|
||||
public event Action<IEditorCommand>? CommandApplied;
|
||||
|
||||
public readonly Bindable<bool> CanUndo = new BindableBool();
|
||||
|
||||
public readonly Bindable<bool> CanRedo = new BindableBool();
|
||||
|
||||
public bool HasUncommittedChanges => currentTransaction.Entries.Count != 0;
|
||||
|
||||
public void Submit(IEditorCommand command, bool commitImmediately = false)
|
||||
{
|
||||
if (command.IsRedundant)
|
||||
return;
|
||||
|
||||
record(command);
|
||||
apply(command);
|
||||
|
||||
if (commitImmediately)
|
||||
Commit();
|
||||
}
|
||||
|
||||
public void Submit(IEnumerable<IEditorCommand> commands, bool commitImmediately = false)
|
||||
{
|
||||
foreach (var command in commands)
|
||||
Submit(command);
|
||||
|
||||
if (commitImmediately)
|
||||
Commit();
|
||||
}
|
||||
|
||||
public bool Commit()
|
||||
{
|
||||
if (!HasUncommittedChanges)
|
||||
{
|
||||
Logger.Log("Nothing to commit");
|
||||
return false;
|
||||
}
|
||||
|
||||
undoStack.Push(currentTransaction);
|
||||
redoStack.Clear();
|
||||
|
||||
Logger.Log($"Added {currentTransaction.Entries.Count} command(s) to undo stack");
|
||||
|
||||
currentTransaction = new Transaction();
|
||||
|
||||
historyChanged();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Undo()
|
||||
{
|
||||
if (undoStack.Count == 0)
|
||||
return false;
|
||||
|
||||
var transaction = undoStack.Pop();
|
||||
|
||||
revertTransaction(transaction);
|
||||
|
||||
redoStack.Push(transaction);
|
||||
|
||||
historyChanged();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Redo()
|
||||
{
|
||||
if (redoStack.Count == 0)
|
||||
return false;
|
||||
|
||||
var transaction = redoStack.Pop();
|
||||
|
||||
foreach (var entry in transaction.Entries)
|
||||
apply(entry.Command);
|
||||
|
||||
undoStack.Push(transaction);
|
||||
|
||||
historyChanged();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool RevertUncommitedChanges()
|
||||
{
|
||||
if (!HasUncommittedChanges)
|
||||
return false;
|
||||
|
||||
revertTransaction(currentTransaction);
|
||||
|
||||
currentTransaction = new Transaction();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void revertTransaction(Transaction transaction)
|
||||
{
|
||||
foreach (var entry in transaction.Entries.Reverse())
|
||||
apply(entry.Reverse);
|
||||
}
|
||||
|
||||
private void historyChanged()
|
||||
{
|
||||
CanUndo.Value = undoStack.Count > 0;
|
||||
CanRedo.Value = redoStack.Count > 0;
|
||||
}
|
||||
|
||||
private Transaction currentTransaction = new Transaction();
|
||||
|
||||
private readonly Stack<Transaction> undoStack = new Stack<Transaction>();
|
||||
|
||||
private readonly Stack<Transaction> redoStack = new Stack<Transaction>();
|
||||
|
||||
private void apply(IEditorCommand command)
|
||||
{
|
||||
command.Apply();
|
||||
CommandApplied?.Invoke(command);
|
||||
}
|
||||
|
||||
private void record(IEditorCommand command)
|
||||
{
|
||||
var reverse = command.CreateUndo();
|
||||
|
||||
currentTransaction.Add(new HistoryEntry(command, reverse));
|
||||
}
|
||||
|
||||
private readonly record struct HistoryEntry(IEditorCommand Command, IEditorCommand Reverse);
|
||||
|
||||
private readonly struct Transaction
|
||||
{
|
||||
public Transaction()
|
||||
{
|
||||
}
|
||||
|
||||
private readonly List<HistoryEntry> entries = new List<HistoryEntry>();
|
||||
|
||||
public IReadOnlyList<HistoryEntry> Entries => entries;
|
||||
|
||||
public void Add(HistoryEntry entry) => entries.Add(entry);
|
||||
}
|
||||
}
|
||||
}
|
30
osu.Game/Screens/Edit/EditorCommandManagerExtension.cs
Normal file
30
osu.Game/Screens/Edit/EditorCommandManagerExtension.cs
Normal file
@ -0,0 +1,30 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Screens.Edit.Commands;
|
||||
|
||||
namespace osu.Game.Screens.Edit
|
||||
{
|
||||
public static class EditorCommandManagerExtension
|
||||
{
|
||||
public static void SafeSubmit(this EditorCommandHandler? manager, IEditorCommand command, bool commitImmediately = false)
|
||||
{
|
||||
if (manager != null)
|
||||
manager.Submit(command, commitImmediately);
|
||||
else
|
||||
command.Apply();
|
||||
}
|
||||
|
||||
public static void SafeSubmit(this EditorCommandHandler? manager, IEnumerable<IEditorCommand> commands, bool commitImmediately = false)
|
||||
{
|
||||
if (manager != null)
|
||||
manager.Submit(commands, commitImmediately);
|
||||
else
|
||||
{
|
||||
foreach (var command in commands)
|
||||
command.Apply();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user