diff --git a/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs b/osu.Game.Tests/Editing/BeatmapEditorChangeHandlerTest.cs similarity index 77% rename from osu.Game.Tests/Editing/EditorChangeHandlerTest.cs rename to osu.Game.Tests/Editing/BeatmapEditorChangeHandlerTest.cs index e1accd5b5fcc..80237fe6c8f5 100644 --- a/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs +++ b/osu.Game.Tests/Editing/BeatmapEditorChangeHandlerTest.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . 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,6 +85,10 @@ 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); @@ -61,7 +96,7 @@ public void TestApplyThenUndoThenApplySameChange() 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; @@ -69,7 +104,7 @@ public void TestApplyThenUndoThenApplySameChange() 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,12 +119,16 @@ 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; @@ -97,7 +136,7 @@ public void TestSaveSameStateDoesNotSave() 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,7 +145,7 @@ 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] @@ -114,11 +153,15 @@ 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); diff --git a/osu.Game/Extensions/DrawableExtensions.cs b/osu.Game/Extensions/DrawableExtensions.cs index 65b9e4676473..375960305cab 100644 --- a/osu.Game/Extensions/DrawableExtensions.cs +++ b/osu.Game/Extensions/DrawableExtensions.cs @@ -66,10 +66,16 @@ public static void ApplySkinnableInfo(this Drawable component, SkinnableInfo inf foreach (var (_, property) in component.GetSettingsSourceProperties()) { + var bindable = ((IBindable)property.GetValue(component)!); + if (!info.Settings.TryGetValue(property.Name.ToSnakeCase(), out object? settingValue)) + { + // TODO: We probably want to restore default if not included in serialisation information. + // This is not simple to do as SetDefault() is only found in the typed Bindable interface right now. continue; + } - skinnable.CopyAdjustedSetting(((IBindable)property.GetValue(component)!), settingValue); + skinnable.CopyAdjustedSetting(bindable, settingValue); } } diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index 83cea65542b8..866de7e62111 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -24,6 +24,7 @@ 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 @@ namespace osu.Game.Overlays.SkinEditor { [Cached(typeof(SkinEditor))] - public partial class SkinEditor : VisibilityContainer, ICanAcceptFiles, IKeyBindingHandler + public partial class SkinEditor : VisibilityContainer, ICanAcceptFiles, IKeyBindingHandler, IEditorChangeHandler { public const double TRANSITION_DURATION = 300; @@ -72,6 +73,11 @@ public partial class SkinEditor : VisibilityContainer, ICanAcceptFiles, IKeyBind 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 @@ private void load() 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 @@ public bool OnPressed(KeyBindingPressEvent e) { 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 @@ public void UpdateTargetScreen(Drawable targetScreen) { 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 @@ void loadBlueprintContainer() { 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) @@ -333,6 +361,10 @@ private void revert() } } + protected void Undo() => changeHandler?.RestoreState(-1); + + protected void Redo() => changeHandler?.RestoreState(1); + public void Save(bool userTriggered = true) { if (!hasBegunMutating) @@ -436,5 +468,27 @@ public SkinEditorToast(LocalisableString value, string skinDisplayName) { } } + + #region Delegation of IEditorChangeHandler + + public event Action? OnStateChange + { + add => throw new NotImplementedException(); + remove => throw new NotImplementedException(); + } + + private IEditorChangeHandler? beginChangeHandler; + + public void BeginChange() + { + // Change handler may change between begin and end, which can cause unbalanced operations. + // Let's track the one that was used when beginning the change so we can call EndChange on it specifically. + (beginChangeHandler = changeHandler)?.BeginChange(); + } + + public void EndChange() => beginChangeHandler?.EndChange(); + public void SaveState() => changeHandler?.SaveState(); + + #endregion } } diff --git a/osu.Game/Overlays/SkinEditor/SkinEditorChangeHandler.cs b/osu.Game/Overlays/SkinEditor/SkinEditorChangeHandler.cs new file mode 100644 index 000000000000..c8f66f3e56e9 --- /dev/null +++ b/osu.Game/Overlays/SkinEditor/SkinEditorChangeHandler.cs @@ -0,0 +1,78 @@ +// Copyright (c) ppy Pty Ltd . 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? 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().FirstOrDefault(); + + if (firstTarget == null) + return; + + components = new BindableList { 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>(Encoding.UTF8.GetString(newState)); + + if (deserializedContent == null) + return; + + SkinnableInfo[] skinnableInfo = deserializedContent.ToArray(); + Drawable[] targetComponents = firstTarget.Components.OfType().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++]); + } + } + } +} diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs index 129b9c1b44e1..86fcd35e0342 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs @@ -241,6 +241,8 @@ private static void updateDrawablePosition(Drawable drawable, Vector2 screenSpac private void applyOrigins(Anchor origin) { + OnOperationBegan(); + foreach (var item in SelectedItems) { var drawable = (Drawable)item; @@ -255,6 +257,8 @@ private void applyOrigins(Anchor origin) ApplyClosestAnchor(drawable); } + + OnOperationEnded(); } /// @@ -266,6 +270,8 @@ private Quad getSelectionQuad() => private void applyFixedAnchors(Anchor anchor) { + OnOperationBegan(); + foreach (var item in SelectedItems) { var drawable = (Drawable)item; @@ -273,15 +279,21 @@ private void applyFixedAnchors(Anchor anchor) item.UsesFixedAnchor = true; applyAnchor(drawable, anchor); } + + OnOperationEnded(); } private void applyClosestAnchors() { + OnOperationBegan(); + foreach (var item in SelectedItems) { item.UsesFixedAnchor = false; ApplyClosestAnchor((Drawable)item); } + + OnOperationEnded(); } private static Anchor getClosestAnchor(Drawable drawable) diff --git a/osu.Game/Overlays/SkinEditor/SkinSettingsToolbox.cs b/osu.Game/Overlays/SkinEditor/SkinSettingsToolbox.cs index 5a48dee973f3..a2b9db266592 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSettingsToolbox.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSettingsToolbox.cs @@ -2,10 +2,13 @@ // See the LICENCE file in the repository root for full licence text. using System.Linq; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Configuration; using osu.Game.Localisation; +using osu.Game.Overlays.Settings; +using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Components; using osuTK; @@ -13,19 +16,41 @@ namespace osu.Game.Overlays.SkinEditor { internal partial class SkinSettingsToolbox : EditorSidebarSection { + [Resolved] + private IEditorChangeHandler? changeHandler { get; set; } + protected override Container Content { get; } + private readonly Drawable component; + public SkinSettingsToolbox(Drawable component) : base(SkinEditorStrings.Settings(component.GetType().Name)) { + this.component = component; + base.Content.Add(Content = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Direction = FillDirection.Vertical, Spacing = new Vector2(10), - Children = component.CreateSettingsControls().ToArray() }); } + + [BackgroundDependencyLoader] + private void load() + { + var controls = component.CreateSettingsControls().ToArray(); + + Content.AddRange(controls); + + // track any changes to update undo states. + foreach (var c in controls.OfType()) + { + // TODO: SettingChanged is called too often for cases like SettingsTextBox and SettingsSlider. + // We will want to expose a SettingCommitted or similar to make this work better. + c.SettingChanged += () => changeHandler?.SaveState(); + } + } } } diff --git a/osu.Game/Screens/Edit/BeatmapEditorChangeHandler.cs b/osu.Game/Screens/Edit/BeatmapEditorChangeHandler.cs new file mode 100644 index 000000000000..3c19994a8a38 --- /dev/null +++ b/osu.Game/Screens/Edit/BeatmapEditorChangeHandler.cs @@ -0,0 +1,40 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.IO; +using System.Text; +using osu.Game.Beatmaps.Formats; +using osu.Game.Rulesets.Objects; + +namespace osu.Game.Screens.Edit +{ + public partial class BeatmapEditorChangeHandler : EditorChangeHandler + { + private readonly LegacyEditorBeatmapPatcher patcher; + private readonly EditorBeatmap editorBeatmap; + + /// + /// Creates a new . + /// + /// The to track the s of. + public BeatmapEditorChangeHandler(EditorBeatmap editorBeatmap) + { + this.editorBeatmap = editorBeatmap; + + editorBeatmap.TransactionBegan += BeginChange; + editorBeatmap.TransactionEnded += EndChange; + editorBeatmap.SaveStateTriggered += SaveState; + + patcher = new LegacyEditorBeatmapPatcher(editorBeatmap); + } + + protected override void WriteCurrentStateToStream(MemoryStream stream) + { + using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) + new LegacyBeatmapEncoder(editorBeatmap, editorBeatmap.BeatmapSkin).Encode(sw); + } + + protected override void ApplyStateChange(byte[] previousState, byte[] newState) => + patcher.Patch(previousState, newState); + } +} diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 74ea93325504..0622cbebaeca 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -240,7 +240,7 @@ private void load(OsuConfigManager config) if (canSave) { - changeHandler = new EditorChangeHandler(editorBeatmap); + changeHandler = new BeatmapEditorChangeHandler(editorBeatmap); dependencies.CacheAs(changeHandler); } diff --git a/osu.Game/Screens/Edit/EditorChangeHandler.cs b/osu.Game/Screens/Edit/EditorChangeHandler.cs index 964b86cad311..0bb17e4c5d78 100644 --- a/osu.Game/Screens/Edit/EditorChangeHandler.cs +++ b/osu.Game/Screens/Edit/EditorChangeHandler.cs @@ -1,31 +1,25 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Text; using osu.Framework.Bindables; using osu.Framework.Extensions; -using osu.Game.Beatmaps.Formats; -using osu.Game.Rulesets.Objects; namespace osu.Game.Screens.Edit { /// /// Tracks changes to the . /// - public partial class EditorChangeHandler : TransactionalCommitComponent, IEditorChangeHandler + public abstract partial class EditorChangeHandler : TransactionalCommitComponent, IEditorChangeHandler { public readonly Bindable CanUndo = new Bindable(); public readonly Bindable CanRedo = new Bindable(); - public event Action OnStateChange; + public event Action? OnStateChange; - private readonly LegacyEditorBeatmapPatcher patcher; private readonly List savedStates = new List(); private int currentState = -1; @@ -37,32 +31,28 @@ public string CurrentStateHash { get { + ensureStateSaved(); + using (var stream = new MemoryStream(savedStates[currentState])) return stream.ComputeSHA2Hash(); } } - private readonly EditorBeatmap editorBeatmap; private bool isRestoring; public const int MAX_SAVED_STATES = 50; - /// - /// Creates a new . - /// - /// The to track the s of. - public EditorChangeHandler(EditorBeatmap editorBeatmap) + public override void BeginChange() { - this.editorBeatmap = editorBeatmap; + ensureStateSaved(); - editorBeatmap.TransactionBegan += BeginChange; - editorBeatmap.TransactionEnded += EndChange; - editorBeatmap.SaveStateTriggered += SaveState; - - patcher = new LegacyEditorBeatmapPatcher(editorBeatmap); + base.BeginChange(); + } - // Initial state. - SaveState(); + private void ensureStateSaved() + { + if (savedStates.Count == 0) + SaveState(); } protected override void UpdateState() @@ -72,9 +62,7 @@ protected override void UpdateState() using (var stream = new MemoryStream()) { - using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) - new LegacyBeatmapEncoder(editorBeatmap, editorBeatmap.BeatmapSkin).Encode(sw); - + WriteCurrentStateToStream(stream); byte[] newState = stream.ToArray(); // if the previous state is binary equal we don't need to push a new one, unless this is the initial state. @@ -113,7 +101,8 @@ public void RestoreState(int direction) isRestoring = true; - patcher.Patch(savedStates[currentState], savedStates[newState]); + ApplyStateChange(savedStates[currentState], savedStates[newState]); + currentState = newState; isRestoring = false; @@ -122,6 +111,20 @@ public void RestoreState(int direction) updateBindables(); } + /// + /// Write a serialised copy of the currently tracked state to the provided stream. + /// This will be stored as a state which can be restored in the future. + /// + /// The stream which the state should be written to. + protected abstract void WriteCurrentStateToStream(MemoryStream stream); + + /// + /// Given a previous and new state, apply any changes required to bring the current state in line with the new state. + /// + /// The previous (current before this call) serialised state. + /// The new state to be applied. + protected abstract void ApplyStateChange(byte[] previousState, byte[] newState); + private void updateBindables() { CanUndo.Value = savedStates.Count > 0 && currentState > 0; diff --git a/osu.Game/Screens/Edit/IEditorChangeHandler.cs b/osu.Game/Screens/Edit/IEditorChangeHandler.cs index e7abc1c43dac..9fe40ba1b12d 100644 --- a/osu.Game/Screens/Edit/IEditorChangeHandler.cs +++ b/osu.Game/Screens/Edit/IEditorChangeHandler.cs @@ -1,9 +1,8 @@ // Copyright (c) ppy Pty Ltd . 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 /// /// Interface for a component that manages changes in the . /// + [Cached] public interface IEditorChangeHandler { /// /// Fired whenever a state change occurs. /// - event Action OnStateChange; + event Action? OnStateChange; /// /// Begins a bulk state change event. should be invoked soon after. diff --git a/osu.Game/Screens/Edit/TransactionalCommitComponent.cs b/osu.Game/Screens/Edit/TransactionalCommitComponent.cs index 55c9cf86c362..92f1e19e6fc6 100644 --- a/osu.Game/Screens/Edit/TransactionalCommitComponent.cs +++ b/osu.Game/Screens/Edit/TransactionalCommitComponent.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Graphics; @@ -16,17 +14,17 @@ public abstract partial class TransactionalCommitComponent : Component /// /// Fires whenever a transaction begins. Will not fire on nested transactions. /// - public event Action TransactionBegan; + public event Action? TransactionBegan; /// /// Fires when the last transaction completes. /// - public event Action TransactionEnded; + public event Action? TransactionEnded; /// /// Fires when is called and results in a non-transactional state save. /// - public event Action SaveStateTriggered; + public event Action? SaveStateTriggered; public bool TransactionActive => bulkChangesStarted > 0; @@ -35,7 +33,7 @@ public abstract partial class TransactionalCommitComponent : Component /// /// Signal the beginning of a change. /// - public void BeginChange() + public virtual void BeginChange() { if (bulkChangesStarted++ == 0) TransactionBegan?.Invoke(); diff --git a/osu.Game/Screens/Play/HUD/SkinnableInfo.cs b/osu.Game/Screens/Play/HUD/SkinnableInfo.cs index da759b432997..9fdae506159a 100644 --- a/osu.Game/Screens/Play/HUD/SkinnableInfo.cs +++ b/osu.Game/Screens/Play/HUD/SkinnableInfo.cs @@ -69,8 +69,7 @@ public SkinnableInfo(Drawable component) { var bindable = (IBindable)property.GetValue(component)!; - if (!bindable.IsDefault) - Settings.Add(property.Name.ToSnakeCase(), bindable.GetUnderlyingSettingValue()); + Settings.Add(property.Name.ToSnakeCase(), bindable.GetUnderlyingSettingValue()); } if (component is Container container) diff --git a/osu.Game/Skinning/ISkinnableTarget.cs b/osu.Game/Skinning/ISkinnableTarget.cs index 57c78bfe1cb9..3f116f8f76ee 100644 --- a/osu.Game/Skinning/ISkinnableTarget.cs +++ b/osu.Game/Skinning/ISkinnableTarget.cs @@ -36,6 +36,11 @@ public interface ISkinnableTarget : IDrawable /// void Reload(); + /// + /// Reload this target from the provided skinnable information. + /// + void Reload(SkinnableInfo[] skinnableInfo); + /// /// Add a new skinnable component to this target. /// @@ -46,6 +51,6 @@ public interface ISkinnableTarget : IDrawable /// Remove an existing skinnable component from this target. /// /// The component to remove. - public void Remove(ISkinnableDrawable component); + void Remove(ISkinnableDrawable component); } } diff --git a/osu.Game/Skinning/SkinnableTargetContainer.cs b/osu.Game/Skinning/SkinnableTargetContainer.cs index 794a12da8267..df5299d42757 100644 --- a/osu.Game/Skinning/SkinnableTargetContainer.cs +++ b/osu.Game/Skinning/SkinnableTargetContainer.cs @@ -2,10 +2,12 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Linq; using System.Threading; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Game.Screens.Play.HUD; namespace osu.Game.Skinning { @@ -30,16 +32,31 @@ public SkinnableTargetContainer(GlobalSkinComponentLookup.LookupType target) Target = target; } - /// - /// Reload all components in this container from the current skin. - /// - public void Reload() + public void Reload(SkinnableInfo[] skinnableInfo) + { + var drawables = new List(); + + foreach (var i in skinnableInfo) + drawables.Add(i.CreateInstance()); + + Reload(new SkinnableTargetComponentsContainer + { + Children = drawables, + }); + } + + public void Reload() => Reload(CurrentSkin.GetDrawableComponent(new GlobalSkinComponentLookup(Target)) as SkinnableTargetComponentsContainer); + + public void Reload(SkinnableTargetComponentsContainer? componentsContainer) { ClearInternal(); components.Clear(); ComponentsLoaded = false; - content = CurrentSkin.GetDrawableComponent(new GlobalSkinComponentLookup(Target)) as SkinnableTargetComponentsContainer; + if (componentsContainer == null) + return; + + content = componentsContainer; cancellationSource?.Cancel(); cancellationSource = null;