1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-21 20:47:28 +08:00

Merge pull request #26717 from peppy/fix-editor-didnt-save

Fix editor not saving when textbox is focused during exit procedure
This commit is contained in:
Bartłomiej Dach 2024-05-24 13:28:28 +02:00 committed by GitHub
commit 80b7653ae7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 173 additions and 16 deletions

View File

@ -3,17 +3,22 @@
#nullable disable #nullable disable
using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input;
using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Setup; using osu.Game.Screens.Edit.Setup;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Editing namespace osu.Game.Tests.Visual.Editing
{ {
public partial class TestSceneMetadataSection : OsuTestScene public partial class TestSceneMetadataSection : OsuManualInputManagerTestScene
{ {
[Cached] [Cached]
private EditorBeatmap editorBeatmap = new EditorBeatmap(new Beatmap private EditorBeatmap editorBeatmap = new EditorBeatmap(new Beatmap
@ -26,6 +31,81 @@ namespace osu.Game.Tests.Visual.Editing
private TestMetadataSection metadataSection; private TestMetadataSection metadataSection;
[Test]
public void TestUpdateViaTextBoxOnFocusLoss()
{
AddStep("set metadata", () =>
{
editorBeatmap.Metadata.Artist = "Example Artist";
editorBeatmap.Metadata.ArtistUnicode = string.Empty;
});
createSection();
TextBox textbox;
AddStep("focus first textbox", () =>
{
textbox = metadataSection.ChildrenOfType<TextBox>().First();
InputManager.MoveMouseTo(textbox);
InputManager.Click(MouseButton.Left);
});
AddStep("simulate changing textbox", () =>
{
// Can't simulate text input but this should work.
InputManager.Keys(PlatformAction.SelectAll);
InputManager.Keys(PlatformAction.Copy);
InputManager.Keys(PlatformAction.Paste);
InputManager.Keys(PlatformAction.Paste);
});
assertArtistMetadata("Example Artist");
// It's important values are committed immediately on focus loss so the editor exit sequence detects them.
AddAssert("value immediately changed on focus loss", () =>
{
((IFocusManager)InputManager).TriggerFocusContention(metadataSection);
return editorBeatmap.Metadata.Artist;
}, () => Is.EqualTo("Example ArtistExample Artist"));
}
[Test]
public void TestUpdateViaTextBoxOnCommit()
{
AddStep("set metadata", () =>
{
editorBeatmap.Metadata.Artist = "Example Artist";
editorBeatmap.Metadata.ArtistUnicode = string.Empty;
});
createSection();
TextBox textbox;
AddStep("focus first textbox", () =>
{
textbox = metadataSection.ChildrenOfType<TextBox>().First();
InputManager.MoveMouseTo(textbox);
InputManager.Click(MouseButton.Left);
});
AddStep("simulate changing textbox", () =>
{
// Can't simulate text input but this should work.
InputManager.Keys(PlatformAction.SelectAll);
InputManager.Keys(PlatformAction.Copy);
InputManager.Keys(PlatformAction.Paste);
InputManager.Keys(PlatformAction.Paste);
});
assertArtistMetadata("Example Artist");
AddStep("commit", () => InputManager.Key(Key.Enter));
assertArtistMetadata("Example ArtistExample Artist");
}
[Test] [Test]
public void TestMinimalMetadata() public void TestMinimalMetadata()
{ {
@ -40,7 +120,7 @@ namespace osu.Game.Tests.Visual.Editing
createSection(); createSection();
assertArtist("Example Artist"); assertArtistTextBox("Example Artist");
assertRomanisedArtist("Example Artist", false); assertRomanisedArtist("Example Artist", false);
assertTitle("Example Title"); assertTitle("Example Title");
@ -61,7 +141,7 @@ namespace osu.Game.Tests.Visual.Editing
createSection(); createSection();
assertArtist("*なみりん"); assertArtistTextBox("*なみりん");
assertRomanisedArtist(string.Empty, true); assertRomanisedArtist(string.Empty, true);
assertTitle("コイシテイク・プラネット"); assertTitle("コイシテイク・プラネット");
@ -82,7 +162,7 @@ namespace osu.Game.Tests.Visual.Editing
createSection(); createSection();
assertArtist("*なみりん"); assertArtistTextBox("*なみりん");
assertRomanisedArtist("*namirin", true); assertRomanisedArtist("*namirin", true);
assertTitle("コイシテイク・プラネット"); assertTitle("コイシテイク・プラネット");
@ -104,11 +184,11 @@ namespace osu.Game.Tests.Visual.Editing
createSection(); createSection();
AddStep("set romanised artist name", () => metadataSection.ArtistTextBox.Current.Value = "*namirin"); AddStep("set romanised artist name", () => metadataSection.ArtistTextBox.Current.Value = "*namirin");
assertArtist("*namirin"); assertArtistTextBox("*namirin");
assertRomanisedArtist("*namirin", false); assertRomanisedArtist("*namirin", false);
AddStep("set native artist name", () => metadataSection.ArtistTextBox.Current.Value = "*なみりん"); AddStep("set native artist name", () => metadataSection.ArtistTextBox.Current.Value = "*なみりん");
assertArtist("*なみりん"); assertArtistTextBox("*なみりん");
assertRomanisedArtist("*namirin", true); assertRomanisedArtist("*namirin", true);
AddStep("set romanised title", () => metadataSection.TitleTextBox.Current.Value = "Hitokoto no kyori"); AddStep("set romanised title", () => metadataSection.TitleTextBox.Current.Value = "Hitokoto no kyori");
@ -123,21 +203,24 @@ namespace osu.Game.Tests.Visual.Editing
private void createSection() private void createSection()
=> AddStep("create metadata section", () => Child = metadataSection = new TestMetadataSection()); => AddStep("create metadata section", () => Child = metadataSection = new TestMetadataSection());
private void assertArtist(string expected) private void assertArtistMetadata(string expected)
=> AddAssert($"artist is {expected}", () => metadataSection.ArtistTextBox.Current.Value == expected); => AddAssert($"artist metadata is {expected}", () => editorBeatmap.Metadata.Artist, () => Is.EqualTo(expected));
private void assertArtistTextBox(string expected)
=> AddAssert($"artist textbox is {expected}", () => metadataSection.ArtistTextBox.Current.Value, () => Is.EqualTo(expected));
private void assertRomanisedArtist(string expected, bool editable) private void assertRomanisedArtist(string expected, bool editable)
{ {
AddAssert($"romanised artist is {expected}", () => metadataSection.RomanisedArtistTextBox.Current.Value == expected); AddAssert($"romanised artist is {expected}", () => metadataSection.RomanisedArtistTextBox.Current.Value, () => Is.EqualTo(expected));
AddAssert($"romanised artist is {(editable ? "" : "not ")}editable", () => metadataSection.RomanisedArtistTextBox.ReadOnly == !editable); AddAssert($"romanised artist is {(editable ? "" : "not ")}editable", () => metadataSection.RomanisedArtistTextBox.ReadOnly == !editable);
} }
private void assertTitle(string expected) private void assertTitle(string expected)
=> AddAssert($"title is {expected}", () => metadataSection.TitleTextBox.Current.Value == expected); => AddAssert($"title is {expected}", () => metadataSection.TitleTextBox.Current.Value, () => Is.EqualTo(expected));
private void assertRomanisedTitle(string expected, bool editable) private void assertRomanisedTitle(string expected, bool editable)
{ {
AddAssert($"romanised title is {expected}", () => metadataSection.RomanisedTitleTextBox.Current.Value == expected); AddAssert($"romanised title is {expected}", () => metadataSection.RomanisedTitleTextBox.Current.Value, () => Is.EqualTo(expected));
AddAssert($"romanised title is {(editable ? "" : "not ")}editable", () => metadataSection.RomanisedTitleTextBox.ReadOnly == !editable); AddAssert($"romanised title is {(editable ? "" : "not ")}editable", () => metadataSection.RomanisedTitleTextBox.ReadOnly == !editable);
} }

View File

@ -7,6 +7,7 @@ using osu.Framework.Extensions;
using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -17,6 +18,7 @@ using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.GameplayTest; using osu.Game.Screens.Edit.GameplayTest;
using osu.Game.Screens.Edit.Setup;
using osu.Game.Screens.Menu; using osu.Game.Screens.Menu;
using osu.Game.Screens.Select; using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Filter; using osu.Game.Screens.Select.Filter;
@ -27,6 +29,59 @@ namespace osu.Game.Tests.Visual.Navigation
{ {
public partial class TestSceneBeatmapEditorNavigation : OsuGameTestScene public partial class TestSceneBeatmapEditorNavigation : OsuGameTestScene
{ {
[Test]
public void TestChangeMetadataExitWhileTextboxFocusedPromptsSave()
{
BeatmapSetInfo beatmapSet = null!;
AddStep("import test beatmap", () => Game.BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely());
AddStep("retrieve beatmap", () => beatmapSet = Game.BeatmapManager.QueryBeatmapSet(set => !set.Protected).AsNonNull().Value.Detach());
AddStep("present beatmap", () => Game.PresentBeatmap(beatmapSet));
AddUntilStep("wait for song select",
() => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet)
&& Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect
&& songSelect.IsLoaded);
AddStep("switch ruleset", () => Game.Ruleset.Value = new ManiaRuleset().RulesetInfo);
AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0)));
AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse);
AddStep("change to song setup", () => InputManager.Key(Key.F4));
TextBox textbox = null!;
AddUntilStep("wait for metadata section", () =>
{
var t = Game.ChildrenOfType<MetadataSection>().SingleOrDefault().ChildrenOfType<TextBox>().FirstOrDefault();
if (t == null)
return false;
textbox = t;
return true;
});
AddStep("focus textbox", () =>
{
InputManager.MoveMouseTo(textbox);
InputManager.Click(MouseButton.Left);
});
AddStep("simulate changing textbox", () =>
{
// Can't simulate text input but this should work.
InputManager.Keys(PlatformAction.SelectAll);
InputManager.Keys(PlatformAction.Copy);
InputManager.Keys(PlatformAction.Paste);
InputManager.Keys(PlatformAction.Paste);
});
AddStep("exit", () => Game.ChildrenOfType<Editor>().Single().Exit());
AddUntilStep("save dialog displayed", () => Game.ChildrenOfType<DialogOverlay>().SingleOrDefault()?.CurrentDialog is PromptForSaveDialog);
}
[Test] [Test]
public void TestEditorGameplayTestAlwaysUsesOriginalRuleset() public void TestEditorGameplayTestAlwaysUsesOriginalRuleset()
{ {

View File

@ -719,6 +719,8 @@ namespace osu.Game.Screens.Edit
public override bool OnExiting(ScreenExitEvent e) public override bool OnExiting(ScreenExitEvent e)
{ {
currentScreen?.OnExiting(e);
if (!ExitConfirmed) if (!ExitConfirmed)
{ {
// dialog overlay may not be available in visual tests. // dialog overlay may not be available in visual tests.

View File

@ -6,6 +6,7 @@ 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.Cursor; using osu.Framework.Graphics.Cursor;
using osu.Framework.Screens;
namespace osu.Game.Screens.Edit namespace osu.Game.Screens.Edit
{ {
@ -37,6 +38,10 @@ namespace osu.Game.Screens.Edit
protected override void PopOut() => this.FadeOut(); protected override void PopOut() => this.FadeOut();
public virtual void OnExiting(ScreenExitEvent e)
{
}
#region Clipboard operations #region Clipboard operations
public BindableBool CanCut { get; } = new BindableBool(); public BindableBool CanCut { get; } = new BindableBool();

View File

@ -53,9 +53,6 @@ namespace osu.Game.Screens.Edit.Setup
sourceTextBox = createTextBox<LabelledTextBox>(BeatmapsetsStrings.ShowInfoSource, metadata.Source), sourceTextBox = createTextBox<LabelledTextBox>(BeatmapsetsStrings.ShowInfoSource, metadata.Source),
tagsTextBox = createTextBox<LabelledTextBox>(BeatmapsetsStrings.ShowInfoTags, metadata.Tags) tagsTextBox = createTextBox<LabelledTextBox>(BeatmapsetsStrings.ShowInfoTags, metadata.Tags)
}; };
foreach (var item in Children.OfType<LabelledTextBox>())
item.OnCommit += onCommit;
} }
private TTextBox createTextBox<TTextBox>(LocalisableString label, string initialValue) private TTextBox createTextBox<TTextBox>(LocalisableString label, string initialValue)
@ -77,6 +74,10 @@ namespace osu.Game.Screens.Edit.Setup
ArtistTextBox.Current.BindValueChanged(artist => transferIfRomanised(artist.NewValue, RomanisedArtistTextBox)); ArtistTextBox.Current.BindValueChanged(artist => transferIfRomanised(artist.NewValue, RomanisedArtistTextBox));
TitleTextBox.Current.BindValueChanged(title => transferIfRomanised(title.NewValue, RomanisedTitleTextBox)); TitleTextBox.Current.BindValueChanged(title => transferIfRomanised(title.NewValue, RomanisedTitleTextBox));
foreach (var item in Children.OfType<LabelledTextBox>())
item.OnCommit += onCommit;
updateReadOnlyState(); updateReadOnlyState();
} }
@ -86,7 +87,6 @@ namespace osu.Game.Screens.Edit.Setup
target.Current.Value = value; target.Current.Value = value;
updateReadOnlyState(); updateReadOnlyState();
Scheduler.AddOnce(updateMetadata);
} }
private void updateReadOnlyState() private void updateReadOnlyState()
@ -101,7 +101,7 @@ namespace osu.Game.Screens.Edit.Setup
// for now, update on commit rather than making BeatmapMetadata bindables. // for now, update on commit rather than making BeatmapMetadata bindables.
// after switching database engines we can reconsider if switching to bindables is a good direction. // after switching database engines we can reconsider if switching to bindables is a good direction.
Scheduler.AddOnce(updateMetadata); updateMetadata();
} }
private void updateMetadata() private void updateMetadata()

View File

@ -5,6 +5,7 @@ using System.Collections.Generic;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Screens;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Overlays; using osu.Game.Overlays;
@ -55,6 +56,17 @@ namespace osu.Game.Screens.Edit.Setup
})); }));
} }
public override void OnExiting(ScreenExitEvent e)
{
base.OnExiting(e);
// Before exiting, trigger a focus loss.
//
// This is important to ensure that if the user is still editing a textbox, it will commit
// (and potentially block the exit procedure for save).
GetContainingFocusManager()?.TriggerFocusContention(this);
}
private partial class SetupScreenSectionsContainer : SectionsContainer<SetupSection> private partial class SetupScreenSectionsContainer : SectionsContainer<SetupSection>
{ {
protected override UserTrackingScrollContainer CreateScrollContainer() protected override UserTrackingScrollContainer CreateScrollContainer()