From ac0c4fcb8c2bfb162b820c9b03a128304fe31d0b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 9 Sep 2020 19:31:18 +0900 Subject: [PATCH 1/5] Add prompt to save beatmap on exiting editor --- osu.Game/Screens/Edit/Editor.cs | 57 ++++++++++++++------ osu.Game/Screens/Edit/PromptForSaveDialog.cs | 33 ++++++++++++ 2 files changed, 74 insertions(+), 16 deletions(-) create mode 100644 osu.Game/Screens/Edit/PromptForSaveDialog.cs diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index ac1f61c4fd..7e17225846 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -2,39 +2,40 @@ // See the LICENCE file in the repository root for full licence text. using System; -using osuTK.Graphics; -using osu.Framework.Screens; +using System.Collections.Generic; +using osu.Framework; +using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Game.Graphics; -using osu.Game.Screens.Edit.Components.Timelines.Summary; -using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics.UserInterface; -using osu.Framework.Input.Events; -using osu.Framework.Platform; -using osu.Framework.Timing; -using osu.Game.Graphics.UserInterface; -using osu.Game.Screens.Edit.Components; -using osu.Game.Screens.Edit.Components.Menus; -using osu.Game.Screens.Edit.Design; -using osuTK.Input; -using System.Collections.Generic; -using osu.Framework; using osu.Framework.Input; using osu.Framework.Input.Bindings; +using osu.Framework.Input.Events; using osu.Framework.Logging; +using osu.Framework.Platform; +using osu.Framework.Screens; +using osu.Framework.Timing; using osu.Game.Beatmaps; +using osu.Game.Graphics; using osu.Game.Graphics.Cursor; +using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Online.API; +using osu.Game.Overlays; using osu.Game.Rulesets.Edit; +using osu.Game.Screens.Edit.Components; +using osu.Game.Screens.Edit.Components.Menus; +using osu.Game.Screens.Edit.Components.Timelines.Summary; using osu.Game.Screens.Edit.Compose; +using osu.Game.Screens.Edit.Design; using osu.Game.Screens.Edit.Setup; using osu.Game.Screens.Edit.Timing; using osu.Game.Screens.Play; using osu.Game.Users; +using osuTK.Graphics; +using osuTK.Input; namespace osu.Game.Screens.Edit { @@ -54,6 +55,11 @@ namespace osu.Game.Screens.Edit [Resolved] private BeatmapManager beatmapManager { get; set; } + [Resolved(canBeNull: true)] + private DialogOverlay dialogOverlay { get; set; } + + private bool exitConfirmed; + private Box bottomBackground; private Container screenContainer; @@ -346,12 +352,31 @@ namespace osu.Game.Screens.Edit public override bool OnExiting(IScreen next) { + if (!exitConfirmed && dialogOverlay != null) + { + dialogOverlay?.Push(new PromptForSaveDialog(confirmExit, confirmExitWithSave)); + return true; + } + Background.FadeColour(Color4.White, 500); resetTrack(); return base.OnExiting(next); } + private void confirmExitWithSave() + { + exitConfirmed = true; + saveBeatmap(); + this.Exit(); + } + + private void confirmExit() + { + exitConfirmed = true; + this.Exit(); + } + protected void Undo() => changeHandler.RestoreState(-1); protected void Redo() => changeHandler.RestoreState(1); diff --git a/osu.Game/Screens/Edit/PromptForSaveDialog.cs b/osu.Game/Screens/Edit/PromptForSaveDialog.cs new file mode 100644 index 0000000000..38d956557d --- /dev/null +++ b/osu.Game/Screens/Edit/PromptForSaveDialog.cs @@ -0,0 +1,33 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Graphics.Sprites; +using osu.Game.Overlays.Dialog; + +namespace osu.Game.Screens.Edit +{ + public class PromptForSaveDialog : PopupDialog + { + public PromptForSaveDialog(Action exit, Action saveAndExit) + { + HeaderText = "Did you want to save your changes?"; + + Icon = FontAwesome.Regular.Save; + + Buttons = new PopupDialogButton[] + { + new PopupDialogCancelButton + { + Text = @"Save my masterpiece!", + Action = saveAndExit + }, + new PopupDialogOkButton + { + Text = @"Forget all changes", + Action = exit + }, + }; + } + } +} From 6f067ff300910cf82a0cfac72d3578fd73c520d2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 9 Sep 2020 19:40:41 +0900 Subject: [PATCH 2/5] Only show confirmation if changes have been made since last save --- osu.Game/Screens/Edit/Editor.cs | 13 ++++++++++++- osu.Game/Screens/Edit/EditorChangeHandler.cs | 13 +++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 7e17225846..58395e4848 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -60,6 +60,8 @@ namespace osu.Game.Screens.Edit private bool exitConfirmed; + private string lastSavedHash; + private Box bottomBackground; private Container screenContainer; @@ -124,6 +126,8 @@ namespace osu.Game.Screens.Edit changeHandler = new EditorChangeHandler(editorBeatmap); dependencies.CacheAs(changeHandler); + updateLastSavedHash(); + EditorMenuBar menuBar; OsuMenuItem undoMenuItem; OsuMenuItem redoMenuItem; @@ -352,7 +356,7 @@ namespace osu.Game.Screens.Edit public override bool OnExiting(IScreen next) { - if (!exitConfirmed && dialogOverlay != null) + if (!exitConfirmed && dialogOverlay != null && changeHandler.CurrentStateHash != lastSavedHash) { dialogOverlay?.Push(new PromptForSaveDialog(confirmExit, confirmExitWithSave)); return true; @@ -447,6 +451,8 @@ namespace osu.Game.Screens.Edit // save the loaded beatmap's data stream. beatmapManager.Save(playableBeatmap.BeatmapInfo, editorBeatmap, editorBeatmap.BeatmapSkin); + + updateLastSavedHash(); } private void exportBeatmap() @@ -455,6 +461,11 @@ namespace osu.Game.Screens.Edit beatmapManager.Export(Beatmap.Value.BeatmapSetInfo); } + private void updateLastSavedHash() + { + lastSavedHash = changeHandler.CurrentStateHash; + } + public double SnapTime(double time, double? referenceTime) => editorBeatmap.SnapTime(time, referenceTime); public double GetBeatLengthAtTime(double referenceTime) => editorBeatmap.GetBeatLengthAtTime(referenceTime); diff --git a/osu.Game/Screens/Edit/EditorChangeHandler.cs b/osu.Game/Screens/Edit/EditorChangeHandler.cs index 927c823c64..aa0f89912a 100644 --- a/osu.Game/Screens/Edit/EditorChangeHandler.cs +++ b/osu.Game/Screens/Edit/EditorChangeHandler.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.IO; using System.Text; using osu.Framework.Bindables; +using osu.Framework.Extensions; using osu.Game.Beatmaps.Formats; using osu.Game.Rulesets.Objects; @@ -24,6 +25,18 @@ namespace osu.Game.Screens.Edit private int currentState = -1; + /// + /// A SHA-2 hash representing the current visible editor state. + /// + public string CurrentStateHash + { + get + { + using (var stream = new MemoryStream(savedStates[currentState])) + return stream.ComputeSHA2Hash(); + } + } + private readonly EditorBeatmap editorBeatmap; private int bulkChangesStarted; private bool isRestoring; From 327179a81efbc9524be1a3a7d0ba1d54a3e46dff Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 9 Sep 2020 19:42:03 +0900 Subject: [PATCH 3/5] Expose unsaved changes state --- osu.Game/Screens/Edit/Editor.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 58395e4848..34c69d09e0 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -52,6 +52,8 @@ namespace osu.Game.Screens.Edit public override bool AllowRateAdjustments => false; + public bool HasUnsavedChanges => lastSavedHash != changeHandler.CurrentStateHash; + [Resolved] private BeatmapManager beatmapManager { get; set; } @@ -356,7 +358,7 @@ namespace osu.Game.Screens.Edit public override bool OnExiting(IScreen next) { - if (!exitConfirmed && dialogOverlay != null && changeHandler.CurrentStateHash != lastSavedHash) + if (!exitConfirmed && dialogOverlay != null && HasUnsavedChanges) { dialogOverlay?.Push(new PromptForSaveDialog(confirmExit, confirmExitWithSave)); return true; From c6e72dabd372c35a589e2b5c23220e5478b43536 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 9 Sep 2020 19:57:28 +0900 Subject: [PATCH 4/5] Add test coverage --- .../Editing/TestSceneEditorChangeStates.cs | 29 +++++++++++++++-- osu.Game/Screens/Edit/Editor.cs | 32 +++++++++---------- osu.Game/Tests/Beatmaps/TestBeatmap.cs | 1 + 3 files changed, 43 insertions(+), 19 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorChangeStates.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorChangeStates.cs index 293a6e6869..c8a32d966f 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorChangeStates.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorChangeStates.cs @@ -18,6 +18,8 @@ namespace osu.Game.Tests.Visual.Editing protected override Ruleset CreateEditorRuleset() => new OsuRuleset(); + protected new TestEditor Editor => (TestEditor)base.Editor; + public override void SetUpSteps() { base.SetUpSteps(); @@ -35,6 +37,7 @@ namespace osu.Game.Tests.Visual.Editing addUndoSteps(); AddAssert("no change occurred", () => hitObjectCount == editorBeatmap.HitObjects.Count); + AddAssert("no unsaved changes", () => !Editor.HasUnsavedChanges); } [Test] @@ -47,6 +50,7 @@ namespace osu.Game.Tests.Visual.Editing addRedoSteps(); AddAssert("no change occurred", () => hitObjectCount == editorBeatmap.HitObjects.Count); + AddAssert("no unsaved changes", () => !Editor.HasUnsavedChanges); } [Test] @@ -64,9 +68,11 @@ namespace osu.Game.Tests.Visual.Editing AddStep("add hitobject", () => editorBeatmap.Add(expectedObject = new HitCircle { StartTime = 1000 })); AddAssert("hitobject added", () => addedObject == expectedObject); + AddAssert("unsaved changes", () => Editor.HasUnsavedChanges); addUndoSteps(); AddAssert("hitobject removed", () => removedObject == expectedObject); + AddAssert("no unsaved changes", () => !Editor.HasUnsavedChanges); } [Test] @@ -94,6 +100,17 @@ namespace osu.Game.Tests.Visual.Editing addRedoSteps(); AddAssert("hitobject added", () => addedObject.StartTime == expectedObject.StartTime); // Can't compare via equality (new hitobject instance) AddAssert("no hitobject removed", () => removedObject == null); + AddAssert("unsaved changes", () => Editor.HasUnsavedChanges); + } + + [Test] + public void TestAddObjectThenSaveHasNoUnsavedChanges() + { + AddStep("add hitobject", () => editorBeatmap.Add(new HitCircle { StartTime = 1000 })); + + AddAssert("unsaved changes", () => Editor.HasUnsavedChanges); + AddStep("save changes", () => Editor.Save()); + AddAssert("no unsaved changes", () => !Editor.HasUnsavedChanges); } [Test] @@ -120,6 +137,7 @@ namespace osu.Game.Tests.Visual.Editing addUndoSteps(); AddAssert("hitobject added", () => addedObject.StartTime == expectedObject.StartTime); // Can't compare via equality (new hitobject instance) AddAssert("no hitobject removed", () => removedObject == null); + AddAssert("unsaved changes", () => Editor.HasUnsavedChanges); // 2 steps performed, 1 undone } [Test] @@ -148,19 +166,24 @@ namespace osu.Game.Tests.Visual.Editing addRedoSteps(); AddAssert("hitobject removed", () => removedObject.StartTime == expectedObject.StartTime); // Can't compare via equality (new hitobject instance after undo) AddAssert("no hitobject added", () => addedObject == null); + AddAssert("no changes", () => !Editor.HasUnsavedChanges); // end result is empty beatmap, matching original state } - private void addUndoSteps() => AddStep("undo", () => ((TestEditor)Editor).Undo()); + private void addUndoSteps() => AddStep("undo", () => Editor.Undo()); - private void addRedoSteps() => AddStep("redo", () => ((TestEditor)Editor).Redo()); + private void addRedoSteps() => AddStep("redo", () => Editor.Redo()); protected override Editor CreateEditor() => new TestEditor(); - private class TestEditor : Editor + protected class TestEditor : Editor { public new void Undo() => base.Undo(); public new void Redo() => base.Redo(); + + public new void Save() => base.Save(); + + public new bool HasUnsavedChanges => base.HasUnsavedChanges; } } } diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 34c69d09e0..23eb704920 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -52,7 +52,7 @@ namespace osu.Game.Screens.Edit public override bool AllowRateAdjustments => false; - public bool HasUnsavedChanges => lastSavedHash != changeHandler.CurrentStateHash; + protected bool HasUnsavedChanges => lastSavedHash != changeHandler.CurrentStateHash; [Resolved] private BeatmapManager beatmapManager { get; set; } @@ -136,7 +136,7 @@ namespace osu.Game.Screens.Edit var fileMenuItems = new List { - new EditorMenuItem("Save", MenuItemType.Standard, saveBeatmap) + new EditorMenuItem("Save", MenuItemType.Standard, Save) }; if (RuntimeInfo.IsDesktop) @@ -249,6 +249,17 @@ namespace osu.Game.Screens.Edit bottomBackground.Colour = colours.Gray2; } + protected void Save() + { + // apply any set-level metadata changes. + beatmapManager.Update(playableBeatmap.BeatmapInfo.BeatmapSet); + + // save the loaded beatmap's data stream. + beatmapManager.Save(playableBeatmap.BeatmapInfo, editorBeatmap, editorBeatmap.BeatmapSkin); + + updateLastSavedHash(); + } + protected override void Update() { base.Update(); @@ -268,7 +279,7 @@ namespace osu.Game.Screens.Edit return true; case PlatformActionType.Save: - saveBeatmap(); + Save(); return true; } @@ -373,7 +384,7 @@ namespace osu.Game.Screens.Edit private void confirmExitWithSave() { exitConfirmed = true; - saveBeatmap(); + Save(); this.Exit(); } @@ -446,20 +457,9 @@ namespace osu.Game.Screens.Edit clock.SeekForward(!clock.IsRunning, amount); } - private void saveBeatmap() - { - // apply any set-level metadata changes. - beatmapManager.Update(playableBeatmap.BeatmapInfo.BeatmapSet); - - // save the loaded beatmap's data stream. - beatmapManager.Save(playableBeatmap.BeatmapInfo, editorBeatmap, editorBeatmap.BeatmapSkin); - - updateLastSavedHash(); - } - private void exportBeatmap() { - saveBeatmap(); + Save(); beatmapManager.Export(Beatmap.Value.BeatmapSetInfo); } diff --git a/osu.Game/Tests/Beatmaps/TestBeatmap.cs b/osu.Game/Tests/Beatmaps/TestBeatmap.cs index 9fc20fd0f2..a375a17bcf 100644 --- a/osu.Game/Tests/Beatmaps/TestBeatmap.cs +++ b/osu.Game/Tests/Beatmaps/TestBeatmap.cs @@ -27,6 +27,7 @@ namespace osu.Game.Tests.Beatmaps BeatmapInfo.Ruleset = ruleset; BeatmapInfo.RulesetID = ruleset.ID ?? 0; BeatmapInfo.BeatmapSet.Metadata = BeatmapInfo.Metadata; + BeatmapInfo.BeatmapSet.Files = new List(); BeatmapInfo.BeatmapSet.Beatmaps = new List { BeatmapInfo }; BeatmapInfo.BeatmapSet.OnlineInfo = new BeatmapSetOnlineInfo { From 1803ecad8001db57c39b62fbab2ecacc07d7a9fe Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 9 Sep 2020 20:00:38 +0900 Subject: [PATCH 5/5] Add cancel exit button --- osu.Game/Screens/Edit/PromptForSaveDialog.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Screens/Edit/PromptForSaveDialog.cs b/osu.Game/Screens/Edit/PromptForSaveDialog.cs index 38d956557d..16504b47bd 100644 --- a/osu.Game/Screens/Edit/PromptForSaveDialog.cs +++ b/osu.Game/Screens/Edit/PromptForSaveDialog.cs @@ -27,6 +27,10 @@ namespace osu.Game.Screens.Edit Text = @"Forget all changes", Action = exit }, + new PopupDialogCancelButton + { + Text = @"Oops, continue editing", + }, }; } }