1
0
mirror of https://github.com/ppy/osu.git synced 2025-03-10 03:57:20 +08:00

Add "discard unsaved changes" operation to beatmap editor

Apparently useful in modding workflows when you want to test out a few
different variants of a thing.

Re-uses `Ctrl-L` binding from stable. Some folks may argue that the
dialog makes the hotkey pointless, but I really do want to protect
users from accidental data loss, and also if you want to power through
it quickly, you can hit the 1 key when the dialog shows, which will
bypass the hold-to-activate period (which wasn't intentional, but so
many people want a bypass at this point that we're probably keeping that
behaviour for power users).
This commit is contained in:
Bartłomiej Dach 2025-03-05 14:11:44 +01:00
parent f6cf63edae
commit 3f461c0734
No known key found for this signature in database
8 changed files with 79 additions and 7 deletions

View File

@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
{
keyCount.Current.Value = 8;
});
AddUntilStep("dialog visible", () => Game.ChildrenOfType<IDialogOverlay>().SingleOrDefault()?.CurrentDialog, Is.InstanceOf<ReloadEditorDialog>);
AddUntilStep("dialog visible", () => Game.ChildrenOfType<IDialogOverlay>().SingleOrDefault()?.CurrentDialog, Is.InstanceOf<SaveAndReloadEditorDialog>);
AddStep("refuse", () => InputManager.Key(Key.Number2));
AddAssert("key count is 5", () => keyCount.Current.Value, () => Is.EqualTo(5));
@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
{
keyCount.Current.Value = 8;
});
AddUntilStep("dialog visible", () => Game.ChildrenOfType<IDialogOverlay>().Single().CurrentDialog, Is.InstanceOf<ReloadEditorDialog>);
AddUntilStep("dialog visible", () => Game.ChildrenOfType<IDialogOverlay>().Single().CurrentDialog, Is.InstanceOf<SaveAndReloadEditorDialog>);
AddStep("acquiesce", () => InputManager.Key(Key.Number1));
AddUntilStep("beatmap became 8K", () => Game.Beatmap.Value.BeatmapInfo.Difficulty.CircleSize, () => Is.EqualTo(8));
}

View File

@ -134,7 +134,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup
updatingKeyCount = true;
editor.Reload().ContinueWith(t =>
editor.SaveAndReload().ContinueWith(t =>
{
if (!t.GetResultSafely())
{

View File

@ -155,6 +155,7 @@ namespace osu.Game.Input.Bindings
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.B }, GlobalAction.EditorRemoveClosestBookmark),
new KeyBinding(new[] { InputKey.Alt, InputKey.Left }, GlobalAction.EditorSeekToPreviousBookmark),
new KeyBinding(new[] { InputKey.Alt, InputKey.Right }, GlobalAction.EditorSeekToNextBookmark),
new KeyBinding(new[] { InputKey.Control, InputKey.L }, GlobalAction.EditorDiscardUnsavedChanges),
};
private static IEnumerable<KeyBinding> editorTestPlayKeyBindings => new[]
@ -502,6 +503,9 @@ namespace osu.Game.Input.Bindings
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorToggleMoveControl))]
EditorToggleMoveControl,
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorDiscardUnsavedChanges))]
EditorDiscardUnsavedChanges,
}
public enum GlobalActionCategory

View File

@ -54,6 +54,11 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString EditorReloadDialogHeader => new TranslatableString(getKey(@"editor_reload_dialog_header"), @"The editor must be reloaded to apply this change. The beatmap will be saved.");
/// <summary>
/// "Discard all unsaved changes? This cannot be undone."
/// </summary>
public static LocalisableString DiscardUnsavedChangesDialogHeader => new TranslatableString(getKey(@"discard_unsaved_changes_dialog_header"), @"Discard all unsaved changes? This cannot be undone.");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View File

@ -459,6 +459,11 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString EditorToggleMoveControl => new TranslatableString(getKey(@"editor_toggle_move_control"), @"Toggle movement control");
/// <summary>
/// "Discard unsaved changes"
/// </summary>
public static LocalisableString EditorDiscardUnsavedChanges => new TranslatableString(getKey(@"editor_discard_unsaved_changes"), @"Discard unsaved changes");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View File

