mirror of
https://github.com/ppy/osu.git
synced 2024-11-11 11:37:28 +08:00
Merge pull request #22506 from peppy/skin-editor-undo-support
Add very basic skin editor undo / redo support
This commit is contained in:
commit
2873905cc6
@ -1,8 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Game.Rulesets.Osu;
|
using osu.Game.Rulesets.Osu;
|
||||||
using osu.Game.Rulesets.Osu.Beatmaps;
|
using osu.Game.Rulesets.Osu.Beatmaps;
|
||||||
@ -12,7 +10,7 @@ using osu.Game.Screens.Edit;
|
|||||||
namespace osu.Game.Tests.Editing
|
namespace osu.Game.Tests.Editing
|
||||||
{
|
{
|
||||||
[TestFixture]
|
[TestFixture]
|
||||||
public class EditorChangeHandlerTest
|
public class BeatmapEditorChangeHandlerTest
|
||||||
{
|
{
|
||||||
private int stateChangedFired;
|
private int stateChangedFired;
|
||||||
|
|
||||||
@ -23,18 +21,23 @@ namespace osu.Game.Tests.Editing
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestSaveRestoreState()
|
public void TestSaveRestoreStateUsingTransaction()
|
||||||
{
|
{
|
||||||
var (handler, beatmap) = createChangeHandler();
|
var (handler, beatmap) = createChangeHandler();
|
||||||
|
|
||||||
Assert.That(handler.CanUndo.Value, Is.False);
|
Assert.That(handler.CanUndo.Value, Is.False);
|
||||||
Assert.That(handler.CanRedo.Value, Is.False);
|
Assert.That(handler.CanRedo.Value, Is.False);
|
||||||
|
|
||||||
addArbitraryChange(beatmap);
|
handler.BeginChange();
|
||||||
handler.SaveState();
|
|
||||||
|
|
||||||
|
// Initial state will be saved on BeginChange
|
||||||
Assert.That(stateChangedFired, Is.EqualTo(1));
|
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.CanUndo.Value, Is.True);
|
||||||
Assert.That(handler.CanRedo.Value, Is.False);
|
Assert.That(handler.CanRedo.Value, Is.False);
|
||||||
|
|
||||||
@ -43,7 +46,35 @@ namespace osu.Game.Tests.Editing
|
|||||||
Assert.That(handler.CanUndo.Value, Is.False);
|
Assert.That(handler.CanUndo.Value, Is.False);
|
||||||
Assert.That(handler.CanRedo.Value, Is.True);
|
Assert.That(handler.CanRedo.Value, Is.True);
|
||||||
|
|
||||||
|
Assert.That(stateChangedFired, Is.EqualTo(3));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSaveRestoreState()
|
||||||
|
{
|
||||||
|
var (handler, beatmap) = createChangeHandler();
|
||||||
|
|
||||||
|
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(2));
|
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]
|
[Test]
|
||||||
@ -54,6 +85,10 @@ namespace osu.Game.Tests.Editing
|
|||||||
Assert.That(handler.CanUndo.Value, Is.False);
|
Assert.That(handler.CanUndo.Value, Is.False);
|
||||||
Assert.That(handler.CanRedo.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;
|
string originalHash = handler.CurrentStateHash;
|
||||||
|
|
||||||
addArbitraryChange(beatmap);
|
addArbitraryChange(beatmap);
|
||||||
@ -61,7 +96,7 @@ namespace osu.Game.Tests.Editing
|
|||||||
|
|
||||||
Assert.That(handler.CanUndo.Value, Is.True);
|
Assert.That(handler.CanUndo.Value, Is.True);
|
||||||
Assert.That(handler.CanRedo.Value, Is.False);
|
Assert.That(handler.CanRedo.Value, Is.False);
|
||||||
Assert.That(stateChangedFired, Is.EqualTo(1));
|
Assert.That(stateChangedFired, Is.EqualTo(2));
|
||||||
|
|
||||||
string hash = handler.CurrentStateHash;
|
string hash = handler.CurrentStateHash;
|
||||||
|
|
||||||
@ -69,7 +104,7 @@ namespace osu.Game.Tests.Editing
|
|||||||
handler.RestoreState(-1);
|
handler.RestoreState(-1);
|
||||||
|
|
||||||
Assert.That(originalHash, Is.EqualTo(handler.CurrentStateHash));
|
Assert.That(originalHash, Is.EqualTo(handler.CurrentStateHash));
|
||||||
Assert.That(stateChangedFired, Is.EqualTo(2));
|
Assert.That(stateChangedFired, Is.EqualTo(3));
|
||||||
|
|
||||||
addArbitraryChange(beatmap);
|
addArbitraryChange(beatmap);
|
||||||
handler.SaveState();
|
handler.SaveState();
|
||||||
@ -84,12 +119,16 @@ namespace osu.Game.Tests.Editing
|
|||||||
Assert.That(handler.CanUndo.Value, Is.False);
|
Assert.That(handler.CanUndo.Value, Is.False);
|
||||||
Assert.That(handler.CanRedo.Value, Is.False);
|
Assert.That(handler.CanRedo.Value, Is.False);
|
||||||
|
|
||||||
|
// Save initial state
|
||||||
|
handler.SaveState();
|
||||||
|
Assert.That(stateChangedFired, Is.EqualTo(1));
|
||||||
|
|
||||||
addArbitraryChange(beatmap);
|
addArbitraryChange(beatmap);
|
||||||
handler.SaveState();
|
handler.SaveState();
|
||||||
|
|
||||||
Assert.That(handler.CanUndo.Value, Is.True);
|
Assert.That(handler.CanUndo.Value, Is.True);
|
||||||
Assert.That(handler.CanRedo.Value, Is.False);
|
Assert.That(handler.CanRedo.Value, Is.False);
|
||||||
Assert.That(stateChangedFired, Is.EqualTo(1));
|
Assert.That(stateChangedFired, Is.EqualTo(2));
|
||||||
|
|
||||||
string hash = handler.CurrentStateHash;
|
string hash = handler.CurrentStateHash;
|
||||||
|
|
||||||
@ -97,7 +136,7 @@ namespace osu.Game.Tests.Editing
|
|||||||
handler.SaveState();
|
handler.SaveState();
|
||||||
|
|
||||||
Assert.That(hash, Is.EqualTo(handler.CurrentStateHash));
|
Assert.That(hash, Is.EqualTo(handler.CurrentStateHash));
|
||||||
Assert.That(stateChangedFired, Is.EqualTo(1));
|
Assert.That(stateChangedFired, Is.EqualTo(2));
|
||||||
|
|
||||||
handler.RestoreState(-1);
|
handler.RestoreState(-1);
|
||||||
|
|
||||||
@ -106,7 +145,7 @@ namespace osu.Game.Tests.Editing
|
|||||||
// we should only be able to restore once even though we saved twice.
|
// we should only be able to restore once even though we saved twice.
|
||||||
Assert.That(handler.CanUndo.Value, Is.False);
|
Assert.That(handler.CanUndo.Value, Is.False);
|
||||||
Assert.That(handler.CanRedo.Value, Is.True);
|
Assert.That(handler.CanRedo.Value, Is.True);
|
||||||
Assert.That(stateChangedFired, Is.EqualTo(2));
|
Assert.That(stateChangedFired, Is.EqualTo(3));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -114,11 +153,15 @@ namespace osu.Game.Tests.Editing
|
|||||||
{
|
{
|
||||||
var (handler, beatmap) = createChangeHandler();
|
var (handler, beatmap) = createChangeHandler();
|
||||||
|
|
||||||
|
// Save initial state
|
||||||
|
handler.SaveState();
|
||||||
|
Assert.That(stateChangedFired, Is.EqualTo(1));
|
||||||
|
|
||||||
Assert.That(handler.CanUndo.Value, Is.False);
|
Assert.That(handler.CanUndo.Value, Is.False);
|
||||||
|
|
||||||
for (int i = 0; i < EditorChangeHandler.MAX_SAVED_STATES; i++)
|
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);
|
addArbitraryChange(beatmap);
|
||||||
handler.SaveState();
|
handler.SaveState();
|
||||||
@ -169,7 +212,7 @@ namespace osu.Game.Tests.Editing
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
var changeHandler = new EditorChangeHandler(beatmap);
|
var changeHandler = new BeatmapEditorChangeHandler(beatmap);
|
||||||
|
|
||||||
changeHandler.OnStateChange += () => stateChangedFired++;
|
changeHandler.OnStateChange += () => stateChangedFired++;
|
||||||
return (changeHandler, beatmap);
|
return (changeHandler, beatmap);
|
@ -66,10 +66,16 @@ namespace osu.Game.Extensions
|
|||||||
|
|
||||||
foreach (var (_, property) in component.GetSettingsSourceProperties())
|
foreach (var (_, property) in component.GetSettingsSourceProperties())
|
||||||
{
|
{
|
||||||
if (!info.Settings.TryGetValue(property.Name.ToSnakeCase(), out object? settingValue))
|
var bindable = ((IBindable)property.GetValue(component)!);
|
||||||
continue;
|
|
||||||
|
|
||||||
skinnable.CopyAdjustedSetting(((IBindable)property.GetValue(component)!), settingValue);
|
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<T> interface right now.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
skinnable.CopyAdjustedSetting(bindable, settingValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,6 +24,7 @@ using osu.Game.Graphics.Cursor;
|
|||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Localisation;
|
using osu.Game.Localisation;
|
||||||
using osu.Game.Overlays.OSD;
|
using osu.Game.Overlays.OSD;
|
||||||
|
using osu.Game.Screens.Edit;
|
||||||
using osu.Game.Screens.Edit.Components;
|
using osu.Game.Screens.Edit.Components;
|
||||||
using osu.Game.Screens.Edit.Components.Menus;
|
using osu.Game.Screens.Edit.Components.Menus;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
@ -31,7 +32,7 @@ using osu.Game.Skinning;
|
|||||||
namespace osu.Game.Overlays.SkinEditor
|
namespace osu.Game.Overlays.SkinEditor
|
||||||
{
|
{
|
||||||
[Cached(typeof(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;
|
public const double TRANSITION_DURATION = 300;
|
||||||
|
|
||||||
@ -72,6 +73,11 @@ namespace osu.Game.Overlays.SkinEditor
|
|||||||
private EditorSidebar componentsSidebar = null!;
|
private EditorSidebar componentsSidebar = null!;
|
||||||
private EditorSidebar settingsSidebar = null!;
|
private EditorSidebar settingsSidebar = null!;
|
||||||
|
|
||||||
|
private SkinEditorChangeHandler? changeHandler;
|
||||||
|
|
||||||
|
private EditorMenuItem undoMenuItem = null!;
|
||||||
|
private EditorMenuItem redoMenuItem = null!;
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private OnScreenDisplay? onScreenDisplay { get; set; }
|
private OnScreenDisplay? onScreenDisplay { get; set; }
|
||||||
|
|
||||||
@ -131,6 +137,14 @@ namespace osu.Game.Overlays.SkinEditor
|
|||||||
new EditorMenuItem(CommonStrings.Exit, MenuItemType.Standard, () => skinEditorOverlay?.Hide()),
|
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
|
headerText = new OsuTextFlowContainer
|
||||||
@ -210,6 +224,14 @@ namespace osu.Game.Overlays.SkinEditor
|
|||||||
{
|
{
|
||||||
switch (e.Action)
|
switch (e.Action)
|
||||||
{
|
{
|
||||||
|
case PlatformAction.Undo:
|
||||||
|
Undo();
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case PlatformAction.Redo:
|
||||||
|
Redo();
|
||||||
|
return true;
|
||||||
|
|
||||||
case PlatformAction.Save:
|
case PlatformAction.Save:
|
||||||
if (e.Repeat)
|
if (e.Repeat)
|
||||||
return false;
|
return false;
|
||||||
@ -229,6 +251,8 @@ namespace osu.Game.Overlays.SkinEditor
|
|||||||
{
|
{
|
||||||
this.targetScreen = targetScreen;
|
this.targetScreen = targetScreen;
|
||||||
|
|
||||||
|
changeHandler?.Dispose();
|
||||||
|
|
||||||
SelectedComponents.Clear();
|
SelectedComponents.Clear();
|
||||||
|
|
||||||
// Immediately clear the previous blueprint container to ensure it doesn't try to interact with the old target.
|
// 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);
|
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);
|
content.Child = new SkinBlueprintContainer(targetScreen);
|
||||||
|
|
||||||
componentsSidebar.Child = new SkinComponentToolbox(getFirstTarget() as CompositeDrawable)
|
componentsSidebar.Child = new SkinComponentToolbox(getFirstTarget() as CompositeDrawable)
|
||||||
@ -333,6 +361,10 @@ namespace osu.Game.Overlays.SkinEditor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected void Undo() => changeHandler?.RestoreState(-1);
|
||||||
|
|
||||||
|
protected void Redo() => changeHandler?.RestoreState(1);
|
||||||
|
|
||||||
public void Save(bool userTriggered = true)
|
public void Save(bool userTriggered = true)
|
||||||
{
|
{
|
||||||
if (!hasBegunMutating)
|
if (!hasBegunMutating)
|
||||||
@ -436,5 +468,27 @@ namespace osu.Game.Overlays.SkinEditor
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
78
osu.Game/Overlays/SkinEditor/SkinEditorChangeHandler.cs
Normal file
78
osu.Game/Overlays/SkinEditor/SkinEditorChangeHandler.cs
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
// 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.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++]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -241,6 +241,8 @@ namespace osu.Game.Overlays.SkinEditor
|
|||||||
|
|
||||||
private void applyOrigins(Anchor origin)
|
private void applyOrigins(Anchor origin)
|
||||||
{
|
{
|
||||||
|
OnOperationBegan();
|
||||||
|
|
||||||
foreach (var item in SelectedItems)
|
foreach (var item in SelectedItems)
|
||||||
{
|
{
|
||||||
var drawable = (Drawable)item;
|
var drawable = (Drawable)item;
|
||||||
@ -255,6 +257,8 @@ namespace osu.Game.Overlays.SkinEditor
|
|||||||
|
|
||||||
ApplyClosestAnchor(drawable);
|
ApplyClosestAnchor(drawable);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
OnOperationEnded();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -266,6 +270,8 @@ namespace osu.Game.Overlays.SkinEditor
|
|||||||
|
|
||||||
private void applyFixedAnchors(Anchor anchor)
|
private void applyFixedAnchors(Anchor anchor)
|
||||||
{
|
{
|
||||||
|
OnOperationBegan();
|
||||||
|
|
||||||
foreach (var item in SelectedItems)
|
foreach (var item in SelectedItems)
|
||||||
{
|
{
|
||||||
var drawable = (Drawable)item;
|
var drawable = (Drawable)item;
|
||||||
@ -273,15 +279,21 @@ namespace osu.Game.Overlays.SkinEditor
|
|||||||
item.UsesFixedAnchor = true;
|
item.UsesFixedAnchor = true;
|
||||||
applyAnchor(drawable, anchor);
|
applyAnchor(drawable, anchor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
OnOperationEnded();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void applyClosestAnchors()
|
private void applyClosestAnchors()
|
||||||
{
|
{
|
||||||
|
OnOperationBegan();
|
||||||
|
|
||||||
foreach (var item in SelectedItems)
|
foreach (var item in SelectedItems)
|
||||||
{
|
{
|
||||||
item.UsesFixedAnchor = false;
|
item.UsesFixedAnchor = false;
|
||||||
ApplyClosestAnchor((Drawable)item);
|
ApplyClosestAnchor((Drawable)item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
OnOperationEnded();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Anchor getClosestAnchor(Drawable drawable)
|
private static Anchor getClosestAnchor(Drawable drawable)
|
||||||
|
@ -2,10 +2,13 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Localisation;
|
using osu.Game.Localisation;
|
||||||
|
using osu.Game.Overlays.Settings;
|
||||||
|
using osu.Game.Screens.Edit;
|
||||||
using osu.Game.Screens.Edit.Components;
|
using osu.Game.Screens.Edit.Components;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
@ -13,19 +16,41 @@ namespace osu.Game.Overlays.SkinEditor
|
|||||||
{
|
{
|
||||||
internal partial class SkinSettingsToolbox : EditorSidebarSection
|
internal partial class SkinSettingsToolbox : EditorSidebarSection
|
||||||
{
|
{
|
||||||
|
[Resolved]
|
||||||
|
private IEditorChangeHandler? changeHandler { get; set; }
|
||||||
|
|
||||||
protected override Container<Drawable> Content { get; }
|
protected override Container<Drawable> Content { get; }
|
||||||
|
|
||||||
|
private readonly Drawable component;
|
||||||
|
|
||||||
public SkinSettingsToolbox(Drawable component)
|
public SkinSettingsToolbox(Drawable component)
|
||||||
: base(SkinEditorStrings.Settings(component.GetType().Name))
|
: base(SkinEditorStrings.Settings(component.GetType().Name))
|
||||||
{
|
{
|
||||||
|
this.component = component;
|
||||||
|
|
||||||
base.Content.Add(Content = new FillFlowContainer
|
base.Content.Add(Content = new FillFlowContainer
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
AutoSizeAxes = Axes.Y,
|
AutoSizeAxes = Axes.Y,
|
||||||
Direction = FillDirection.Vertical,
|
Direction = FillDirection.Vertical,
|
||||||
Spacing = new Vector2(10),
|
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<ISettingsItem>())
|
||||||
|
{
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
40
osu.Game/Screens/Edit/BeatmapEditorChangeHandler.cs
Normal file
40
osu.Game/Screens/Edit/BeatmapEditorChangeHandler.cs
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
// 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.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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new <see cref="EditorChangeHandler"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="editorBeatmap">The <see cref="EditorBeatmap"/> to track the <see cref="HitObject"/>s of.</param>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
@ -240,7 +240,7 @@ namespace osu.Game.Screens.Edit
|
|||||||
|
|
||||||
if (canSave)
|
if (canSave)
|
||||||
{
|
{
|
||||||
changeHandler = new EditorChangeHandler(editorBeatmap);
|
changeHandler = new BeatmapEditorChangeHandler(editorBeatmap);
|
||||||
dependencies.CacheAs<IEditorChangeHandler>(changeHandler);
|
dependencies.CacheAs<IEditorChangeHandler>(changeHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,31 +1,25 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Extensions;
|
using osu.Framework.Extensions;
|
||||||
using osu.Game.Beatmaps.Formats;
|
|
||||||
using osu.Game.Rulesets.Objects;
|
|
||||||
|
|
||||||
namespace osu.Game.Screens.Edit
|
namespace osu.Game.Screens.Edit
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Tracks changes to the <see cref="Editor"/>.
|
/// Tracks changes to the <see cref="Editor"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class EditorChangeHandler : TransactionalCommitComponent, IEditorChangeHandler
|
public abstract partial class EditorChangeHandler : TransactionalCommitComponent, IEditorChangeHandler
|
||||||
{
|
{
|
||||||
public readonly Bindable<bool> CanUndo = new Bindable<bool>();
|
public readonly Bindable<bool> CanUndo = new Bindable<bool>();
|
||||||
public readonly Bindable<bool> CanRedo = new Bindable<bool>();
|
public readonly Bindable<bool> CanRedo = new Bindable<bool>();
|
||||||
|
|
||||||
public event Action OnStateChange;
|
public event Action? OnStateChange;
|
||||||
|
|
||||||
private readonly LegacyEditorBeatmapPatcher patcher;
|
|
||||||
private readonly List<byte[]> savedStates = new List<byte[]>();
|
private readonly List<byte[]> savedStates = new List<byte[]>();
|
||||||
|
|
||||||
private int currentState = -1;
|
private int currentState = -1;
|
||||||
@ -37,32 +31,28 @@ namespace osu.Game.Screens.Edit
|
|||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
|
ensureStateSaved();
|
||||||
|
|
||||||
using (var stream = new MemoryStream(savedStates[currentState]))
|
using (var stream = new MemoryStream(savedStates[currentState]))
|
||||||
return stream.ComputeSHA2Hash();
|
return stream.ComputeSHA2Hash();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly EditorBeatmap editorBeatmap;
|
|
||||||
private bool isRestoring;
|
private bool isRestoring;
|
||||||
|
|
||||||
public const int MAX_SAVED_STATES = 50;
|
public const int MAX_SAVED_STATES = 50;
|
||||||
|
|
||||||
/// <summary>
|
public override void BeginChange()
|
||||||
/// Creates a new <see cref="EditorChangeHandler"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="editorBeatmap">The <see cref="EditorBeatmap"/> to track the <see cref="HitObject"/>s of.</param>
|
|
||||||
public EditorChangeHandler(EditorBeatmap editorBeatmap)
|
|
||||||
{
|
{
|
||||||
this.editorBeatmap = editorBeatmap;
|
ensureStateSaved();
|
||||||
|
|
||||||
editorBeatmap.TransactionBegan += BeginChange;
|
base.BeginChange();
|
||||||
editorBeatmap.TransactionEnded += EndChange;
|
}
|
||||||
editorBeatmap.SaveStateTriggered += SaveState;
|
|
||||||
|
|
||||||
patcher = new LegacyEditorBeatmapPatcher(editorBeatmap);
|
private void ensureStateSaved()
|
||||||
|
{
|
||||||
// Initial state.
|
if (savedStates.Count == 0)
|
||||||
SaveState();
|
SaveState();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void UpdateState()
|
protected override void UpdateState()
|
||||||
@ -72,9 +62,7 @@ namespace osu.Game.Screens.Edit
|
|||||||
|
|
||||||
using (var stream = new MemoryStream())
|
using (var stream = new MemoryStream())
|
||||||
{
|
{
|
||||||
using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true))
|
WriteCurrentStateToStream(stream);
|
||||||
new LegacyBeatmapEncoder(editorBeatmap, editorBeatmap.BeatmapSkin).Encode(sw);
|
|
||||||
|
|
||||||
byte[] newState = stream.ToArray();
|
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.
|
// 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 @@ namespace osu.Game.Screens.Edit
|
|||||||
|
|
||||||
isRestoring = true;
|
isRestoring = true;
|
||||||
|
|
||||||
patcher.Patch(savedStates[currentState], savedStates[newState]);
|
ApplyStateChange(savedStates[currentState], savedStates[newState]);
|
||||||
|
|
||||||
currentState = newState;
|
currentState = newState;
|
||||||
|
|
||||||
isRestoring = false;
|
isRestoring = false;
|
||||||
@ -122,6 +111,20 @@ namespace osu.Game.Screens.Edit
|
|||||||
updateBindables();
|
updateBindables();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="stream">The stream which the state should be written to.</param>
|
||||||
|
protected abstract void WriteCurrentStateToStream(MemoryStream stream);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Given a previous and new state, apply any changes required to bring the current state in line with the new state.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="previousState">The previous (current before this call) serialised state.</param>
|
||||||
|
/// <param name="newState">The new state to be applied.</param>
|
||||||
|
protected abstract void ApplyStateChange(byte[] previousState, byte[] newState);
|
||||||
|
|
||||||
private void updateBindables()
|
private void updateBindables()
|
||||||
{
|
{
|
||||||
CanUndo.Value = savedStates.Count > 0 && currentState > 0;
|
CanUndo.Value = savedStates.Count > 0 && currentState > 0;
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
|
|
||||||
namespace osu.Game.Screens.Edit
|
namespace osu.Game.Screens.Edit
|
||||||
@ -11,12 +10,13 @@ namespace osu.Game.Screens.Edit
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Interface for a component that manages changes in the <see cref="Editor"/>.
|
/// Interface for a component that manages changes in the <see cref="Editor"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[Cached]
|
||||||
public interface IEditorChangeHandler
|
public interface IEditorChangeHandler
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Fired whenever a state change occurs.
|
/// Fired whenever a state change occurs.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
event Action OnStateChange;
|
event Action? OnStateChange;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Begins a bulk state change event. <see cref="EndChange"/> should be invoked soon after.
|
/// Begins a bulk state change event. <see cref="EndChange"/> should be invoked soon after.
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
|
|
||||||
@ -16,17 +14,17 @@ namespace osu.Game.Screens.Edit
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Fires whenever a transaction begins. Will not fire on nested transactions.
|
/// Fires whenever a transaction begins. Will not fire on nested transactions.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public event Action TransactionBegan;
|
public event Action? TransactionBegan;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Fires when the last transaction completes.
|
/// Fires when the last transaction completes.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public event Action TransactionEnded;
|
public event Action? TransactionEnded;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Fires when <see cref="SaveState"/> is called and results in a non-transactional state save.
|
/// Fires when <see cref="SaveState"/> is called and results in a non-transactional state save.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public event Action SaveStateTriggered;
|
public event Action? SaveStateTriggered;
|
||||||
|
|
||||||
public bool TransactionActive => bulkChangesStarted > 0;
|
public bool TransactionActive => bulkChangesStarted > 0;
|
||||||
|
|
||||||
@ -35,7 +33,7 @@ namespace osu.Game.Screens.Edit
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Signal the beginning of a change.
|
/// Signal the beginning of a change.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void BeginChange()
|
public virtual void BeginChange()
|
||||||
{
|
{
|
||||||
if (bulkChangesStarted++ == 0)
|
if (bulkChangesStarted++ == 0)
|
||||||
TransactionBegan?.Invoke();
|
TransactionBegan?.Invoke();
|
||||||
|
@ -69,8 +69,7 @@ namespace osu.Game.Screens.Play.HUD
|
|||||||
{
|
{
|
||||||
var bindable = (IBindable)property.GetValue(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<Drawable> container)
|
if (component is Container<Drawable> container)
|
||||||
|
@ -36,6 +36,11 @@ namespace osu.Game.Skinning
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
void Reload();
|
void Reload();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reload this target from the provided skinnable information.
|
||||||
|
/// </summary>
|
||||||
|
void Reload(SkinnableInfo[] skinnableInfo);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Add a new skinnable component to this target.
|
/// Add a new skinnable component to this target.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -46,6 +51,6 @@ namespace osu.Game.Skinning
|
|||||||
/// Remove an existing skinnable component from this target.
|
/// Remove an existing skinnable component from this target.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="component">The component to remove.</param>
|
/// <param name="component">The component to remove.</param>
|
||||||
public void Remove(ISkinnableDrawable component);
|
void Remove(ISkinnableDrawable component);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,10 +2,12 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Game.Screens.Play.HUD;
|
||||||
|
|
||||||
namespace osu.Game.Skinning
|
namespace osu.Game.Skinning
|
||||||
{
|
{
|
||||||
@ -30,16 +32,31 @@ namespace osu.Game.Skinning
|
|||||||
Target = target;
|
Target = target;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public void Reload(SkinnableInfo[] skinnableInfo)
|
||||||
/// Reload all components in this container from the current skin.
|
{
|
||||||
/// </summary>
|
var drawables = new List<Drawable>();
|
||||||
public void Reload()
|
|
||||||
|
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();
|
ClearInternal();
|
||||||
components.Clear();
|
components.Clear();
|
||||||
ComponentsLoaded = false;
|
ComponentsLoaded = false;
|
||||||
|
|
||||||
content = CurrentSkin.GetDrawableComponent(new GlobalSkinComponentLookup(Target)) as SkinnableTargetComponentsContainer;
|
if (componentsContainer == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
content = componentsContainer;
|
||||||
|
|
||||||
cancellationSource?.Cancel();
|
cancellationSource?.Cancel();
|
||||||
cancellationSource = null;
|
cancellationSource = null;
|
||||||
|
Loading…
Reference in New Issue
Block a user