diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index 624fd28c85..64b26728f3 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -182,7 +182,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components private bool isSplittable(PathControlPointPiece p) => // A hit object can only be split on control points which connect two different path segments. - p.ControlPoint.Type.HasValue && p != Pieces.FirstOrDefault() && p != Pieces.LastOrDefault(); + p.ControlPoint.Type.HasValue && p.ControlPoint != controlPoints.FirstOrDefault() && p.ControlPoint != controlPoints.LastOrDefault(); private void onControlPointsChanged(object sender, NotifyCollectionChangedEventArgs e) { @@ -402,8 +402,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components selectedControlPoints = new HashSet(Pieces.Where(piece => piece.IsSelected.Value).Select(piece => piece.ControlPoint)); Debug.Assert(draggedControlPointIndex >= 0); - - commandHandler?.Commit(); } [Resolved(CanBeNull = true)] diff --git a/osu.Game/Screens/Edit/Commands/IMergeableCommand.cs b/osu.Game/Screens/Edit/Commands/IMergeableCommand.cs new file mode 100644 index 0000000000..d0afc6a23e --- /dev/null +++ b/osu.Game/Screens/Edit/Commands/IMergeableCommand.cs @@ -0,0 +1,10 @@ +// Copyright (c) ppy Pty Ltd . 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 IMergeableCommand : IEditorCommand + { + public IEditorCommand? MergeWith(IEditorCommand previous); + } +} diff --git a/osu.Game/Screens/Edit/Commands/MoveCommand.cs b/osu.Game/Screens/Edit/Commands/MoveCommand.cs index 360080fe4e..6ab4606fd2 100644 --- a/osu.Game/Screens/Edit/Commands/MoveCommand.cs +++ b/osu.Game/Screens/Edit/Commands/MoveCommand.cs @@ -6,7 +6,7 @@ using osuTK; namespace osu.Game.Screens.Edit.Commands { - public class MoveCommand : IEditorCommand + public class MoveCommand : IEditorCommand, IMergeableCommand { public readonly IHasMutablePosition Target; @@ -23,5 +23,13 @@ namespace osu.Game.Screens.Edit.Commands public IEditorCommand CreateUndo() => new MoveCommand(Target, Target.Position); public bool IsRedundant => Position == Target.Position; + + public IEditorCommand? MergeWith(IEditorCommand previous) + { + if (previous is MoveCommand moveCommand) + return moveCommand.Target != Target ? null : this; + + return null; + } } } diff --git a/osu.Game/Screens/Edit/Commands/PropertyChangeCommand.cs b/osu.Game/Screens/Edit/Commands/PropertyChangeCommand.cs new file mode 100644 index 0000000000..b2b375d10f --- /dev/null +++ b/osu.Game/Screens/Edit/Commands/PropertyChangeCommand.cs @@ -0,0 +1,36 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Screens.Edit.Commands +{ + public abstract class PropertyChangeCommand : IEditorCommand, IMergeableCommand where TTarget : class + { + protected abstract TProperty ReadValue(TTarget target); + + protected abstract void WriteValue(TTarget target, TProperty value); + + protected abstract PropertyChangeCommand CreateInstance(TTarget target, TProperty value); + + public readonly TTarget Target; + + public readonly TProperty Value; + + protected PropertyChangeCommand(TTarget target, TProperty value) + { + Target = target; + Value = value; + } + + public void Apply() => WriteValue(Target, Value); + + public IEditorCommand CreateUndo() => CreateInstance(Target, Value); + + public IEditorCommand? MergeWith(IEditorCommand previous) + { + if (previous is PropertyChangeCommand command && command.Target == Target) + return this; + + return null; + } + } +} diff --git a/osu.Game/Screens/Edit/EditorCommandHandler.cs b/osu.Game/Screens/Edit/EditorCommandHandler.cs index 1cb7fdcb47..b5fe13891e 100644 --- a/osu.Game/Screens/Edit/EditorCommandHandler.cs +++ b/osu.Game/Screens/Edit/EditorCommandHandler.cs @@ -22,7 +22,7 @@ namespace osu.Game.Screens.Edit public readonly Bindable CanRedo = new BindableBool(); - public bool HasUncommittedChanges => currentTransaction.Entries.Count != 0; + public bool HasUncommittedChanges => currentTransaction.Commands.Count != 0; public void Submit(IEditorCommand command, bool commitImmediately = false) { @@ -56,7 +56,7 @@ namespace osu.Game.Screens.Edit undoStack.Push(currentTransaction); redoStack.Clear(); - Logger.Log($"Added {currentTransaction.Entries.Count} command(s) to undo stack"); + Logger.Log($"Added {currentTransaction.Commands.Count} command(s) to undo stack"); currentTransaction = new Transaction(); @@ -71,10 +71,11 @@ namespace osu.Game.Screens.Edit return false; var transaction = undoStack.Pop(); + var redoTransaction = transaction.Reverse(); revertTransaction(transaction); - redoStack.Push(transaction); + redoStack.Push(redoTransaction); historyChanged(); @@ -87,11 +88,12 @@ namespace osu.Game.Screens.Edit return false; var transaction = redoStack.Pop(); + var undoTransaction = transaction.Reverse(); - foreach (var entry in transaction.Entries) - apply(entry.Command); + foreach (var command in transaction.Commands) + apply(command); - undoStack.Push(transaction); + undoStack.Push(undoTransaction); historyChanged(); @@ -112,8 +114,8 @@ namespace osu.Game.Screens.Edit private void revertTransaction(Transaction transaction) { - foreach (var entry in transaction.Entries.Reverse()) - apply(entry.Reverse); + foreach (var command in transaction.Commands.Reverse()) + apply(command); } private void historyChanged() @@ -138,7 +140,7 @@ namespace osu.Game.Screens.Edit { var reverse = command.CreateUndo(); - currentTransaction.Add(new HistoryEntry(command, reverse)); + currentTransaction.Add(reverse); } private readonly record struct HistoryEntry(IEditorCommand Command, IEditorCommand Reverse); @@ -149,11 +151,47 @@ namespace osu.Game.Screens.Edit { } - private readonly List entries = new List(); + private readonly List commands = new List(); - public IReadOnlyList Entries => entries; + public IReadOnlyList Commands => commands; - public void Add(HistoryEntry entry) => entries.Add(entry); + public void Add(IEditorCommand command) + { + if (command is IMergeableCommand mergeable) + { + for (int i = 0; i < commands.Count; i++) + { + var merged = mergeable.MergeWith(commands[i]); + + if (merged == null) + continue; + + command = merged; + commands.RemoveAt(i--); + + if (command is IMergeableCommand newMergeable) + { + mergeable = newMergeable; + } + else + { + break; + } + } + } + + commands.Add(command); + } + + public Transaction Reverse() + { + var reversed = new Transaction(); + + foreach (var command in Commands.Reverse()) + reversed.Add(command.CreateUndo()); + + return reversed; + } } } } diff --git a/osu.Game/Screens/Edit/EditorCommandManagerExtension.cs b/osu.Game/Screens/Edit/EditorCommandHandlerExtension.cs similarity index 94% rename from osu.Game/Screens/Edit/EditorCommandManagerExtension.cs rename to osu.Game/Screens/Edit/EditorCommandHandlerExtension.cs index 029a0c5878..3b8e8dc404 100644 --- a/osu.Game/Screens/Edit/EditorCommandManagerExtension.cs +++ b/osu.Game/Screens/Edit/EditorCommandHandlerExtension.cs @@ -6,7 +6,7 @@ using osu.Game.Screens.Edit.Commands; namespace osu.Game.Screens.Edit { - public static class EditorCommandManagerExtension + public static class EditorCommandHandlerExtension { public static void SafeSubmit(this EditorCommandHandler? manager, IEditorCommand command, bool commitImmediately = false) {