1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-16 10:17:36 +08:00
osu-lazer/osu.Game/Screens/Edit/Editor.cs

1390 lines
52 KiB
C#
Raw Normal View History

// 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.
2018-04-13 17:19:50 +08:00
2022-06-17 15:37:17 +08:00
#nullable disable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using JetBrains.Annotations;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
2018-04-13 17:19:50 +08:00
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
2018-04-13 17:19:50 +08:00
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input;
2019-06-30 18:31:31 +08:00
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
2022-07-20 03:38:23 +08:00
using osu.Framework.Localisation;
using osu.Framework.Logging;
using osu.Framework.Screens;
Centralise and harden editor-ready-for-use check Not only does this combine the check into one location, but it also adds a check on the global `WorkingBeatmap` being updated, which is the only way I can see the following failure happening: ```csharp 05:19:07 osu.Game.Tests: osu.Game.Tests.Visual.Editing.TestSceneEditorBeatmapCreation.TestAddAudioTrack 05:19:07 Failed TestAddAudioTrack [161 ms] 05:19:07 Error Message: 05:19:07 TearDown : System.NullReferenceException : Object reference not set to an instance of an object. 05:19:07 Stack Trace: 05:19:07 --TearDown 05:19:07 at osu.Game.Database.ModelManager`1.<>c__DisplayClass7_0.<AddFile>b__0(TModel managed) in C:\BuildAgent\work\ecd860037212ac52\osu.Game\Database\ModelManager.cs:line 36 05:19:07 at osu.Game.Database.ModelManager`1.<>c__DisplayClass8_0.<performFileOperation>b__0(Realm realm) in C:\BuildAgent\work\ecd860037212ac52\osu.Game\Database\ModelManager.cs:line 49 05:19:07 at osu.Game.Database.RealmExtensions.Write(Realm realm, Action`1 function) in C:\BuildAgent\work\ecd860037212ac52\osu.Game\Database\RealmExtensions.cs:line 14 05:19:07 at osu.Game.Database.ModelManager`1.performFileOperation(TModel item, Action`1 operation) in C:\BuildAgent\work\ecd860037212ac52\osu.Game\Database\ModelManager.cs:line 46 05:19:07 at osu.Game.Database.ModelManager`1.AddFile(TModel item, Stream contents, String filename) in C:\BuildAgent\work\ecd860037212ac52\osu.Game\Database\ModelManager.cs:line 36 05:19:07 at osu.Game.Screens.Edit.Setup.ResourcesSection.ChangeAudioTrack(FileInfo source) in C:\BuildAgent\work\ecd860037212ac52\osu.Game\Screens\Edit\Setup\ResourcesSection.cs:line 115 05:19:07 at osu.Game.Tests.Visual.Editing.TestSceneEditorBeatmapCreation.<TestAddAudioTrack>b__13_0() in C:\BuildAgent\work\ecd860037212ac52\osu.Game.Tests\Visual\Editing\TestSceneEditorBeatmapCreation.cs:line 101 05:19:07 at osu.Framework.Testing.Drawables.Steps.AssertButton.checkAssert() 05:19:07 at osu.Framework.Testing.Drawables.Steps.StepButton.PerformStep(Boolean userTriggered) 05:19:07 at osu.Framework.Testing.TestScene.runNextStep(Action onCompletion, Action`1 onError, Func`2 stopCondition) ```
2022-06-27 15:19:31 +08:00
using osu.Framework.Testing;
using osu.Framework.Threading;
using osu.Framework.Timing;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Configuration;
using osu.Game.Database;
using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface;
2019-06-30 18:31:31 +08:00
using osu.Game.Input.Bindings;
2022-07-20 03:38:23 +08:00
using osu.Game.Localisation;
using osu.Game.Online.API;
using osu.Game.Online.Multiplayer;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
2022-07-20 03:38:23 +08:00
using osu.Game.Overlays.OSD;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Screens.Edit.Components.Menus;
2019-10-09 15:04:58 +08:00
using osu.Game.Screens.Edit.Compose;
Centralise and harden editor-ready-for-use check Not only does this combine the check into one location, but it also adds a check on the global `WorkingBeatmap` being updated, which is the only way I can see the following failure happening: ```csharp 05:19:07 osu.Game.Tests: osu.Game.Tests.Visual.Editing.TestSceneEditorBeatmapCreation.TestAddAudioTrack 05:19:07 Failed TestAddAudioTrack [161 ms] 05:19:07 Error Message: 05:19:07 TearDown : System.NullReferenceException : Object reference not set to an instance of an object. 05:19:07 Stack Trace: 05:19:07 --TearDown 05:19:07 at osu.Game.Database.ModelManager`1.<>c__DisplayClass7_0.<AddFile>b__0(TModel managed) in C:\BuildAgent\work\ecd860037212ac52\osu.Game\Database\ModelManager.cs:line 36 05:19:07 at osu.Game.Database.ModelManager`1.<>c__DisplayClass8_0.<performFileOperation>b__0(Realm realm) in C:\BuildAgent\work\ecd860037212ac52\osu.Game\Database\ModelManager.cs:line 49 05:19:07 at osu.Game.Database.RealmExtensions.Write(Realm realm, Action`1 function) in C:\BuildAgent\work\ecd860037212ac52\osu.Game\Database\RealmExtensions.cs:line 14 05:19:07 at osu.Game.Database.ModelManager`1.performFileOperation(TModel item, Action`1 operation) in C:\BuildAgent\work\ecd860037212ac52\osu.Game\Database\ModelManager.cs:line 46 05:19:07 at osu.Game.Database.ModelManager`1.AddFile(TModel item, Stream contents, String filename) in C:\BuildAgent\work\ecd860037212ac52\osu.Game\Database\ModelManager.cs:line 36 05:19:07 at osu.Game.Screens.Edit.Setup.ResourcesSection.ChangeAudioTrack(FileInfo source) in C:\BuildAgent\work\ecd860037212ac52\osu.Game\Screens\Edit\Setup\ResourcesSection.cs:line 115 05:19:07 at osu.Game.Tests.Visual.Editing.TestSceneEditorBeatmapCreation.<TestAddAudioTrack>b__13_0() in C:\BuildAgent\work\ecd860037212ac52\osu.Game.Tests\Visual\Editing\TestSceneEditorBeatmapCreation.cs:line 101 05:19:07 at osu.Framework.Testing.Drawables.Steps.AssertButton.checkAssert() 05:19:07 at osu.Framework.Testing.Drawables.Steps.StepButton.PerformStep(Boolean userTriggered) 05:19:07 at osu.Framework.Testing.TestScene.runNextStep(Action onCompletion, Action`1 onError, Func`2 stopCondition) ```
2022-06-27 15:19:31 +08:00
using osu.Game.Screens.Edit.Compose.Components.Timeline;
using osu.Game.Screens.Edit.Design;
using osu.Game.Screens.Edit.GameplayTest;
using osu.Game.Screens.Edit.Setup;
using osu.Game.Screens.Edit.Timing;
using osu.Game.Screens.Edit.Verify;
using osu.Game.Screens.OnlinePlay;
2019-12-12 12:04:32 +08:00
using osu.Game.Screens.Play;
using osu.Game.Users;
using osuTK.Input;
2023-01-16 00:37:40 +08:00
using WebCommonStrings = osu.Game.Resources.Localisation.Web.CommonStrings;
2018-04-13 17:19:50 +08:00
namespace osu.Game.Screens.Edit
{
[Cached(typeof(IBeatSnapProvider))]
[Cached]
2022-11-24 13:32:20 +08:00
public partial class Editor : ScreenWithBeatmapBackground, IKeyBindingHandler<GlobalAction>, IKeyBindingHandler<PlatformAction>, IBeatSnapProvider, ISamplePlaybackDisabler, IBeatSyncProvider
2018-04-13 17:19:50 +08:00
{
/// <summary>
/// An offset applied to waveform visuals to align them with expectations.
/// </summary>
/// <remarks>
/// Historically, osu! beatmaps have an assumption of full system latency baked in.
/// This comes from a culmination of stable's platform offset, average hardware playback
/// latency, and users having their universal offsets tweaked to previous beatmaps.
///
/// Coming to this value involved running various tests with existing users / beatmaps.
/// This included both visual and audible comparisons. Ballpark confidence is ≈2 ms.
/// </remarks>
public const float WAVEFORM_VISUAL_OFFSET = 20;
2019-11-06 17:11:56 +08:00
public override float BackgroundParallaxAmount => 0.1f;
2019-06-25 15:55:49 +08:00
public override bool AllowBackButton => false;
public override bool HideOverlaysOnEnter => true;
2019-02-01 14:42:15 +08:00
public override bool DisallowExternalBeatmapRulesetChanges => true;
2018-04-13 17:19:50 +08:00
public override bool? ApplyModTrackAdjustments => false;
protected override bool PlayExitSound => !ExitConfirmed && !switchingDifficulty;
protected bool HasUnsavedChanges
{
get
{
if (!canSave)
return false;
return lastSavedHash != changeHandler?.CurrentStateHash;
}
}
2020-09-09 18:42:03 +08:00
2020-01-10 18:57:34 +08:00
[Resolved]
private BeatmapManager beatmapManager { get; set; }
2022-01-23 23:34:02 +08:00
[Resolved]
private RulesetStore rulesets { get; set; }
[Resolved(canBeNull: true)]
private IDialogOverlay dialogOverlay { get; set; }
[Resolved(canBeNull: true)]
private INotificationOverlay notifications { get; set; }
[Resolved]
private RealmAccess realm { get; set; }
public readonly Bindable<EditorScreenMode> Mode = new Bindable<EditorScreenMode>();
public IBindable<bool> SamplePlaybackDisabled => samplePlaybackDisabled;
Centralise and harden editor-ready-for-use check Not only does this combine the check into one location, but it also adds a check on the global `WorkingBeatmap` being updated, which is the only way I can see the following failure happening: ```csharp 05:19:07 osu.Game.Tests: osu.Game.Tests.Visual.Editing.TestSceneEditorBeatmapCreation.TestAddAudioTrack 05:19:07 Failed TestAddAudioTrack [161 ms] 05:19:07 Error Message: 05:19:07 TearDown : System.NullReferenceException : Object reference not set to an instance of an object. 05:19:07 Stack Trace: 05:19:07 --TearDown 05:19:07 at osu.Game.Database.ModelManager`1.<>c__DisplayClass7_0.<AddFile>b__0(TModel managed) in C:\BuildAgent\work\ecd860037212ac52\osu.Game\Database\ModelManager.cs:line 36 05:19:07 at osu.Game.Database.ModelManager`1.<>c__DisplayClass8_0.<performFileOperation>b__0(Realm realm) in C:\BuildAgent\work\ecd860037212ac52\osu.Game\Database\ModelManager.cs:line 49 05:19:07 at osu.Game.Database.RealmExtensions.Write(Realm realm, Action`1 function) in C:\BuildAgent\work\ecd860037212ac52\osu.Game\Database\RealmExtensions.cs:line 14 05:19:07 at osu.Game.Database.ModelManager`1.performFileOperation(TModel item, Action`1 operation) in C:\BuildAgent\work\ecd860037212ac52\osu.Game\Database\ModelManager.cs:line 46 05:19:07 at osu.Game.Database.ModelManager`1.AddFile(TModel item, Stream contents, String filename) in C:\BuildAgent\work\ecd860037212ac52\osu.Game\Database\ModelManager.cs:line 36 05:19:07 at osu.Game.Screens.Edit.Setup.ResourcesSection.ChangeAudioTrack(FileInfo source) in C:\BuildAgent\work\ecd860037212ac52\osu.Game\Screens\Edit\Setup\ResourcesSection.cs:line 115 05:19:07 at osu.Game.Tests.Visual.Editing.TestSceneEditorBeatmapCreation.<TestAddAudioTrack>b__13_0() in C:\BuildAgent\work\ecd860037212ac52\osu.Game.Tests\Visual\Editing\TestSceneEditorBeatmapCreation.cs:line 101 05:19:07 at osu.Framework.Testing.Drawables.Steps.AssertButton.checkAssert() 05:19:07 at osu.Framework.Testing.Drawables.Steps.StepButton.PerformStep(Boolean userTriggered) 05:19:07 at osu.Framework.Testing.TestScene.runNextStep(Action onCompletion, Action`1 onError, Func`2 stopCondition) ```
2022-06-27 15:19:31 +08:00
/// <summary>
/// Ensure all asynchronously loading pieces of the editor are in a good state.
/// This exists here for convenience for tests, not for actual use.
/// Eventually we'd probably want a better way to signal this.
/// </summary>
public bool ReadyForUse
{
get
{
if (!workingBeatmapUpdated)
return false;
if (currentScreen?.IsLoaded != true)
Centralise and harden editor-ready-for-use check Not only does this combine the check into one location, but it also adds a check on the global `WorkingBeatmap` being updated, which is the only way I can see the following failure happening: ```csharp 05:19:07 osu.Game.Tests: osu.Game.Tests.Visual.Editing.TestSceneEditorBeatmapCreation.TestAddAudioTrack 05:19:07 Failed TestAddAudioTrack [161 ms] 05:19:07 Error Message: 05:19:07 TearDown : System.NullReferenceException : Object reference not set to an instance of an object. 05:19:07 Stack Trace: 05:19:07 --TearDown 05:19:07 at osu.Game.Database.ModelManager`1.<>c__DisplayClass7_0.<AddFile>b__0(TModel managed) in C:\BuildAgent\work\ecd860037212ac52\osu.Game\Database\ModelManager.cs:line 36 05:19:07 at osu.Game.Database.ModelManager`1.<>c__DisplayClass8_0.<performFileOperation>b__0(Realm realm) in C:\BuildAgent\work\ecd860037212ac52\osu.Game\Database\ModelManager.cs:line 49 05:19:07 at osu.Game.Database.RealmExtensions.Write(Realm realm, Action`1 function) in C:\BuildAgent\work\ecd860037212ac52\osu.Game\Database\RealmExtensions.cs:line 14 05:19:07 at osu.Game.Database.ModelManager`1.performFileOperation(TModel item, Action`1 operation) in C:\BuildAgent\work\ecd860037212ac52\osu.Game\Database\ModelManager.cs:line 46 05:19:07 at osu.Game.Database.ModelManager`1.AddFile(TModel item, Stream contents, String filename) in C:\BuildAgent\work\ecd860037212ac52\osu.Game\Database\ModelManager.cs:line 36 05:19:07 at osu.Game.Screens.Edit.Setup.ResourcesSection.ChangeAudioTrack(FileInfo source) in C:\BuildAgent\work\ecd860037212ac52\osu.Game\Screens\Edit\Setup\ResourcesSection.cs:line 115 05:19:07 at osu.Game.Tests.Visual.Editing.TestSceneEditorBeatmapCreation.<TestAddAudioTrack>b__13_0() in C:\BuildAgent\work\ecd860037212ac52\osu.Game.Tests\Visual\Editing\TestSceneEditorBeatmapCreation.cs:line 101 05:19:07 at osu.Framework.Testing.Drawables.Steps.AssertButton.checkAssert() 05:19:07 at osu.Framework.Testing.Drawables.Steps.StepButton.PerformStep(Boolean userTriggered) 05:19:07 at osu.Framework.Testing.TestScene.runNextStep(Action onCompletion, Action`1 onError, Func`2 stopCondition) ```
2022-06-27 15:19:31 +08:00
return false;
if (currentScreen is EditorScreenWithTimeline)
return currentScreen.ChildrenOfType<TimelineArea>().FirstOrDefault()?.IsLoaded == true;
Centralise and harden editor-ready-for-use check Not only does this combine the check into one location, but it also adds a check on the global `WorkingBeatmap` being updated, which is the only way I can see the following failure happening: ```csharp 05:19:07 osu.Game.Tests: osu.Game.Tests.Visual.Editing.TestSceneEditorBeatmapCreation.TestAddAudioTrack 05:19:07 Failed TestAddAudioTrack [161 ms] 05:19:07 Error Message: 05:19:07 TearDown : System.NullReferenceException : Object reference not set to an instance of an object. 05:19:07 Stack Trace: 05:19:07 --TearDown 05:19:07 at osu.Game.Database.ModelManager`1.<>c__DisplayClass7_0.<AddFile>b__0(TModel managed) in C:\BuildAgent\work\ecd860037212ac52\osu.Game\Database\ModelManager.cs:line 36 05:19:07 at osu.Game.Database.ModelManager`1.<>c__DisplayClass8_0.<performFileOperation>b__0(Realm realm) in C:\BuildAgent\work\ecd860037212ac52\osu.Game\Database\ModelManager.cs:line 49 05:19:07 at osu.Game.Database.RealmExtensions.Write(Realm realm, Action`1 function) in C:\BuildAgent\work\ecd860037212ac52\osu.Game\Database\RealmExtensions.cs:line 14 05:19:07 at osu.Game.Database.ModelManager`1.performFileOperation(TModel item, Action`1 operation) in C:\BuildAgent\work\ecd860037212ac52\osu.Game\Database\ModelManager.cs:line 46 05:19:07 at osu.Game.Database.ModelManager`1.AddFile(TModel item, Stream contents, String filename) in C:\BuildAgent\work\ecd860037212ac52\osu.Game\Database\ModelManager.cs:line 36 05:19:07 at osu.Game.Screens.Edit.Setup.ResourcesSection.ChangeAudioTrack(FileInfo source) in C:\BuildAgent\work\ecd860037212ac52\osu.Game\Screens\Edit\Setup\ResourcesSection.cs:line 115 05:19:07 at osu.Game.Tests.Visual.Editing.TestSceneEditorBeatmapCreation.<TestAddAudioTrack>b__13_0() in C:\BuildAgent\work\ecd860037212ac52\osu.Game.Tests\Visual\Editing\TestSceneEditorBeatmapCreation.cs:line 101 05:19:07 at osu.Framework.Testing.Drawables.Steps.AssertButton.checkAssert() 05:19:07 at osu.Framework.Testing.Drawables.Steps.StepButton.PerformStep(Boolean userTriggered) 05:19:07 at osu.Framework.Testing.TestScene.runNextStep(Action onCompletion, Action`1 onError, Func`2 stopCondition) ```
2022-06-27 15:19:31 +08:00
return true;
}
}
private bool workingBeatmapUpdated;
private readonly Bindable<bool> samplePlaybackDisabled = new Bindable<bool>();
private bool canSave;
private readonly List<MenuItem> saveRelatedMenuItems = new List<MenuItem>();
/// <summary>
/// Tracks ongoing mutually-exclusive operations related to changing the beatmap
/// (e.g. save, export).
/// </summary>
public OngoingOperationTracker MutationTracker { get; } = new OngoingOperationTracker();
protected bool ExitConfirmed { get; private set; }
private bool switchingDifficulty;
private string lastSavedHash;
private Container<EditorScreen> screenContainer;
2018-04-13 17:19:50 +08:00
[CanBeNull]
private readonly EditorLoader loader;
2018-04-13 17:19:50 +08:00
private EditorScreen currentScreen;
private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor();
private EditorClock clock;
private IBeatmap playableBeatmap;
private EditorBeatmap editorBeatmap;
2022-05-25 22:10:58 +08:00
private BottomBar bottomBar;
[CanBeNull] // Should be non-null once it can support custom rulesets.
private EditorChangeHandler changeHandler;
2018-04-13 17:19:50 +08:00
private DependencyContainer dependencies;
private bool isNewBeatmap;
2023-02-13 05:11:55 +08:00
protected override UserActivity InitialActivity
{
get
{
if (Beatmap.Value.Metadata.Author.OnlineID == api.LocalUser.Value.OnlineID)
return new UserActivity.EditingBeatmap(Beatmap.Value.BeatmapInfo);
return new UserActivity.ModdingBeatmap(Beatmap.Value.BeatmapInfo);
}
}
2018-07-11 16:07:14 +08:00
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
=> dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
2018-04-13 17:19:50 +08:00
[Resolved]
private IAPIProvider api { get; set; }
[Cached]
public readonly EditorClipboard Clipboard = new EditorClipboard();
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
2022-07-20 03:38:23 +08:00
[Resolved(canBeNull: true)]
private OnScreenDisplay onScreenDisplay { get; set; }
private Bindable<float> editorBackgroundDim;
private Bindable<bool> editorHitMarkers;
private Bindable<bool> editorAutoSeekOnPlacement;
private Bindable<bool> editorLimitedDistanceSnap;
private Bindable<bool> editorTimelineShowTimingChanges;
private Bindable<bool> editorTimelineShowTicks;
/// <summary>
/// This controls the opacity of components like the timelines, sidebars, etc.
/// In "composer focus" mode the opacity of the aforementioned components is reduced so that the user can focus on the composer better.
/// </summary>
/// <remarks>
/// The state of this bindable is controlled by <see cref="HitObjectComposer"/> when in <see cref="EditorScreenMode.Compose"/> mode.
/// </remarks>
public Bindable<bool> ComposerFocusMode { get; } = new Bindable<bool>();
public Editor(EditorLoader loader = null)
{
this.loader = loader;
}
2018-04-13 17:19:50 +08:00
[BackgroundDependencyLoader]
2022-05-25 22:10:58 +08:00
private void load(OsuConfigManager config)
2018-04-13 17:19:50 +08:00
{
var loadableBeatmap = Beatmap.Value;
if (loadableBeatmap is DummyWorkingBeatmap)
{
Logger.Log("Editor was loaded without a valid beatmap; creating a new beatmap.");
isNewBeatmap = true;
loadableBeatmap = beatmapManager.CreateNew(Ruleset.Value, api.LocalUser.Value);
// required so we can get the track length in EditorClock.
// this is ONLY safe because the track being provided is a `TrackVirtual` which we don't really care about disposing.
loadableBeatmap.LoadTrack();
// this is a bit haphazard, but guards against setting the lease Beatmap bindable if
// the editor has already been exited.
if (!ValidForPush)
2023-04-28 22:45:00 +08:00
{
beatmapManager.Delete(loadableBeatmap.BeatmapSetInfo);
return;
2023-04-28 22:45:00 +08:00
}
}
try
{
playableBeatmap = loadableBeatmap.GetPlayableBeatmap(loadableBeatmap.BeatmapInfo.Ruleset);
2021-01-25 17:29:00 +08:00
// clone these locally for now to avoid incurring overhead on GetPlayableBeatmap usages.
// eventually we will want to improve how/where this is done as there are issues with *not* cloning it in all cases.
playableBeatmap.ControlPointInfo = playableBeatmap.ControlPointInfo.DeepClone();
}
catch (Exception e)
{
Logger.Error(e, "Could not load beatmap successfully!");
// couldn't load, hard abort!
this.Exit();
return;
}
// Todo: should probably be done at a DrawableRuleset level to share logic with Player.
clock = new EditorClock(playableBeatmap, beatDivisor);
clock.ChangeSource(loadableBeatmap.Track);
dependencies.CacheAs(clock);
AddInternal(clock);
clock.SeekingOrStopped.BindValueChanged(_ => updateSampleDisabledState());
// todo: remove caching of this and consume via editorBeatmap?
dependencies.Cache(beatDivisor);
AddInternal(editorBeatmap = new EditorBeatmap(playableBeatmap, loadableBeatmap.GetSkin(), loadableBeatmap.BeatmapInfo));
dependencies.CacheAs(editorBeatmap);
editorBeatmap.UpdateInProgress.BindValueChanged(_ => updateSampleDisabledState());
canSave = editorBeatmap.BeatmapInfo.Ruleset.CreateInstance() is ILegacyRuleset;
if (canSave)
{
changeHandler = new BeatmapEditorChangeHandler(editorBeatmap);
dependencies.CacheAs<IEditorChangeHandler>(changeHandler);
}
beatDivisor.SetArbitraryDivisor(editorBeatmap.BeatmapInfo.BeatDivisor);
2022-01-25 16:43:16 +08:00
beatDivisor.BindValueChanged(divisor => editorBeatmap.BeatmapInfo.BeatDivisor = divisor.NewValue);
updateLastSavedHash();
Schedule(() =>
{
// we need to avoid changing the beatmap from an asynchronous load thread. it can potentially cause weirdness including crashes.
// this assumes that nothing during the rest of this load() method is accessing Beatmap.Value (loadableBeatmap should be preferred).
// generally this is quite safe, as the actual load of editor content comes after menuBar.Mode.ValueChanged is fired in its own LoadComplete.
Beatmap.Value = loadableBeatmap;
Centralise and harden editor-ready-for-use check Not only does this combine the check into one location, but it also adds a check on the global `WorkingBeatmap` being updated, which is the only way I can see the following failure happening: ```csharp 05:19:07 osu.Game.Tests: osu.Game.Tests.Visual.Editing.TestSceneEditorBeatmapCreation.TestAddAudioTrack 05:19:07 Failed TestAddAudioTrack [161 ms] 05:19:07 Error Message: 05:19:07 TearDown : System.NullReferenceException : Object reference not set to an instance of an object. 05:19:07 Stack Trace: 05:19:07 --TearDown 05:19:07 at osu.Game.Database.ModelManager`1.<>c__DisplayClass7_0.<AddFile>b__0(TModel managed) in C:\BuildAgent\work\ecd860037212ac52\osu.Game\Database\ModelManager.cs:line 36 05:19:07 at osu.Game.Database.ModelManager`1.<>c__DisplayClass8_0.<performFileOperation>b__0(Realm realm) in C:\BuildAgent\work\ecd860037212ac52\osu.Game\Database\ModelManager.cs:line 49 05:19:07 at osu.Game.Database.RealmExtensions.Write(Realm realm, Action`1 function) in C:\BuildAgent\work\ecd860037212ac52\osu.Game\Database\RealmExtensions.cs:line 14 05:19:07 at osu.Game.Database.ModelManager`1.performFileOperation(TModel item, Action`1 operation) in C:\BuildAgent\work\ecd860037212ac52\osu.Game\Database\ModelManager.cs:line 46 05:19:07 at osu.Game.Database.ModelManager`1.AddFile(TModel item, Stream contents, String filename) in C:\BuildAgent\work\ecd860037212ac52\osu.Game\Database\ModelManager.cs:line 36 05:19:07 at osu.Game.Screens.Edit.Setup.ResourcesSection.ChangeAudioTrack(FileInfo source) in C:\BuildAgent\work\ecd860037212ac52\osu.Game\Screens\Edit\Setup\ResourcesSection.cs:line 115 05:19:07 at osu.Game.Tests.Visual.Editing.TestSceneEditorBeatmapCreation.<TestAddAudioTrack>b__13_0() in C:\BuildAgent\work\ecd860037212ac52\osu.Game.Tests\Visual\Editing\TestSceneEditorBeatmapCreation.cs:line 101 05:19:07 at osu.Framework.Testing.Drawables.Steps.AssertButton.checkAssert() 05:19:07 at osu.Framework.Testing.Drawables.Steps.StepButton.PerformStep(Boolean userTriggered) 05:19:07 at osu.Framework.Testing.TestScene.runNextStep(Action onCompletion, Action`1 onError, Func`2 stopCondition) ```
2022-06-27 15:19:31 +08:00
workingBeatmapUpdated = true;
});
OsuMenuItem undoMenuItem;
OsuMenuItem redoMenuItem;
2018-04-13 17:19:50 +08:00
editorBackgroundDim = config.GetBindable<float>(OsuSetting.EditorDim);
editorHitMarkers = config.GetBindable<bool>(OsuSetting.EditorShowHitMarkers);
editorAutoSeekOnPlacement = config.GetBindable<bool>(OsuSetting.EditorAutoSeekOnPlacement);
editorLimitedDistanceSnap = config.GetBindable<bool>(OsuSetting.EditorLimitedDistanceSnap);
editorTimelineShowTimingChanges = config.GetBindable<bool>(OsuSetting.EditorTimelineShowTimingChanges);
editorTimelineShowTicks = config.GetBindable<bool>(OsuSetting.EditorTimelineShowTicks);
AddInternal(new OsuContextMenuContainer
2018-04-13 17:19:50 +08:00
{
RelativeSizeAxes = Axes.Both,
2022-05-25 22:10:58 +08:00
Children = new Drawable[]
2018-04-13 17:19:50 +08:00
{
new Container
2018-04-13 17:19:50 +08:00
{
Name = "Screen container",
2018-04-13 17:19:50 +08:00
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Top = 40, Bottom = 40 },
Child = screenContainer = new Container<EditorScreen>
{
RelativeSizeAxes = Axes.Both,
}
},
new Container
2018-04-13 17:19:50 +08:00
{
Name = "Top bar",
RelativeSizeAxes = Axes.X,
Height = 40,
Children = new Drawable[]
2018-04-13 17:19:50 +08:00
{
new EditorMenuBar
2018-04-13 17:19:50 +08:00
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.Both,
MaxHeight = 600,
Items = new[]
{
2023-01-16 00:37:40 +08:00
new MenuItem(CommonStrings.MenuBarFile)
{
Items = createFileMenuItems().ToList()
},
2023-01-16 00:37:40 +08:00
new MenuItem(CommonStrings.MenuBarEdit)
{
Items = new[]
{
2023-01-16 00:37:40 +08:00
undoMenuItem = new EditorMenuItem(CommonStrings.Undo, MenuItemType.Standard, Undo),
redoMenuItem = new EditorMenuItem(CommonStrings.Redo, MenuItemType.Standard, Redo),
new OsuMenuItemSpacer(),
2023-01-16 00:37:40 +08:00
cutMenuItem = new EditorMenuItem(CommonStrings.Cut, MenuItemType.Standard, Cut),
copyMenuItem = new EditorMenuItem(CommonStrings.Copy, MenuItemType.Standard, Copy),
pasteMenuItem = new EditorMenuItem(CommonStrings.Paste, MenuItemType.Standard, Paste),
cloneMenuItem = new EditorMenuItem(CommonStrings.Clone, MenuItemType.Standard, Clone),
}
},
2023-01-16 00:37:40 +08:00
new MenuItem(CommonStrings.MenuBarView)
{
2024-06-17 16:54:52 +08:00
Items = new[]
{
new MenuItem(EditorStrings.Timeline)
{
Items =
[
new WaveformOpacityMenuItem(config.GetBindable<float>(OsuSetting.EditorWaveformOpacity)),
new ToggleMenuItem(EditorStrings.TimelineShowTimingChanges)
{
State = { BindTarget = editorTimelineShowTimingChanges }
},
new ToggleMenuItem(EditorStrings.TimelineShowTicks)
{
State = { BindTarget = editorTimelineShowTicks }
},
]
},
new BackgroundDimMenuItem(editorBackgroundDim),
2023-01-15 06:50:41 +08:00
new ToggleMenuItem(EditorStrings.ShowHitMarkers)
{
State = { BindTarget = editorHitMarkers },
},
new ToggleMenuItem(EditorStrings.AutoSeekOnPlacement)
{
State = { BindTarget = editorAutoSeekOnPlacement },
},
new ToggleMenuItem(EditorStrings.LimitedDistanceSnap)
{
State = { BindTarget = editorLimitedDistanceSnap },
}
}
2022-12-16 00:03:30 +08:00
},
2023-01-15 06:50:41 +08:00
new MenuItem(EditorStrings.Timing)
2022-12-16 00:03:30 +08:00
{
Items = new MenuItem[]
{
2023-01-15 06:50:41 +08:00
new EditorMenuItem(EditorStrings.SetPreviewPointToCurrent, MenuItemType.Standard, SetPreviewPointToCurrentTime)
2022-12-16 00:03:30 +08:00
}
}
}
},
screenSwitcher = new EditorScreenSwitcherControl
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
X = -10,
Current = Mode,
},
},
},
2022-05-25 22:10:58 +08:00
bottomBar = new BottomBar(),
MutationTracker,
}
});
changeHandler?.CanUndo.BindValueChanged(v => undoMenuItem.Action.Disabled = !v.NewValue, true);
changeHandler?.CanRedo.BindValueChanged(v => redoMenuItem.Action.Disabled = !v.NewValue, true);
editorBackgroundDim.BindValueChanged(_ => dimBackground());
2018-04-13 17:19:50 +08:00
}
[Resolved]
private MusicController musicController { get; set; }
protected override void LoadComplete()
{
base.LoadComplete();
setUpClipboardActionAvailability();
Mode.Value = isNewBeatmap ? EditorScreenMode.SongSetup : EditorScreenMode.Compose;
Mode.BindValueChanged(onModeChanged, true);
musicController.TrackChanged += onTrackChanged;
MutationTracker.InProgress.BindValueChanged(_ =>
{
foreach (var item in saveRelatedMenuItems)
item.Action.Disabled = MutationTracker.InProgress.Value;
}, true);
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
musicController.TrackChanged -= onTrackChanged;
}
private void onTrackChanged(WorkingBeatmap working, TrackChangeDirection direction) => clock.ChangeSource(working.Track);
/// <summary>
/// Creates an <see cref="EditorState"/> instance representing the current state of the editor.
/// </summary>
/// <param name="nextRuleset">
/// The ruleset of the next beatmap to be shown, in the case of difficulty switch.
/// <see langword="null"/> indicates that the beatmap will not be changing.
/// </param>
public EditorState GetState([CanBeNull] RulesetInfo nextRuleset = null) => new EditorState
{
Time = clock.CurrentTimeAccurate,
ClipboardContent = nextRuleset == null || editorBeatmap.BeatmapInfo.Ruleset.ShortName == nextRuleset.ShortName ? Clipboard.Content.Value : string.Empty
};
/// <summary>
/// Restore the editor to a provided state.
/// </summary>
/// <param name="state">The state to restore.</param>
public void RestoreState([NotNull] EditorState state) => Schedule(() =>
{
clock.Seek(state.Time);
Clipboard.Content.Value = state.ClipboardContent;
});
2022-05-25 22:10:58 +08:00
public void TestGameplay()
{
if (HasUnsavedChanges)
{
2024-07-11 11:15:17 +08:00
dialogOverlay.Push(new SaveRequiredPopupDialog(() => attemptMutationOperation(() =>
2022-05-25 22:10:58 +08:00
{
if (!Save()) return false;
pushEditorPlayer();
return true;
})));
2022-05-25 22:10:58 +08:00
}
else
{
pushEditorPlayer();
}
void pushEditorPlayer() => this.Push(new EditorPlayerLoader(this));
}
private bool attemptMutationOperation(Func<bool> mutationOperation)
{
if (MutationTracker.InProgress.Value)
return false;
using (MutationTracker.BeginOperation())
return mutationOperation.Invoke();
}
private bool attemptAsyncMutationOperation(Func<Task> mutationTask)
{
if (MutationTracker.InProgress.Value)
return false;
var operation = MutationTracker.BeginOperation();
var task = mutationTask.Invoke();
task.FireAndForget(operation.Dispose, _ => operation.Dispose());
return true;
}
/// <summary>
/// Saves the currently edited beatmap.
/// </summary>
/// <returns>Whether the save was successful.</returns>
internal bool Save()
2020-09-09 18:57:28 +08:00
{
if (!canSave)
{
notifications?.Post(new SimpleErrorNotification { Text = "Saving is not supported for this ruleset yet, sorry!" });
return false;
}
try
{
// save the loaded beatmap's data stream.
beatmapManager.Save(editorBeatmap.BeatmapInfo, editorBeatmap.PlayableBeatmap, editorBeatmap.BeatmapSkin);
}
catch (Exception ex)
{
// can fail e.g. due to duplicated difficulty names.
Logger.Error(ex, ex.Message);
return false;
}
// no longer new after first user-triggered save.
isNewBeatmap = false;
2020-09-09 18:57:28 +08:00
updateLastSavedHash();
2022-07-22 08:46:17 +08:00
onScreenDisplay?.Display(new BeatmapEditorToast(ToastStrings.BeatmapSaved, editorBeatmap.BeatmapInfo.GetDisplayTitle()));
return true;
2020-09-09 18:57:28 +08:00
}
protected override void Update()
{
base.Update();
clock.ProcessFrame();
}
2021-09-16 17:26:12 +08:00
public bool OnPressed(KeyBindingPressEvent<PlatformAction> e)
{
2021-09-16 17:26:12 +08:00
switch (e.Action)
{
2021-07-20 13:23:34 +08:00
case PlatformAction.Cut:
2020-09-11 18:55:41 +08:00
Cut();
return true;
2021-07-20 13:23:34 +08:00
case PlatformAction.Copy:
2020-09-11 18:55:41 +08:00
Copy();
return true;
2021-07-20 13:23:34 +08:00
case PlatformAction.Paste:
2020-09-11 18:55:41 +08:00
Paste();
return true;
2021-07-20 13:23:34 +08:00
case PlatformAction.Undo:
2020-04-22 17:14:21 +08:00
Undo();
return true;
2021-07-20 13:23:34 +08:00
case PlatformAction.Redo:
2020-04-22 17:14:21 +08:00
Redo();
return true;
2021-07-20 13:23:34 +08:00
case PlatformAction.Save:
if (e.Repeat)
return false;
return attemptMutationOperation(Save);
}
return false;
}
2021-09-16 17:26:12 +08:00
public void OnReleased(KeyBindingReleaseEvent<PlatformAction> e)
{
}
protected override bool OnKeyDown(KeyDownEvent e)
2018-04-13 17:19:50 +08:00
{
if (e.ControlPressed || e.AltPressed || e.SuperPressed) return false;
switch (e.Key)
2018-04-13 17:19:50 +08:00
{
case Key.Left:
seek(e, -1);
return true;
2019-04-01 11:16:05 +08:00
case Key.Right:
seek(e, 1);
return true;
// Of those, these two keys are reversed from stable because it feels more natural (and matches mouse wheel scroll directionality).
case Key.Up:
seekControlPoint(-1);
return true;
case Key.Down:
seekControlPoint(1);
return true;
// Track traversal keys.
// Matching osu-stable implementations.
case Key.Z:
if (e.Repeat)
return false;
// Seek to first object time, or track start if already there.
double? firstObjectTime = editorBeatmap.HitObjects.FirstOrDefault()?.StartTime;
if (firstObjectTime == null || clock.CurrentTime == firstObjectTime)
clock.Seek(0);
else
clock.Seek(firstObjectTime.Value);
return true;
case Key.X:
if (e.Repeat)
return false;
// Restart playback from beginning of track.
clock.Seek(0);
clock.Start();
return true;
case Key.C:
if (e.Repeat)
return false;
// Pause or resume.
if (clock.IsRunning)
clock.Stop();
else
clock.Start();
return true;
case Key.V:
if (e.Repeat)
return false;
// Seek to last object time, or track end if already there.
// Note that in osu-stable subsequent presses when at track end won't return to last object.
// This has intentionally been changed to make it more useful.
if (!editorBeatmap.HitObjects.Any())
{
clock.Seek(clock.TrackLength);
return true;
}
double lastObjectTime = editorBeatmap.GetLastObjectTime();
clock.Seek(clock.CurrentTime == lastObjectTime ? clock.TrackLength : lastObjectTime);
return true;
2018-04-13 17:19:50 +08:00
}
return base.OnKeyDown(e);
2018-04-13 17:19:50 +08:00
}
private double scrollAccumulation;
2018-10-02 11:02:47 +08:00
protected override bool OnScroll(ScrollEvent e)
2018-04-13 17:19:50 +08:00
{
if (e.ControlPressed || e.AltPressed || e.SuperPressed)
return false;
const double precision = 1;
double scrollComponent = e.ScrollDelta.X + e.ScrollDelta.Y;
double scrollDirection = Math.Sign(scrollComponent);
// this is a special case to handle the "pivot" scenario.
// if we are precise scrolling in one direction then change our mind and scroll backwards,
// the existing accumulation should be applied in the inverse direction to maintain responsiveness.
if (scrollAccumulation != 0 && Math.Sign(scrollAccumulation) != scrollDirection)
scrollAccumulation = scrollDirection * (precision - Math.Abs(scrollAccumulation));
scrollAccumulation += scrollComponent;
// because we are doing snapped seeking, we need to add up precise scrolls until they accumulate to an arbitrary cut-off.
while (Math.Abs(scrollAccumulation) >= precision)
{
if (scrollAccumulation > 0)
seek(e, -1);
else
seek(e, 1);
scrollAccumulation = scrollAccumulation < 0 ? Math.Min(0, scrollAccumulation + precision) : Math.Max(0, scrollAccumulation - precision);
}
2018-04-13 17:19:50 +08:00
return true;
}
2021-09-16 17:26:12 +08:00
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
2019-06-30 18:31:31 +08:00
{
if (e.Repeat)
return false;
2021-09-16 17:26:12 +08:00
switch (e.Action)
2019-06-30 18:31:31 +08:00
{
case GlobalAction.Back:
// as we don't want to display the back button, manual handling of exit action is required.
this.Exit();
return true;
2019-06-30 18:31:31 +08:00
case GlobalAction.EditorCloneSelection:
Clone();
return true;
case GlobalAction.EditorComposeMode:
screenSwitcher.SelectItem(EditorScreenMode.Compose);
return true;
case GlobalAction.EditorDesignMode:
screenSwitcher.SelectItem(EditorScreenMode.Design);
return true;
case GlobalAction.EditorTimingMode:
screenSwitcher.SelectItem(EditorScreenMode.Timing);
return true;
case GlobalAction.EditorSetupMode:
screenSwitcher.SelectItem(EditorScreenMode.SongSetup);
return true;
case GlobalAction.EditorVerifyMode:
screenSwitcher.SelectItem(EditorScreenMode.Verify);
return true;
case GlobalAction.EditorTestGameplay:
2022-05-25 22:10:58 +08:00
bottomBar.TestGameplayButton.TriggerClick();
return true;
default:
return false;
}
2019-06-30 18:31:31 +08:00
}
2021-09-16 17:26:12 +08:00
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
{
}
2019-06-30 18:31:31 +08:00
public override void OnEntering(ScreenTransitionEvent e)
2018-04-13 17:19:50 +08:00
{
base.OnEntering(e);
dimBackground();
resetTrack(true);
}
public override void OnResuming(ScreenTransitionEvent e)
{
base.OnResuming(e);
dimBackground();
}
2019-07-10 23:22:40 +08:00
private void dimBackground()
{
ApplyToBackground(b =>
{
b.IgnoreUserSettings.Value = true;
2022-11-02 16:49:52 +08:00
b.DimWhenUserSettingsIgnored.Value = editorBackgroundDim.Value;
b.BlurAmount.Value = 0;
});
2018-04-13 17:19:50 +08:00
}
public override bool OnExiting(ScreenExitEvent e)
2018-04-13 17:19:50 +08:00
{
2024-05-24 16:24:50 +08:00
currentScreen?.OnExiting(e);
if (!ExitConfirmed)
{
// dialog overlay may not be available in visual tests.
if (dialogOverlay == null)
{
confirmExit();
return true;
}
// if the dialog is already displayed, block exiting until the user explicitly makes a decision.
if (dialogOverlay.CurrentDialog is PromptForSaveDialog saveDialog)
{
saveDialog.Flash();
return true;
}
if (isNewBeatmap || HasUnsavedChanges)
{
updateSampleDisabledState();
2021-09-07 03:27:17 +08:00
dialogOverlay?.Push(new PromptForSaveDialog(confirmExit, confirmExitWithSave, cancelExit));
return true;
}
}
realm.Write(r =>
{
var beatmap = r.Find<BeatmapInfo>(editorBeatmap.BeatmapInfo.ID);
if (beatmap != null)
beatmap.EditorTimestamp = clock.CurrentTime;
});
ApplyToBackground(b =>
{
2022-11-02 16:49:52 +08:00
b.DimWhenUserSettingsIgnored.Value = 0;
});
2022-11-02 17:05:22 +08:00
2019-07-10 23:22:40 +08:00
resetTrack();
2019-07-10 16:43:02 +08:00
refetchBeatmap();
return base.OnExiting(e);
}
public override void OnSuspending(ScreenTransitionEvent e)
{
base.OnSuspending(e);
clock.Stop();
refetchBeatmap();
}
private void refetchBeatmap()
{
// To update the game-wide beatmap with any changes, perform a re-fetch on exit/suspend.
// This is required as the editor makes its local changes via EditorBeatmap
// (which are not propagated outwards to a potentially cached WorkingBeatmap).
var refetchedBeatmap = beatmapManager.GetWorkingBeatmap(Beatmap.Value.BeatmapInfo, true);
if (!(refetchedBeatmap is DummyWorkingBeatmap))
{
2023-01-15 06:50:41 +08:00
Logger.Log(@"Editor providing re-fetched beatmap post edit session");
Beatmap.Value = refetchedBeatmap;
}
2018-04-13 17:19:50 +08:00
}
2019-07-10 23:22:40 +08:00
private void confirmExitWithSave()
{
if (!attemptMutationOperation(Save))
return;
ExitConfirmed = true;
this.Exit();
}
private void confirmExit()
{
// stop the track if playing to allow the parent screen to choose a suitable playback mode.
Beatmap.Value.Track.Stop();
if (isNewBeatmap)
{
// confirming exit without save means we should delete the new beatmap completely.
2022-01-12 21:34:07 +08:00
if (playableBeatmap.BeatmapInfo.BeatmapSet != null)
beatmapManager.Delete(playableBeatmap.BeatmapInfo.BeatmapSet);
// eagerly clear contents before restoring default beatmap to prevent value change callbacks from firing.
ClearInternal();
// in theory this shouldn't be required but due to EF core not sharing instance states 100%
// MusicController is unaware of the changed DeletePending state.
Beatmap.SetDefault();
}
ExitConfirmed = true;
this.Exit();
}
#region Clipboard support
private EditorMenuItem cutMenuItem;
private EditorMenuItem copyMenuItem;
private EditorMenuItem cloneMenuItem;
private EditorMenuItem pasteMenuItem;
private readonly BindableWithCurrent<bool> canCut = new BindableWithCurrent<bool>();
private readonly BindableWithCurrent<bool> canCopy = new BindableWithCurrent<bool>();
private readonly BindableWithCurrent<bool> canPaste = new BindableWithCurrent<bool>();
private void setUpClipboardActionAvailability()
{
canCut.Current.BindValueChanged(cut => cutMenuItem.Action.Disabled = !cut.NewValue, true);
canCopy.Current.BindValueChanged(copy =>
{
copyMenuItem.Action.Disabled = !copy.NewValue;
cloneMenuItem.Action.Disabled = !copy.NewValue;
}, true);
canPaste.Current.BindValueChanged(paste => pasteMenuItem.Action.Disabled = !paste.NewValue, true);
}
private void rebindClipboardBindables()
{
canCut.Current = currentScreen.CanCut;
canCopy.Current = currentScreen.CanCopy;
canPaste.Current = currentScreen.CanPaste;
}
protected void Cut() => currentScreen?.Cut();
protected void Copy() => currentScreen?.Copy();
protected void Clone()
{
// Avoid attempting to clone if copying is not available (as it may result in pasting something unexpected).
if (!canCopy.Value)
return;
// This is an initial implementation just to get an idea of how people used this function.
// There are a couple of differences from osu!stable's implementation which will require more work to match:
// - The "clipboard" is not populated during the duplication process.
// - The duplicated hitobjects are inserted after the original pattern (add one beat_length and then quantize using beat snap).
// - The duplicated hitobjects are selected (but this is also applied for all paste operations so should be changed there).
Copy();
Paste();
}
protected void Paste() => currentScreen?.Paste();
#endregion
protected void Undo() => changeHandler?.RestoreState(-1);
protected void Redo() => changeHandler?.RestoreState(1);
2023-01-02 01:48:56 +08:00
protected void SetPreviewPointToCurrentTime()
2022-12-16 00:03:30 +08:00
{
editorBeatmap.PreviewTime.Value = (int)clock.CurrentTime;
}
private void resetTrack(bool seekToStart = false)
2019-07-10 23:22:40 +08:00
{
clock.Stop();
if (seekToStart)
{
double targetTime = 0;
if (editorBeatmap.BeatmapInfo.EditorTimestamp != null)
2023-06-01 01:07:04 +08:00
{
targetTime = editorBeatmap.BeatmapInfo.EditorTimestamp.Value;
2023-06-01 01:07:04 +08:00
}
else if (Beatmap.Value.Beatmap.HitObjects.Count > 0)
{
// seek to one beat length before the first hitobject
targetTime = Beatmap.Value.Beatmap.HitObjects[0].StartTime;
targetTime -= Beatmap.Value.Beatmap.ControlPointInfo.TimingPointAt(targetTime).BeatLength;
}
clock.Seek(Math.Max(0, targetTime));
}
2019-07-10 23:22:40 +08:00
}
2019-02-21 17:56:34 +08:00
private void onModeChanged(ValueChangedEvent<EditorScreenMode> e)
{
var lastScreen = currentScreen;
lastScreen?.Hide();
try
{
if ((currentScreen = screenContainer.SingleOrDefault(s => s.Type == e.NewValue)) != null)
{
screenContainer.ChangeChildDepth(currentScreen, lastScreen?.Depth + 1 ?? 0);
currentScreen.Show();
return;
}
switch (e.NewValue)
{
case EditorScreenMode.SongSetup:
currentScreen = new SetupScreen();
break;
case EditorScreenMode.Compose:
currentScreen = new ComposeScreen();
break;
2019-04-01 11:16:05 +08:00
case EditorScreenMode.Design:
currentScreen = new DesignScreen();
break;
2019-04-01 11:16:05 +08:00
case EditorScreenMode.Timing:
currentScreen = new TimingScreen();
break;
case EditorScreenMode.Verify:
currentScreen = new VerifyScreen();
break;
2021-05-14 11:04:38 +08:00
default:
throw new InvalidOperationException("Editor menu bar switched to an unsupported mode");
}
LoadComponentAsync(currentScreen, newScreen =>
{
if (newScreen == currentScreen)
{
screenContainer.Add(newScreen);
newScreen.Show();
}
});
}
finally
{
if (Mode.Value != EditorScreenMode.Compose)
ComposerFocusMode.Value = false;
updateSampleDisabledState();
rebindClipboardBindables();
}
}
/// <summary>
/// Forces a reload of the compose screen after significant configuration changes.
/// </summary>
public void ReloadComposeScreen()
{
screenContainer.SingleOrDefault(s => s.Type == EditorScreenMode.Compose)?.RemoveAndDisposeImmediately();
// If not currently on compose screen, the reload will happen on next mode change.
// That said, control points *can* change on compose screen (e.g. via undo), so we have to handle that case too.
if (Mode.Value == EditorScreenMode.Compose)
Mode.TriggerChange();
}
[CanBeNull]
private ScheduledDelegate playbackDisabledDebounce;
private EditorScreenSwitcherControl screenSwitcher;
private void updateSampleDisabledState()
{
bool shouldDisableSamples = clock.SeekingOrStopped.Value
|| currentScreen is not ComposeScreen
|| editorBeatmap.UpdateInProgress.Value
|| dialogOverlay?.CurrentDialog != null;
playbackDisabledDebounce?.Cancel();
if (shouldDisableSamples)
{
samplePlaybackDisabled.Value = true;
}
else
{
// Debounce re-enabling arbitrarily high enough to avoid flip-flopping during beatmap updates
// or rapid user seeks.
playbackDisabledDebounce = Scheduler.AddDelayed(() => samplePlaybackDisabled.Value = false, 50);
}
}
private void seekControlPoint(int direction)
{
var found = direction < 1
? editorBeatmap.ControlPointInfo.AllControlPoints.LastOrDefault(p => p.Time < clock.CurrentTime)
: editorBeatmap.ControlPointInfo.AllControlPoints.FirstOrDefault(p => p.Time > clock.CurrentTime);
if (found != null)
clock.Seek(found.Time);
}
private void seek(UIEvent e, int direction)
{
double amount = e.ShiftPressed ? 4 : 1;
bool trackPlaying = clock.IsRunning;
if (trackPlaying)
{
// generally users are not looking to perform tiny seeks when the track is playing.
// this multiplication undoes the division that will be applied in the underlying seek operation.
// scale by BPM to keep the seek amount constant across all BPMs.
var timingPoint = editorBeatmap.ControlPointInfo.TimingPointAt(clock.CurrentTimeAccurate);
amount *= beatDivisor.Value * (timingPoint.BPM / 120);
}
if (direction < 1)
clock.SeekBackward(!trackPlaying, amount);
else
clock.SeekForward(!trackPlaying, amount);
}
2020-01-14 18:05:52 +08:00
private void updateLastSavedHash()
{
lastSavedHash = changeHandler?.CurrentStateHash;
}
private IEnumerable<MenuItem> createFileMenuItems()
{
yield return createDifficultyCreationMenu();
yield return createDifficultySwitchMenu();
yield return new OsuMenuItemSpacer();
yield return new EditorMenuItem(EditorStrings.DeleteDifficulty, MenuItemType.Standard, deleteDifficulty) { Action = { Disabled = Beatmap.Value.BeatmapSetInfo.Beatmaps.Count < 2 } };
yield return new OsuMenuItemSpacer();
var save = new EditorMenuItem(WebCommonStrings.ButtonsSave, MenuItemType.Standard, () => attemptMutationOperation(Save));
saveRelatedMenuItems.Add(save);
yield return save;
if (RuntimeInfo.IsDesktop)
{
var export = createExportMenu();
saveRelatedMenuItems.AddRange(export.Items);
yield return export;
var externalEdit = new EditorMenuItem("Edit externally", MenuItemType.Standard, editExternally);
saveRelatedMenuItems.Add(externalEdit);
yield return externalEdit;
}
yield return new OsuMenuItemSpacer();
yield return new EditorMenuItem(CommonStrings.Exit, MenuItemType.Standard, this.Exit);
}
2023-07-23 13:05:44 +08:00
private EditorMenuItem createExportMenu()
{
var exportItems = new List<MenuItem>
{
new EditorMenuItem(EditorStrings.ExportForEditing, MenuItemType.Standard, () => exportBeatmap(false)),
new EditorMenuItem(EditorStrings.ExportForCompatibility, MenuItemType.Standard, () => exportBeatmap(true)),
};
return new EditorMenuItem(CommonStrings.Export) { Items = exportItems };
}
private void editExternally()
{
if (HasUnsavedChanges)
{
dialogOverlay.Push(new SaveRequiredPopupDialog(() => attemptMutationOperation(() =>
{
if (!Save())
return false;
startEdit();
return true;
})));
}
else
{
startEdit();
}
void startEdit()
{
this.Push(new ExternalEditScreen());
}
}
private void exportBeatmap(bool legacy)
2022-09-02 16:07:09 +08:00
{
if (HasUnsavedChanges)
{
2024-07-11 11:15:17 +08:00
dialogOverlay.Push(new SaveRequiredPopupDialog(() => attemptAsyncMutationOperation(() =>
{
if (!Save())
return Task.CompletedTask;
2022-09-02 16:07:09 +08:00
return runExport();
})));
}
else
{
attemptAsyncMutationOperation(runExport);
}
Task runExport()
{
if (legacy)
return beatmapManager.ExportLegacy(Beatmap.Value.BeatmapSetInfo);
else
return beatmapManager.Export(Beatmap.Value.BeatmapSetInfo);
}
2023-07-11 08:30:16 +08:00
}
/// <summary>
/// Beatmaps of the currently edited set, grouped by ruleset and ordered by difficulty.
/// </summary>
private IOrderedEnumerable<IGrouping<RulesetInfo, BeatmapInfo>> groupedOrderedBeatmaps => Beatmap.Value.BeatmapSetInfo.Beatmaps
.OrderBy(b => b.StarRating)
.GroupBy(b => b.Ruleset)
.OrderBy(group => group.Key);
2022-09-02 16:07:09 +08:00
private void deleteDifficulty()
{
if (dialogOverlay == null)
delete();
else
dialogOverlay.Push(new DeleteDifficultyConfirmationDialog(Beatmap.Value.BeatmapInfo, delete));
void delete()
{
BeatmapInfo difficultyToDelete = playableBeatmap.BeatmapInfo;
var difficultiesBeforeDeletion = groupedOrderedBeatmaps.SelectMany(g => g).ToList();
// if the difficulty being currently deleted has unsaved changes,
// the editor exit flow would prompt for save *after* this method has done its thing.
// this is generally undesirable and also ends up leaving the user in a broken state.
// therefore, just update the last saved hash to make the exit flow think the deleted beatmap is not dirty,
// so that it will not show the save dialog on exit.
updateLastSavedHash();
beatmapManager.DeleteDifficultyImmediately(difficultyToDelete);
int deletedIndex = difficultiesBeforeDeletion.IndexOf(difficultyToDelete);
// of note, we're still working with the cloned version, so indices are all prior to deletion.
BeatmapInfo nextToShow = difficultiesBeforeDeletion[deletedIndex == 0 ? 1 : deletedIndex - 1];
2022-09-02 16:07:09 +08:00
Beatmap.Value = beatmapManager.GetWorkingBeatmap(nextToShow);
SwitchToDifficulty(nextToShow);
2022-09-02 16:07:09 +08:00
}
}
2022-01-23 23:34:02 +08:00
private EditorMenuItem createDifficultyCreationMenu()
{
var rulesetItems = new List<MenuItem>();
foreach (var ruleset in rulesets.AvailableRulesets)
rulesetItems.Add(new EditorMenuItem(ruleset.Name, MenuItemType.Standard, () => CreateNewDifficulty(ruleset)));
2022-01-23 23:34:02 +08:00
saveRelatedMenuItems.AddRange(rulesetItems);
2023-01-15 06:50:41 +08:00
return new EditorMenuItem(EditorStrings.CreateNewDifficulty) { Items = rulesetItems };
2022-01-23 23:34:02 +08:00
}
protected void CreateNewDifficulty(RulesetInfo rulesetInfo)
{
if (isNewBeatmap)
{
2024-07-11 11:15:17 +08:00
dialogOverlay.Push(new SaveRequiredPopupDialog(() => attemptMutationOperation(() =>
{
2024-07-11 11:15:17 +08:00
if (!Save())
return false;
2024-07-11 11:15:17 +08:00
CreateNewDifficulty(rulesetInfo);
return true;
})));
return;
}
if (!rulesetInfo.Equals(editorBeatmap.BeatmapInfo.Ruleset))
{
switchToNewDifficulty(rulesetInfo, false);
return;
}
dialogOverlay.Push(new CreateNewDifficultyDialog(createCopy => switchToNewDifficulty(rulesetInfo, createCopy)));
}
private void switchToNewDifficulty(RulesetInfo rulesetInfo, bool createCopy)
{
switchingDifficulty = true;
loader?.ScheduleSwitchToNewDifficulty(editorBeatmap.BeatmapInfo, rulesetInfo, createCopy, GetState(rulesetInfo));
}
private EditorMenuItem createDifficultySwitchMenu()
{
var difficultyItems = new List<MenuItem>();
foreach (var rulesetBeatmaps in groupedOrderedBeatmaps)
{
if (difficultyItems.Count > 0)
difficultyItems.Add(new OsuMenuItemSpacer());
foreach (var beatmap in rulesetBeatmaps)
{
bool isCurrentDifficulty = playableBeatmap.BeatmapInfo.Equals(beatmap);
difficultyItems.Add(new DifficultyMenuItem(beatmap, isCurrentDifficulty, SwitchToDifficulty));
}
}
2023-01-15 06:50:41 +08:00
return new EditorMenuItem(EditorStrings.ChangeDifficulty) { Items = difficultyItems };
2021-09-05 23:26:09 +08:00
}
public void SwitchToDifficulty(BeatmapInfo nextBeatmap)
{
switchingDifficulty = true;
loader?.ScheduleSwitchToExistingDifficulty(nextBeatmap, GetState(nextBeatmap.Ruleset));
}
private void cancelExit()
{
updateSampleDisabledState();
loader?.CancelPendingDifficultySwitch();
}
public Task<bool> Reload()
{
var tcs = new TaskCompletionSource<bool>();
dialogOverlay.Push(new ReloadEditorDialog(
reload: () =>
{
bool reloadedSuccessfully = attemptMutationOperation(() =>
{
if (!Save())
return false;
SwitchToDifficulty(editorBeatmap.BeatmapInfo);
return true;
});
tcs.SetResult(reloadedSuccessfully);
},
cancel: () => tcs.SetResult(false)));
return tcs.Task;
}
2024-06-18 18:33:12 +08:00
public bool HandleTimestamp(string timestamp, bool notifyOnError = false)
{
if (!EditorTimestampParser.TryParse(timestamp, out var timeSpan, out string selection))
{
2024-06-18 18:33:12 +08:00
if (notifyOnError)
{
2024-06-18 18:33:12 +08:00
Schedule(() => notifications?.Post(new SimpleErrorNotification
{
Icon = FontAwesome.Solid.ExclamationTriangle,
Text = EditorStrings.FailedToParseEditorLink
}));
}
return false;
}
editorBeatmap.SelectedHitObjects.Clear();
2023-11-07 19:23:22 +08:00
if (clock.IsRunning)
clock.Stop();
double position = timeSpan.Value.TotalMilliseconds;
if (string.IsNullOrEmpty(selection))
{
clock.SeekSmoothlyTo(position);
2024-06-18 18:33:12 +08:00
return true;
}
// Seek to the next closest HitObject instead
HitObject nextObject = editorBeatmap.HitObjects.FirstOrDefault(x => x.StartTime >= position);
if (nextObject != null)
position = nextObject.StartTime;
clock.SeekSmoothlyTo(position);
Mode.Value = EditorScreenMode.Compose;
// Delegate handling the selection to the ruleset.
currentScreen.Dependencies.Get<HitObjectComposer>().SelectFromTimestamp(position, selection);
2024-06-18 18:33:12 +08:00
return true;
}
public double SnapTime(double time, double? referenceTime) => editorBeatmap.SnapTime(time, referenceTime);
2020-01-23 14:31:56 +08:00
public double GetBeatLengthAtTime(double referenceTime) => editorBeatmap.GetBeatLengthAtTime(referenceTime);
public int BeatDivisor => beatDivisor.Value;
ControlPointInfo IBeatSyncProvider.ControlPoints => editorBeatmap.ControlPointInfo;
IClock IBeatSyncProvider.Clock => clock;
ChannelAmplitudes IHasAmplitudes.CurrentAmplitudes => Beatmap.Value.TrackLoaded ? Beatmap.Value.Track.CurrentAmplitudes : ChannelAmplitudes.Empty;
2022-07-20 03:38:23 +08:00
2022-11-24 13:32:20 +08:00
private partial class BeatmapEditorToast : Toast
2022-07-20 03:38:23 +08:00
{
public BeatmapEditorToast(LocalisableString value, string beatmapDisplayName)
: base(InputSettingsStrings.EditorSection, value, beatmapDisplayName)
{
}
2022-07-20 03:38:23 +08:00
}
2018-04-13 17:19:50 +08:00
}
}