-
-
Notifications
You must be signed in to change notification settings - Fork 2.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #22506 from peppy/skin-editor-undo-support
Add very basic skin editor undo / redo support
- Loading branch information
Showing
14 changed files
with
339 additions
and
59 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,6 @@ | ||
// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the MIT Licence. | ||
// See the LICENCE file in the repository root for full licence text. | ||
|
||
#nullable disable | ||
|
||
using NUnit.Framework; | ||
using osu.Game.Rulesets.Osu; | ||
using osu.Game.Rulesets.Osu.Beatmaps; | ||
|
@@ -12,7 +10,7 @@ | |
namespace osu.Game.Tests.Editing | ||
{ | ||
[TestFixture] | ||
public class EditorChangeHandlerTest | ||
public class BeatmapEditorChangeHandlerTest | ||
{ | ||
private int stateChangedFired; | ||
|
||
|
@@ -22,6 +20,35 @@ public void SetUp() | |
stateChangedFired = 0; | ||
} | ||
|
||
[Test] | ||
public void TestSaveRestoreStateUsingTransaction() | ||
{ | ||
var (handler, beatmap) = createChangeHandler(); | ||
|
||
Assert.That(handler.CanUndo.Value, Is.False); | ||
Assert.That(handler.CanRedo.Value, Is.False); | ||
|
||
handler.BeginChange(); | ||
|
||
// Initial state will be saved on BeginChange | ||
Assert.That(stateChangedFired, Is.EqualTo(1)); | ||
|
||
addArbitraryChange(beatmap); | ||
handler.EndChange(); | ||
|
||
Assert.That(stateChangedFired, Is.EqualTo(2)); | ||
|
||
Assert.That(handler.CanUndo.Value, Is.True); | ||
Assert.That(handler.CanRedo.Value, Is.False); | ||
|
||
handler.RestoreState(-1); | ||
|
||
Assert.That(handler.CanUndo.Value, Is.False); | ||
Assert.That(handler.CanRedo.Value, Is.True); | ||
|
||
Assert.That(stateChangedFired, Is.EqualTo(3)); | ||
} | ||
|
||
[Test] | ||
public void TestSaveRestoreState() | ||
{ | ||
|
@@ -30,10 +57,14 @@ public void TestSaveRestoreState() | |
Assert.That(handler.CanUndo.Value, Is.False); | ||
Assert.That(handler.CanRedo.Value, Is.False); | ||
|
||
// Save initial state | ||
handler.SaveState(); | ||
Assert.That(stateChangedFired, Is.EqualTo(1)); | ||
|
||
addArbitraryChange(beatmap); | ||
handler.SaveState(); | ||
|
||
Assert.That(stateChangedFired, Is.EqualTo(1)); | ||
Assert.That(stateChangedFired, Is.EqualTo(2)); | ||
|
||
Assert.That(handler.CanUndo.Value, Is.True); | ||
Assert.That(handler.CanRedo.Value, Is.False); | ||
|
@@ -43,7 +74,7 @@ public void TestSaveRestoreState() | |
Assert.That(handler.CanUndo.Value, Is.False); | ||
Assert.That(handler.CanRedo.Value, Is.True); | ||
|
||
Assert.That(stateChangedFired, Is.EqualTo(2)); | ||
Assert.That(stateChangedFired, Is.EqualTo(3)); | ||
} | ||
|
||
[Test] | ||
|
@@ -54,22 +85,26 @@ public void TestApplyThenUndoThenApplySameChange() | |
Assert.That(handler.CanUndo.Value, Is.False); | ||
Assert.That(handler.CanRedo.Value, Is.False); | ||
|
||
// Save initial state | ||
handler.SaveState(); | ||
Assert.That(stateChangedFired, Is.EqualTo(1)); | ||
|
||
string originalHash = handler.CurrentStateHash; | ||
|
||
addArbitraryChange(beatmap); | ||
handler.SaveState(); | ||
|
||
Assert.That(handler.CanUndo.Value, Is.True); | ||
Assert.That(handler.CanRedo.Value, Is.False); | ||
Assert.That(stateChangedFired, Is.EqualTo(1)); | ||
Assert.That(stateChangedFired, Is.EqualTo(2)); | ||
|
||
string hash = handler.CurrentStateHash; | ||
|
||
// undo a change without saving | ||
handler.RestoreState(-1); | ||
|
||
Assert.That(originalHash, Is.EqualTo(handler.CurrentStateHash)); | ||
Assert.That(stateChangedFired, Is.EqualTo(2)); | ||
Assert.That(stateChangedFired, Is.EqualTo(3)); | ||
|
||
addArbitraryChange(beatmap); | ||
handler.SaveState(); | ||
|
@@ -84,20 +119,24 @@ public void TestSaveSameStateDoesNotSave() | |
Assert.That(handler.CanUndo.Value, Is.False); | ||
Assert.That(handler.CanRedo.Value, Is.False); | ||
|
||
// Save initial state | ||
handler.SaveState(); | ||
Assert.That(stateChangedFired, Is.EqualTo(1)); | ||
|
||
addArbitraryChange(beatmap); | ||
handler.SaveState(); | ||
|
||
Assert.That(handler.CanUndo.Value, Is.True); | ||
Assert.That(handler.CanRedo.Value, Is.False); | ||
Assert.That(stateChangedFired, Is.EqualTo(1)); | ||
Assert.That(stateChangedFired, Is.EqualTo(2)); | ||
|
||
string hash = handler.CurrentStateHash; | ||
|
||
// save a save without making any changes | ||
handler.SaveState(); | ||
|
||
Assert.That(hash, Is.EqualTo(handler.CurrentStateHash)); | ||
Assert.That(stateChangedFired, Is.EqualTo(1)); | ||
Assert.That(stateChangedFired, Is.EqualTo(2)); | ||
|
||
handler.RestoreState(-1); | ||
|
||
|
@@ -106,19 +145,23 @@ public void TestSaveSameStateDoesNotSave() | |
// we should only be able to restore once even though we saved twice. | ||
Assert.That(handler.CanUndo.Value, Is.False); | ||
Assert.That(handler.CanRedo.Value, Is.True); | ||
Assert.That(stateChangedFired, Is.EqualTo(2)); | ||
Assert.That(stateChangedFired, Is.EqualTo(3)); | ||
} | ||
|
||
[Test] | ||
public void TestMaxStatesSaved() | ||
{ | ||
var (handler, beatmap) = createChangeHandler(); | ||
|
||
// Save initial state | ||
handler.SaveState(); | ||
Assert.That(stateChangedFired, Is.EqualTo(1)); | ||
|
||
Assert.That(handler.CanUndo.Value, Is.False); | ||
|
||
for (int i = 0; i < EditorChangeHandler.MAX_SAVED_STATES; i++) | ||
{ | ||
Assert.That(stateChangedFired, Is.EqualTo(i)); | ||
Assert.That(stateChangedFired, Is.EqualTo(i + 1)); | ||
|
||
addArbitraryChange(beatmap); | ||
handler.SaveState(); | ||
|
@@ -169,7 +212,7 @@ public void TestMaxStatesExceeded() | |
}, | ||
}); | ||
|
||
var changeHandler = new EditorChangeHandler(beatmap); | ||
var changeHandler = new BeatmapEditorChangeHandler(beatmap); | ||
|
||
changeHandler.OnStateChange += () => stateChangedFired++; | ||
return (changeHandler, beatmap); | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the MIT Licence. | ||
// See the LICENCE file in the repository root for full licence text. | ||
|
||
using System.Collections.Generic; | ||
using System.IO; | ||
using System.Linq; | ||
using System.Text; | ||
using Newtonsoft.Json; | ||
using osu.Framework.Bindables; | ||
using osu.Framework.Graphics; | ||
using osu.Framework.Testing; | ||
using osu.Game.Extensions; | ||
using osu.Game.Screens.Edit; | ||
using osu.Game.Screens.Play.HUD; | ||
using osu.Game.Skinning; | ||
|
||
namespace osu.Game.Overlays.SkinEditor | ||
{ | ||
public partial class SkinEditorChangeHandler : EditorChangeHandler | ||
{ | ||
private readonly ISkinnableTarget? firstTarget; | ||
|
||
// ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable | ||
private readonly BindableList<ISkinnableDrawable>? components; | ||
|
||
public SkinEditorChangeHandler(Drawable targetScreen) | ||
{ | ||
// To keep things simple, we are currently only handling the current target screen for undo / redo. | ||
// In the future we'll want this to cover all changes, even to skin's `InstantiationInfo`. | ||
// We'll also need to consider cases where multiple targets are on screen at the same time. | ||
|
||
firstTarget = targetScreen.ChildrenOfType<ISkinnableTarget>().FirstOrDefault(); | ||
|
||
if (firstTarget == null) | ||
return; | ||
|
||
components = new BindableList<ISkinnableDrawable> { BindTarget = firstTarget.Components }; | ||
components.BindCollectionChanged((_, _) => SaveState()); | ||
} | ||
|
||
protected override void WriteCurrentStateToStream(MemoryStream stream) | ||
{ | ||
if (firstTarget == null) | ||
return; | ||
|
||
var skinnableInfos = firstTarget.CreateSkinnableInfo().ToArray(); | ||
string json = JsonConvert.SerializeObject(skinnableInfos, new JsonSerializerSettings { Formatting = Formatting.Indented }); | ||
stream.Write(Encoding.UTF8.GetBytes(json)); | ||
} | ||
|
||
protected override void ApplyStateChange(byte[] previousState, byte[] newState) | ||
{ | ||
if (firstTarget == null) | ||
return; | ||
|
||
var deserializedContent = JsonConvert.DeserializeObject<IEnumerable<SkinnableInfo>>(Encoding.UTF8.GetString(newState)); | ||
|
||
if (deserializedContent == null) | ||
return; | ||
|
||
SkinnableInfo[] skinnableInfo = deserializedContent.ToArray(); | ||
Drawable[] targetComponents = firstTarget.Components.OfType<Drawable>().ToArray(); | ||
|
||
if (!skinnableInfo.Select(s => s.Type).SequenceEqual(targetComponents.Select(d => d.GetType()))) | ||
{ | ||
// Perform a naive full reload for now. | ||
firstTarget.Reload(skinnableInfo); | ||
} | ||
else | ||
{ | ||
int i = 0; | ||
|
||
foreach (var drawable in targetComponents) | ||
drawable.ApplySkinnableInfo(skinnableInfo[i++]); | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.