1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 08:27:49 +08:00

Merge pull request #10102 from peppy/editor-prompt-for-save

Prompt to save changes when exiting the editor
This commit is contained in:
Dan Balasescu 2020-09-09 23:56:41 +09:00 committed by GitHub
commit 75ebfe41e0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 143 additions and 31 deletions

View File

@ -18,6 +18,8 @@ namespace osu.Game.Tests.Visual.Editing
protected override Ruleset CreateEditorRuleset() => new OsuRuleset(); protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
protected new TestEditor Editor => (TestEditor)base.Editor;
public override void SetUpSteps() public override void SetUpSteps()
{ {
base.SetUpSteps(); base.SetUpSteps();
@ -35,6 +37,7 @@ namespace osu.Game.Tests.Visual.Editing
addUndoSteps(); addUndoSteps();
AddAssert("no change occurred", () => hitObjectCount == editorBeatmap.HitObjects.Count); AddAssert("no change occurred", () => hitObjectCount == editorBeatmap.HitObjects.Count);
AddAssert("no unsaved changes", () => !Editor.HasUnsavedChanges);
} }
[Test] [Test]
@ -47,6 +50,7 @@ namespace osu.Game.Tests.Visual.Editing
addRedoSteps(); addRedoSteps();
AddAssert("no change occurred", () => hitObjectCount == editorBeatmap.HitObjects.Count); AddAssert("no change occurred", () => hitObjectCount == editorBeatmap.HitObjects.Count);
AddAssert("no unsaved changes", () => !Editor.HasUnsavedChanges);
} }
[Test] [Test]
@ -64,9 +68,11 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("add hitobject", () => editorBeatmap.Add(expectedObject = new HitCircle { StartTime = 1000 })); AddStep("add hitobject", () => editorBeatmap.Add(expectedObject = new HitCircle { StartTime = 1000 }));
AddAssert("hitobject added", () => addedObject == expectedObject); AddAssert("hitobject added", () => addedObject == expectedObject);
AddAssert("unsaved changes", () => Editor.HasUnsavedChanges);
addUndoSteps(); addUndoSteps();
AddAssert("hitobject removed", () => removedObject == expectedObject); AddAssert("hitobject removed", () => removedObject == expectedObject);
AddAssert("no unsaved changes", () => !Editor.HasUnsavedChanges);
} }
[Test] [Test]
@ -94,6 +100,17 @@ namespace osu.Game.Tests.Visual.Editing
addRedoSteps(); addRedoSteps();
AddAssert("hitobject added", () => addedObject.StartTime == expectedObject.StartTime); // Can't compare via equality (new hitobject instance) AddAssert("hitobject added", () => addedObject.StartTime == expectedObject.StartTime); // Can't compare via equality (new hitobject instance)
AddAssert("no hitobject removed", () => removedObject == null); 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] [Test]
@ -120,6 +137,7 @@ namespace osu.Game.Tests.Visual.Editing
addUndoSteps(); addUndoSteps();
AddAssert("hitobject added", () => addedObject.StartTime == expectedObject.StartTime); // Can't compare via equality (new hitobject instance) AddAssert("hitobject added", () => addedObject.StartTime == expectedObject.StartTime); // Can't compare via equality (new hitobject instance)
AddAssert("no hitobject removed", () => removedObject == null); AddAssert("no hitobject removed", () => removedObject == null);
AddAssert("unsaved changes", () => Editor.HasUnsavedChanges); // 2 steps performed, 1 undone
} }
[Test] [Test]
@ -148,19 +166,24 @@ namespace osu.Game.Tests.Visual.Editing
addRedoSteps(); addRedoSteps();
AddAssert("hitobject removed", () => removedObject.StartTime == expectedObject.StartTime); // Can't compare via equality (new hitobject instance after undo) 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 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(); protected override Editor CreateEditor() => new TestEditor();
private class TestEditor : Editor protected class TestEditor : Editor
{ {
public new void Undo() => base.Undo(); public new void Undo() => base.Undo();
public new void Redo() => base.Redo(); public new void Redo() => base.Redo();
public new void Save() => base.Save();
public new bool HasUnsavedChanges => base.HasUnsavedChanges;
} }
} }
} }

View File

