1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-14 07:42:57 +08:00

Add a basic change handler to the skin editor

This commit is contained in:
Dean Herbert 2023-02-03 18:53:22 +09:00
parent 2f30306ea2
commit 2cda277c09
3 changed files with 127 additions and 4 deletions

View File

@ -24,6 +24,7 @@ using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
using osu.Game.Overlays.OSD;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Components;
using osu.Game.Screens.Edit.Components.Menus;
using osu.Game.Skinning;
@ -31,7 +32,7 @@ using osu.Game.Skinning;
namespace osu.Game.Overlays.SkinEditor
{
[Cached(typeof(SkinEditor))]
public partial class SkinEditor : VisibilityContainer, ICanAcceptFiles, IKeyBindingHandler<PlatformAction>
public partial class SkinEditor : VisibilityContainer, ICanAcceptFiles, IKeyBindingHandler<PlatformAction>, IEditorChangeHandler
{
public const double TRANSITION_DURATION = 300;
@ -72,6 +73,11 @@ namespace osu.Game.Overlays.SkinEditor
private EditorSidebar componentsSidebar = null!;
private EditorSidebar settingsSidebar = null!;
private SkinEditorChangeHandler? changeHandler;
private EditorMenuItem undoMenuItem = null!;
private EditorMenuItem redoMenuItem = null!;
[Resolved]
private OnScreenDisplay? onScreenDisplay { get; set; }
@ -131,6 +137,14 @@ namespace osu.Game.Overlays.SkinEditor
new EditorMenuItem(CommonStrings.Exit, MenuItemType.Standard, () => skinEditorOverlay?.Hide()),
},
},
new MenuItem(CommonStrings.MenuBarEdit)
{
Items = new[]
{
undoMenuItem = new EditorMenuItem(CommonStrings.Undo, MenuItemType.Standard, Undo),
redoMenuItem = new EditorMenuItem(CommonStrings.Redo, MenuItemType.Standard, Redo),
}
},
}
},
headerText = new OsuTextFlowContainer
@ -210,6 +224,14 @@ namespace osu.Game.Overlays.SkinEditor
{
switch (e.Action)
{
case PlatformAction.Undo:
Undo();
return true;
case PlatformAction.Redo:
Redo();
return true;
case PlatformAction.Save:
if (e.Repeat)
return false;
@ -229,6 +251,8 @@ namespace osu.Game.Overlays.SkinEditor
{
this.targetScreen = targetScreen;
changeHandler?.Dispose();
SelectedComponents.Clear();
// Immediately clear the previous blueprint container to ensure it doesn't try to interact with the old target.
@ -241,6 +265,10 @@ namespace osu.Game.Overlays.SkinEditor
{
Debug.Assert(content != null);
changeHandler = new SkinEditorChangeHandler(targetScreen);
changeHandler.CanUndo.BindValueChanged(v => undoMenuItem.Action.Disabled = !v.NewValue, true);
changeHandler.CanRedo.BindValueChanged(v => redoMenuItem.Action.Disabled = !v.NewValue, true);
content.Child = new SkinBlueprintContainer(targetScreen);
componentsSidebar.Child = new SkinComponentToolbox(getFirstTarget() as CompositeDrawable)
@ -301,6 +329,8 @@ namespace osu.Game.Overlays.SkinEditor
SelectedComponents.Clear();
SelectedComponents.Add(component);
changeHandler?.SaveState();
}
private void populateSettings()
@ -333,6 +363,10 @@ namespace osu.Game.Overlays.SkinEditor
}
}
protected void Undo() => changeHandler?.RestoreState(-1);
protected void Redo() => changeHandler?.RestoreState(1);
public void Save()
{
if (!hasBegunMutating)
@ -371,6 +405,8 @@ namespace osu.Game.Overlays.SkinEditor
{
foreach (var item in items)
availableTargets.FirstOrDefault(t => t.Components.Contains(item))?.Remove(item);
changeHandler?.SaveState();
}
#region Drag & drop import handling
@ -435,5 +471,19 @@ namespace osu.Game.Overlays.SkinEditor
{
}
}
#region Delegation of IEditorChangeHandler
public event Action? OnStateChange
{
add => changeHandler!.OnStateChange += value;
remove => changeHandler!.OnStateChange -= value;
}
public void BeginChange() => changeHandler?.BeginChange();
public void EndChange() => changeHandler?.EndChange();
public void SaveState() => changeHandler?.SaveState();
#endregion
}
}

View File

@ -0,0 +1,73 @@
// 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 System.IO;
using System.Linq;
using System.Text;
using Newtonsoft.Json;
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 Drawable targetScreen;
private ISkinnableTarget? firstTarget => targetScreen.ChildrenOfType<ISkinnableTarget>().FirstOrDefault();
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.
this.targetScreen = targetScreen;
// Save initial state.
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++]);
}
}
}
}

View File

@ -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 System;
using osu.Framework.Allocation;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Screens.Edit
@ -11,12 +10,13 @@ namespace osu.Game.Screens.Edit
/// <summary>
/// Interface for a component that manages changes in the <see cref="Editor"/>.
/// </summary>
[Cached]
public interface IEditorChangeHandler
{
/// <summary>
/// Fired whenever a state change occurs.
/// </summary>
event Action OnStateChange;
event Action? OnStateChange;
/// <summary>
/// Begins a bulk state change event. <see cref="EndChange"/> should be invoked soon after.