@ -0,0 +1,33 @@
// 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.Localisation;
using osu.Game.Overlays.Dialog;
namespace osu.Game.Screens.Edit
{
public partial class DiscardUnsavedChangesDialog : PopupDialog
{
public DiscardUnsavedChangesDialog(Action exit)
{
HeaderText = EditorDialogsStrings.DiscardUnsavedChangesDialogHeader;
Icon = FontAwesome.Solid.Trash;
Buttons = new PopupDialogButton[]
{
new PopupDialogDangerousButton
{
Text = EditorDialogsStrings.ForgetAllChanges,
Action = exit
},
new PopupDialogCancelButton
{
Text = EditorDialogsStrings.ContinueEditing,
},
};
}
}
}

View File

@ -164,6 +164,7 @@ namespace osu.Game.Screens.Edit
private bool switchingDifficulty;
private string lastSavedHash;
private EditorMenuItem discardChangesMenuItem;
private ScreenContainer screenContainer;
@ -391,6 +392,10 @@ namespace osu.Game.Screens.Edit
{
undoMenuItem = new EditorMenuItem(CommonStrings.Undo, MenuItemType.Standard, Undo) { Hotkey = new Hotkey(PlatformAction.Undo) },
redoMenuItem = new EditorMenuItem(CommonStrings.Redo, MenuItemType.Standard, Redo) { Hotkey = new Hotkey(PlatformAction.Redo) },
discardChangesMenuItem = new EditorMenuItem("Discard unsaved changes", MenuItemType.Destructive, DiscardUnsavedChanges)
{
Hotkey = new Hotkey(GlobalAction.EditorDiscardUnsavedChanges)
},
new OsuMenuItemSpacer(),
cutMenuItem = new EditorMenuItem(CommonStrings.Cut, MenuItemType.Standard, Cut) { Hotkey = new Hotkey(PlatformAction.Cut) },
copyMenuItem = new EditorMenuItem(CommonStrings.Copy, MenuItemType.Standard, Copy) { Hotkey = new Hotkey(PlatformAction.Copy) },
@ -607,6 +612,8 @@ namespace osu.Game.Screens.Edit
{
base.Update();
clock.ProcessFrame();
discardChangesMenuItem.Action.Disabled = !HasUnsavedChanges;
}
public bool OnPressed(KeyBindingPressEvent<PlatformAction> e)
@ -821,6 +828,10 @@ namespace osu.Game.Screens.Edit
case GlobalAction.EditorTestGameplay:
bottomBar.TestGameplayButton.TriggerClick();
return true;
case GlobalAction.EditorDiscardUnsavedChanges:
DiscardUnsavedChanges();
return true;
}
return false;
@ -1008,6 +1019,20 @@ namespace osu.Game.Screens.Edit
protected void Redo() => changeHandler?.RestoreState(1);
protected void DiscardUnsavedChanges()
{
if (!HasUnsavedChanges)
return;
// we're not doing this via `changeHandler` because `changeHandler` has limited number of undo actions
// and therefore there's no guarantee that it even *has* the beatmap's last saved state in its history still.
dialogOverlay.Push(new DiscardUnsavedChangesDialog(() =>
{
updateLastSavedHash(); // without this a second dialog will show (the standard "save unsaved changes" one that shows on exit).
SwitchToDifficulty(editorBeatmap.BeatmapInfo);
}));
}
protected void SetPreviewPointToCurrentTime()
{
editorBeatmap.PreviewTime.Value = (int)clock.CurrentTime;
@ -1510,11 +1535,11 @@ namespace osu.Game.Screens.Edit
loader?.CancelPendingDifficultySwitch();
}
public Task<bool> Reload()
public Task<bool> SaveAndReload()
{
var tcs = new TaskCompletionSource<bool>();
dialogOverlay.Push(new ReloadEditorDialog(
dialogOverlay.Push(new SaveAndReloadEditorDialog(
reload: () =>
{
bool reloadedSuccessfully = attemptMutationOperation(() =>

View File

@ -8,9 +8,9 @@ using osu.Game.Localisation;
namespace osu.Game.Screens.Edit
{
public partial class ReloadEditorDialog : PopupDialog
public partial class SaveAndReloadEditorDialog : PopupDialog
{
public ReloadEditorDialog(Action reload, Action cancel)
public SaveAndReloadEditorDialog(Action reload, Action cancel)
{
HeaderText = EditorDialogsStrings.EditorReloadDialogHeader;