@ -2,39 +2,40 @@
// 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 osuTK.Graphics; using System.Collections.Generic;
using osu.Framework.Screens; using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; 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.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;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Framework.Timing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Graphics.Cursor; using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Overlays;
using osu.Game.Rulesets.Edit; 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.Compose;
using osu.Game.Screens.Edit.Design;
using osu.Game.Screens.Edit.Setup; using osu.Game.Screens.Edit.Setup;
using osu.Game.Screens.Edit.Timing; using osu.Game.Screens.Edit.Timing;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Users; using osu.Game.Users;
using osuTK.Graphics;
using osuTK.Input;
namespace osu.Game.Screens.Edit namespace osu.Game.Screens.Edit
{ {
@ -51,9 +52,18 @@ namespace osu.Game.Screens.Edit
public override bool AllowRateAdjustments => false; public override bool AllowRateAdjustments => false;
protected bool HasUnsavedChanges => lastSavedHash != changeHandler.CurrentStateHash;
[Resolved] [Resolved]
private BeatmapManager beatmapManager { get; set; } private BeatmapManager beatmapManager { get; set; }
[Resolved(canBeNull: true)]
private DialogOverlay dialogOverlay { get; set; }
private bool exitConfirmed;
private string lastSavedHash;
private Box bottomBackground; private Box bottomBackground;
private Container screenContainer; private Container screenContainer;
@ -118,13 +128,15 @@ namespace osu.Game.Screens.Edit
changeHandler = new EditorChangeHandler(editorBeatmap); changeHandler = new EditorChangeHandler(editorBeatmap);
dependencies.CacheAs<IEditorChangeHandler>(changeHandler); dependencies.CacheAs<IEditorChangeHandler>(changeHandler);
updateLastSavedHash();
EditorMenuBar menuBar; EditorMenuBar menuBar;
OsuMenuItem undoMenuItem; OsuMenuItem undoMenuItem;
OsuMenuItem redoMenuItem; OsuMenuItem redoMenuItem;
var fileMenuItems = new List<MenuItem> var fileMenuItems = new List<MenuItem>
{ {
new EditorMenuItem("Save", MenuItemType.Standard, saveBeatmap) new EditorMenuItem("Save", MenuItemType.Standard, Save)
}; };
if (RuntimeInfo.IsDesktop) if (RuntimeInfo.IsDesktop)
@ -237,6 +249,17 @@ namespace osu.Game.Screens.Edit
bottomBackground.Colour = colours.Gray2; 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() protected override void Update()
{ {
base.Update(); base.Update();
@ -256,7 +279,7 @@ namespace osu.Game.Screens.Edit
return true; return true;
case PlatformActionType.Save: case PlatformActionType.Save:
saveBeatmap(); Save();
return true; return true;
} }
@ -346,12 +369,31 @@ namespace osu.Game.Screens.Edit
public override bool OnExiting(IScreen next) public override bool OnExiting(IScreen next)
{ {
if (!exitConfirmed && dialogOverlay != null && HasUnsavedChanges)
{
dialogOverlay?.Push(new PromptForSaveDialog(confirmExit, confirmExitWithSave));
return true;
}
Background.FadeColour(Color4.White, 500); Background.FadeColour(Color4.White, 500);
resetTrack(); resetTrack();
return base.OnExiting(next); return base.OnExiting(next);
} }
private void confirmExitWithSave()
{
exitConfirmed = true;
Save();
this.Exit();
}
private void confirmExit()
{
exitConfirmed = true;
this.Exit();
}
protected void Undo() => changeHandler.RestoreState(-1); protected void Undo() => changeHandler.RestoreState(-1);
protected void Redo() => changeHandler.RestoreState(1); protected void Redo() => changeHandler.RestoreState(1);
@ -415,21 +457,17 @@ namespace osu.Game.Screens.Edit
clock.SeekForward(!clock.IsRunning, amount); 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);
}
private void exportBeatmap() private void exportBeatmap()
{ {
saveBeatmap(); Save();
beatmapManager.Export(Beatmap.Value.BeatmapSetInfo); beatmapManager.Export(Beatmap.Value.BeatmapSetInfo);
} }
private void updateLastSavedHash()
{
lastSavedHash = changeHandler.CurrentStateHash;
}
public double SnapTime(double time, double? referenceTime) => editorBeatmap.SnapTime(time, referenceTime); public double SnapTime(double time, double? referenceTime) => editorBeatmap.SnapTime(time, referenceTime);
public double GetBeatLengthAtTime(double referenceTime) => editorBeatmap.GetBeatLengthAtTime(referenceTime); public double GetBeatLengthAtTime(double referenceTime) => editorBeatmap.GetBeatLengthAtTime(referenceTime);

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Text; using System.Text;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Game.Beatmaps.Formats; using osu.Game.Beatmaps.Formats;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
@ -24,6 +25,18 @@ namespace osu.Game.Screens.Edit
private int currentState = -1; private int currentState = -1;
/// <summary>
/// A SHA-2 hash representing the current visible editor state.
/// </summary>
public string CurrentStateHash
{
get
{
using (var stream = new MemoryStream(savedStates[currentState]))
return stream.ComputeSHA2Hash();
}
}
private readonly EditorBeatmap editorBeatmap; private readonly EditorBeatmap editorBeatmap;
private int bulkChangesStarted; private int bulkChangesStarted;
private bool isRestoring; private bool isRestoring;

View File

@ -0,0 +1,37 @@
// 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;
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
},
new PopupDialogCancelButton
{
Text = @"Oops, continue editing",
},
};
}
}
}

View File

@ -27,6 +27,7 @@ namespace osu.Game.Tests.Beatmaps
BeatmapInfo.Ruleset = ruleset; BeatmapInfo.Ruleset = ruleset;
BeatmapInfo.RulesetID = ruleset.ID ?? 0; BeatmapInfo.RulesetID = ruleset.ID ?? 0;
BeatmapInfo.BeatmapSet.Metadata = BeatmapInfo.Metadata; BeatmapInfo.BeatmapSet.Metadata = BeatmapInfo.Metadata;
BeatmapInfo.BeatmapSet.Files = new List<BeatmapSetFileInfo>();
BeatmapInfo.BeatmapSet.Beatmaps = new List<BeatmapInfo> { BeatmapInfo }; BeatmapInfo.BeatmapSet.Beatmaps = new List<BeatmapInfo> { BeatmapInfo };
BeatmapInfo.BeatmapSet.OnlineInfo = new BeatmapSetOnlineInfo BeatmapInfo.BeatmapSet.OnlineInfo = new BeatmapSetOnlineInfo
{ {