2019-01-24 16:43:03 +08:00
|
|
|
|
// 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
|
|
|
|
|
|
2018-11-22 18:40:44 +08:00
|
|
|
|
using System;
|
2020-09-09 18:31:18 +08:00
|
|
|
|
using System.Collections.Generic;
|
2020-09-11 18:54:20 +08:00
|
|
|
|
using System.Linq;
|
2021-09-05 22:59:28 +08:00
|
|
|
|
using JetBrains.Annotations;
|
2020-09-09 18:31:18 +08:00
|
|
|
|
using osu.Framework;
|
|
|
|
|
using osu.Framework.Allocation;
|
2022-08-02 16:54:42 +08:00
|
|
|
|
using osu.Framework.Audio;
|
2022-05-22 21:15:53 +08:00
|
|
|
|
using osu.Framework.Audio.Track;
|
2020-09-09 18:31:18 +08:00
|
|
|
|
using osu.Framework.Bindables;
|
2017-07-15 00:18:12 +08:00
|
|
|
|
using osu.Framework.Graphics;
|
2017-08-28 16:55:50 +08:00
|
|
|
|
using osu.Framework.Graphics.Containers;
|
2023-11-07 08:36:58 +08:00
|
|
|
|
using osu.Framework.Graphics.Sprites;
|
2017-10-06 23:51:30 +08:00
|
|
|
|
using osu.Framework.Graphics.UserInterface;
|
2020-04-22 16:41:24 +08:00
|
|
|
|
using osu.Framework.Input;
|
2019-06-30 18:31:31 +08:00
|
|
|
|
using osu.Framework.Input.Bindings;
|
2020-09-09 18:31:18 +08:00
|
|
|
|
using osu.Framework.Input.Events;
|
2022-07-20 03:38:23 +08:00
|
|
|
|
using osu.Framework.Localisation;
|
2020-03-13 13:28:11 +08:00
|
|
|
|
using osu.Framework.Logging;
|
2020-09-09 18:31:18 +08:00
|
|
|
|
using osu.Framework.Screens;
|
2022-06-27 15:19:31 +08:00
|
|
|
|
using osu.Framework.Testing;
|
2022-08-17 13:04:55 +08:00
|
|
|
|
using osu.Framework.Threading;
|
2022-05-22 21:15:53 +08:00
|
|
|
|
using osu.Framework.Timing;
|
2022-05-07 22:17:23 +08:00
|
|
|
|
using osu.Game.Audio;
|
2019-12-27 18:46:33 +08:00
|
|
|
|
using osu.Game.Beatmaps;
|
2022-05-22 21:15:53 +08:00
|
|
|
|
using osu.Game.Beatmaps.ControlPoints;
|
2020-11-03 15:01:14 +08:00
|
|
|
|
using osu.Game.Configuration;
|
2023-06-06 14:11:31 +08:00
|
|
|
|
using osu.Game.Database;
|
2019-11-07 21:51:49 +08:00
|
|
|
|
using osu.Game.Graphics.Cursor;
|
2020-09-09 18:31:18 +08:00
|
|
|
|
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;
|
2020-09-01 17:56:49 +08:00
|
|
|
|
using osu.Game.Online.API;
|
2020-09-09 18:31:18 +08:00
|
|
|
|
using osu.Game.Overlays;
|
2022-01-28 13:01:31 +08:00
|
|
|
|
using osu.Game.Overlays.Notifications;
|
2022-07-20 03:38:23 +08:00
|
|
|
|
using osu.Game.Overlays.OSD;
|
2022-01-28 13:01:31 +08:00
|
|
|
|
using osu.Game.Rulesets;
|
2020-01-23 13:39:56 +08:00
|
|
|
|
using osu.Game.Rulesets.Edit;
|
2023-11-04 09:01:18 +08:00
|
|
|
|
using osu.Game.Rulesets.Objects;
|
2020-09-09 18:31:18 +08:00
|
|
|
|
using osu.Game.Screens.Edit.Components.Menus;
|
2019-10-09 15:04:58 +08:00
|
|
|
|
using osu.Game.Screens.Edit.Compose;
|
2022-06-27 15:19:31 +08:00
|
|
|
|
using osu.Game.Screens.Edit.Compose.Components.Timeline;
|
2020-09-09 18:31:18 +08:00
|
|
|
|
using osu.Game.Screens.Edit.Design;
|
2021-11-12 19:07:38 +08:00
|
|
|
|
using osu.Game.Screens.Edit.GameplayTest;
|
2019-10-08 13:23:13 +08:00
|
|
|
|
using osu.Game.Screens.Edit.Setup;
|
|
|
|
|
using osu.Game.Screens.Edit.Timing;
|
2021-03-28 23:36:22 +08:00
|
|
|
|
using osu.Game.Screens.Edit.Verify;
|
2019-12-12 12:04:32 +08:00
|
|
|
|
using osu.Game.Screens.Play;
|
2019-04-13 04:54:35 +08:00
|
|
|
|
using osu.Game.Users;
|
2020-09-09 18:31:18 +08:00
|
|
|
|
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
|
|
|
|
|
2016-11-14 16:23:33 +08:00
|
|
|
|
namespace osu.Game.Screens.Edit
|
2016-09-29 19:13:58 +08:00
|
|
|
|
{
|
2020-01-23 13:39:56 +08:00
|
|
|
|
[Cached(typeof(IBeatSnapProvider))]
|
2020-09-24 17:55:49 +08:00
|
|
|
|
[Cached]
|
2022-11-17 06:32:54 +08:00
|
|
|
|
public partial class Editor : ScreenWithBeatmapBackground, IKeyBindingHandler<GlobalAction>, IKeyBindingHandler<PlatformAction>, IBeatSnapProvider, ISamplePlaybackDisabler, IBeatSyncProvider
|
2016-09-29 19:13:58 +08:00
|
|
|
|
{
|
2023-12-26 16:44:49 +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;
|
|
|
|
|
|
2019-01-28 14:41:54 +08:00
|
|
|
|
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
|
|
|
|
|
2023-07-25 18:58:23 +08:00
|
|
|
|
public override bool? ApplyModTrackAdjustments => false;
|
2020-01-24 12:10:02 +08:00
|
|
|
|
|
2022-06-15 17:44:02 +08:00
|
|
|
|
protected override bool PlayExitSound => !ExitConfirmed && !switchingDifficulty;
|
2022-06-15 12:04:17 +08:00
|
|
|
|
|
2022-01-28 13:01:31 +08:00
|
|
|
|
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; }
|
|
|
|
|
|
2020-09-09 18:31:18 +08:00
|
|
|
|
[Resolved(canBeNull: true)]
|
2022-04-18 17:09:14 +08:00
|
|
|
|
private IDialogOverlay dialogOverlay { get; set; }
|
2020-09-09 18:31:18 +08:00
|
|
|
|
|
2022-01-28 13:01:31 +08:00
|
|
|
|
[Resolved(canBeNull: true)]
|
2022-04-18 18:59:57 +08:00
|
|
|
|
private INotificationOverlay notifications { get; set; }
|
2022-01-28 13:01:31 +08:00
|
|
|
|
|
2023-06-06 14:11:31 +08:00
|
|
|
|
[Resolved]
|
|
|
|
|
private RealmAccess realm { get; set; }
|
|
|
|
|
|
2022-03-02 18:19:39 +08:00
|
|
|
|
public readonly Bindable<EditorScreenMode> Mode = new Bindable<EditorScreenMode>();
|
|
|
|
|
|
2020-10-27 13:31:56 +08:00
|
|
|
|
public IBindable<bool> SamplePlaybackDisabled => samplePlaybackDisabled;
|
|
|
|
|
|
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;
|
|
|
|
|
|
2022-06-27 17:28:00 +08:00
|
|
|
|
if (currentScreen?.IsLoaded != true)
|
2022-06-27 15:19:31 +08:00
|
|
|
|
return false;
|
|
|
|
|
|
2022-06-27 17:28:00 +08:00
|
|
|
|
if (currentScreen is EditorScreenWithTimeline)
|
|
|
|
|
return currentScreen.ChildrenOfType<TimelineArea>().FirstOrDefault()?.IsLoaded == true;
|
2022-06-27 15:19:31 +08:00
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private bool workingBeatmapUpdated;
|
|
|
|
|
|
2020-10-27 13:31:56 +08:00
|
|
|
|
private readonly Bindable<bool> samplePlaybackDisabled = new Bindable<bool>();
|
|
|
|
|
|
2022-01-28 13:01:31 +08:00
|
|
|
|
private bool canSave;
|
|
|
|
|
|
2022-03-22 14:28:31 +08:00
|
|
|
|
protected bool ExitConfirmed { get; private set; }
|
2020-09-09 18:31:18 +08:00
|
|
|
|
|
2022-06-15 17:44:02 +08:00
|
|
|
|
private bool switchingDifficulty;
|
|
|
|
|
|
2020-09-09 18:40:41 +08:00
|
|
|
|
private string lastSavedHash;
|
|
|
|
|
|
2020-09-24 16:03:54 +08:00
|
|
|
|
private Container<EditorScreen> screenContainer;
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2021-09-05 22:59:28 +08:00
|
|
|
|
[CanBeNull]
|
|
|
|
|
private readonly EditorLoader loader;
|
|
|
|
|
|
2017-10-02 08:26:41 +08:00
|
|
|
|
private EditorScreen currentScreen;
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2018-04-06 16:40:06 +08:00
|
|
|
|
private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor();
|
|
|
|
|
private EditorClock clock;
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2019-12-27 18:46:33 +08:00
|
|
|
|
private IBeatmap playableBeatmap;
|
|
|
|
|
private EditorBeatmap editorBeatmap;
|
2022-01-28 13:01:31 +08:00
|
|
|
|
|
2022-05-25 22:10:58 +08:00
|
|
|
|
private BottomBar bottomBar;
|
|
|
|
|
|
2022-01-28 13:01:31 +08:00
|
|
|
|
[CanBeNull] // Should be non-null once it can support custom rulesets.
|
2020-04-09 20:22:07 +08:00
|
|
|
|
private EditorChangeHandler changeHandler;
|
2019-12-27 18:46:33 +08:00
|
|
|
|
|
2018-03-19 15:27:52 +08:00
|
|
|
|
private DependencyContainer dependencies;
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2020-10-04 22:47:16 +08:00
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-04-13 04:54:35 +08:00
|
|
|
|
|
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
|
|
|
|
|
2020-09-01 17:56:49 +08:00
|
|
|
|
[Resolved]
|
|
|
|
|
private IAPIProvider api { get; set; }
|
|
|
|
|
|
2021-11-10 19:36:23 +08:00
|
|
|
|
[Cached]
|
|
|
|
|
public readonly EditorClipboard Clipboard = new EditorClipboard();
|
2021-11-08 21:24:39 +08:00
|
|
|
|
|
2021-11-11 21:00:09 +08:00
|
|
|
|
[Cached]
|
2022-05-24 16:36:43 +08:00
|
|
|
|
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
|
2021-11-11 21:00:09 +08:00
|
|
|
|
|
2022-07-20 03:38:23 +08:00
|
|
|
|
[Resolved(canBeNull: true)]
|
|
|
|
|
private OnScreenDisplay onScreenDisplay { get; set; }
|
|
|
|
|
|
2022-11-02 14:12:15 +08:00
|
|
|
|
private Bindable<float> editorBackgroundDim;
|
2022-11-08 17:07:06 +08:00
|
|
|
|
private Bindable<bool> editorHitMarkers;
|
2023-02-19 22:06:40 +08:00
|
|
|
|
private Bindable<bool> editorAutoSeekOnPlacement;
|
2023-06-09 14:54:22 +08:00
|
|
|
|
private Bindable<bool> editorLimitedDistanceSnap;
|
2022-10-24 22:18:34 +08:00
|
|
|
|
|
2021-09-05 22:59:28 +08:00
|
|
|
|
public Editor(EditorLoader loader = null)
|
|
|
|
|
{
|
|
|
|
|
this.loader = loader;
|
|
|
|
|
}
|
|
|
|
|
|
2018-03-15 16:10:08 +08:00
|
|
|
|
[BackgroundDependencyLoader]
|
2022-05-25 22:10:58 +08:00
|
|
|
|
private void load(OsuConfigManager config)
|
2017-08-28 16:55:50 +08:00
|
|
|
|
{
|
2021-03-17 14:52:24 +08:00
|
|
|
|
var loadableBeatmap = Beatmap.Value;
|
|
|
|
|
|
|
|
|
|
if (loadableBeatmap is DummyWorkingBeatmap)
|
2020-12-01 15:16:26 +08:00
|
|
|
|
{
|
2023-08-16 13:23:17 +08:00
|
|
|
|
Logger.Log("Editor was loaded without a valid beatmap; creating a new beatmap.");
|
|
|
|
|
|
2020-12-01 15:16:26 +08:00
|
|
|
|
isNewBeatmap = true;
|
2021-01-22 16:47:38 +08:00
|
|
|
|
|
2021-03-17 14:52:24 +08:00
|
|
|
|
loadableBeatmap = beatmapManager.CreateNew(Ruleset.Value, api.LocalUser.Value);
|
|
|
|
|
|
|
|
|
|
// required so we can get the track length in EditorClock.
|
2022-07-13 18:02:21 +08:00
|
|
|
|
// this is ONLY safe because the track being provided is a `TrackVirtual` which we don't really care about disposing.
|
2021-03-17 14:52:24 +08:00
|
|
|
|
loadableBeatmap.LoadTrack();
|
2021-01-22 16:47:38 +08:00
|
|
|
|
|
|
|
|
|
// 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);
|
2021-01-22 16:47:38 +08:00
|
|
|
|
return;
|
2023-04-28 22:45:00 +08:00
|
|
|
|
}
|
2020-12-01 15:16:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
2020-03-13 13:28:11 +08:00
|
|
|
|
try
|
|
|
|
|
{
|
2021-03-17 14:52:24 +08:00
|
|
|
|
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.
|
2021-07-19 11:38:22 +08:00
|
|
|
|
playableBeatmap.ControlPointInfo = playableBeatmap.ControlPointInfo.DeepClone();
|
2020-03-13 13:28:11 +08:00
|
|
|
|
}
|
|
|
|
|
catch (Exception e)
|
|
|
|
|
{
|
2020-03-16 10:29:28 +08:00
|
|
|
|
Logger.Error(e, "Could not load beatmap successfully!");
|
|
|
|
|
// couldn't load, hard abort!
|
2020-03-13 13:28:11 +08:00
|
|
|
|
this.Exit();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2021-04-03 17:43:47 +08:00
|
|
|
|
// Todo: should probably be done at a DrawableRuleset level to share logic with Player.
|
2022-08-26 17:08:39 +08:00
|
|
|
|
clock = new EditorClock(playableBeatmap, beatDivisor);
|
2021-05-07 14:54:58 +08:00
|
|
|
|
clock.ChangeSource(loadableBeatmap.Track);
|
2021-04-03 17:43:47 +08:00
|
|
|
|
|
|
|
|
|
dependencies.CacheAs(clock);
|
|
|
|
|
AddInternal(clock);
|
|
|
|
|
|
|
|
|
|
clock.SeekingOrStopped.BindValueChanged(_ => updateSampleDisabledState());
|
|
|
|
|
|
|
|
|
|
// todo: remove caching of this and consume via editorBeatmap?
|
|
|
|
|
dependencies.Cache(beatDivisor);
|
|
|
|
|
|
2021-10-13 13:34:31 +08:00
|
|
|
|
AddInternal(editorBeatmap = new EditorBeatmap(playableBeatmap, loadableBeatmap.GetSkin(), loadableBeatmap.BeatmapInfo));
|
2019-12-27 18:46:33 +08:00
|
|
|
|
dependencies.CacheAs(editorBeatmap);
|
2022-01-28 13:01:31 +08:00
|
|
|
|
|
2022-11-09 16:42:33 +08:00
|
|
|
|
editorBeatmap.UpdateInProgress.BindValueChanged(_ => updateSampleDisabledState());
|
2022-08-17 13:04:55 +08:00
|
|
|
|
|
2022-01-28 13:01:31 +08:00
|
|
|
|
canSave = editorBeatmap.BeatmapInfo.Ruleset.CreateInstance() is ILegacyRuleset;
|
|
|
|
|
|
|
|
|
|
if (canSave)
|
|
|
|
|
{
|
2023-02-03 16:53:54 +08:00
|
|
|
|
changeHandler = new BeatmapEditorChangeHandler(editorBeatmap);
|
2022-01-28 13:01:31 +08:00
|
|
|
|
dependencies.CacheAs<IEditorChangeHandler>(changeHandler);
|
|
|
|
|
}
|
2020-04-09 20:22:07 +08:00
|
|
|
|
|
2022-01-25 16:43:16 +08:00
|
|
|
|
beatDivisor.Value = editorBeatmap.BeatmapInfo.BeatDivisor;
|
|
|
|
|
beatDivisor.BindValueChanged(divisor => editorBeatmap.BeatmapInfo.BeatDivisor = divisor.NewValue);
|
|
|
|
|
|
2020-09-09 18:40:41 +08:00
|
|
|
|
updateLastSavedHash();
|
|
|
|
|
|
2021-03-17 14:52:24 +08:00
|
|
|
|
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;
|
2022-06-27 15:19:31 +08:00
|
|
|
|
workingBeatmapUpdated = true;
|
2021-03-17 14:52:24 +08:00
|
|
|
|
});
|
|
|
|
|
|
2020-04-16 12:25:08 +08:00
|
|
|
|
OsuMenuItem undoMenuItem;
|
|
|
|
|
OsuMenuItem redoMenuItem;
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2022-11-02 14:12:15 +08:00
|
|
|
|
editorBackgroundDim = config.GetBindable<float>(OsuSetting.EditorDim);
|
2022-11-08 17:07:06 +08:00
|
|
|
|
editorHitMarkers = config.GetBindable<bool>(OsuSetting.EditorShowHitMarkers);
|
2023-02-19 22:06:40 +08:00
|
|
|
|
editorAutoSeekOnPlacement = config.GetBindable<bool>(OsuSetting.EditorAutoSeekOnPlacement);
|
2023-06-09 14:54:22 +08:00
|
|
|
|
editorLimitedDistanceSnap = config.GetBindable<bool>(OsuSetting.EditorLimitedDistanceSnap);
|
2022-10-24 22:18:34 +08:00
|
|
|
|
|
2020-02-05 16:16:15 +08:00
|
|
|
|
AddInternal(new OsuContextMenuContainer
|
2017-08-28 16:55:50 +08:00
|
|
|
|
{
|
2019-11-07 21:51:49 +08:00
|
|
|
|
RelativeSizeAxes = Axes.Both,
|
2022-05-25 22:10:58 +08:00
|
|
|
|
Children = new Drawable[]
|
2017-10-07 00:56:11 +08:00
|
|
|
|
{
|
2019-11-07 21:51:49 +08:00
|
|
|
|
new Container
|
2017-10-07 00:56:11 +08:00
|
|
|
|
{
|
2019-11-07 21:51:49 +08:00
|
|
|
|
Name = "Screen container",
|
2017-10-07 00:56:11 +08:00
|
|
|
|
RelativeSizeAxes = Axes.Both,
|
2019-11-07 21:51:49 +08:00
|
|
|
|
Padding = new MarginPadding { Top = 40, Bottom = 60 },
|
2020-09-24 16:03:54 +08:00
|
|
|
|
Child = screenContainer = new Container<EditorScreen>
|
2019-11-07 21:51:49 +08:00
|
|
|
|
{
|
|
|
|
|
RelativeSizeAxes = Axes.Both,
|
|
|
|
|
Masking = true
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
new Container
|
2017-08-28 16:55:50 +08:00
|
|
|
|
{
|
2019-11-07 21:51:49 +08:00
|
|
|
|
Name = "Top bar",
|
|
|
|
|
RelativeSizeAxes = Axes.X,
|
|
|
|
|
Height = 40,
|
2022-03-02 18:19:39 +08:00
|
|
|
|
Children = new Drawable[]
|
2017-10-06 23:51:30 +08:00
|
|
|
|
{
|
2022-03-02 18:19:39 +08:00
|
|
|
|
new EditorMenuBar
|
2017-10-06 23:51:30 +08:00
|
|
|
|
{
|
2022-03-02 18:19:39 +08:00
|
|
|
|
Anchor = Anchor.CentreLeft,
|
|
|
|
|
Origin = Anchor.CentreLeft,
|
|
|
|
|
RelativeSizeAxes = Axes.Both,
|
|
|
|
|
Items = new[]
|
2020-04-09 20:22:07 +08:00
|
|
|
|
{
|
2023-01-16 00:37:40 +08:00
|
|
|
|
new MenuItem(CommonStrings.MenuBarFile)
|
2020-04-09 20:22:07 +08:00
|
|
|
|
{
|
2022-03-02 18:19:39 +08:00
|
|
|
|
Items = createFileMenuItems()
|
|
|
|
|
},
|
2023-01-16 00:37:40 +08:00
|
|
|
|
new MenuItem(CommonStrings.MenuBarEdit)
|
2020-11-03 15:01:14 +08:00
|
|
|
|
{
|
2022-03-02 18:19:39 +08:00
|
|
|
|
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),
|
2023-11-21 13:24:10 +08:00
|
|
|
|
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),
|
2022-03-02 18:19:39 +08:00
|
|
|
|
}
|
|
|
|
|
},
|
2023-01-16 00:37:40 +08:00
|
|
|
|
new MenuItem(CommonStrings.MenuBarView)
|
2022-03-02 18:19:39 +08:00
|
|
|
|
{
|
|
|
|
|
Items = new MenuItem[]
|
|
|
|
|
{
|
|
|
|
|
new WaveformOpacityMenuItem(config.GetBindable<float>(OsuSetting.EditorWaveformOpacity)),
|
2022-11-02 14:12:15 +08:00
|
|
|
|
new BackgroundDimMenuItem(editorBackgroundDim),
|
2023-01-15 06:50:41 +08:00
|
|
|
|
new ToggleMenuItem(EditorStrings.ShowHitMarkers)
|
2022-11-08 17:07:06 +08:00
|
|
|
|
{
|
|
|
|
|
State = { BindTarget = editorHitMarkers },
|
2023-02-19 02:43:45 +08:00
|
|
|
|
},
|
2023-02-19 22:06:40 +08:00
|
|
|
|
new ToggleMenuItem(EditorStrings.AutoSeekOnPlacement)
|
2023-02-19 02:43:45 +08:00
|
|
|
|
{
|
2023-02-19 22:06:40 +08:00
|
|
|
|
State = { BindTarget = editorAutoSeekOnPlacement },
|
2023-06-09 14:54:22 +08:00
|
|
|
|
},
|
|
|
|
|
new ToggleMenuItem(EditorStrings.LimitedDistanceSnap)
|
|
|
|
|
{
|
|
|
|
|
State = { BindTarget = editorLimitedDistanceSnap },
|
2022-11-08 17:07:06 +08:00
|
|
|
|
}
|
2022-03-02 18:19:39 +08:00
|
|
|
|
}
|
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
|
|
|
|
}
|
2020-11-03 15:01:14 +08:00
|
|
|
|
}
|
2019-11-07 21:51:49 +08:00
|
|
|
|
}
|
2022-03-02 18:19:39 +08:00
|
|
|
|
},
|
2022-05-25 21:20:33 +08:00
|
|
|
|
new EditorScreenSwitcherControl
|
2022-03-02 18:19:39 +08:00
|
|
|
|
{
|
|
|
|
|
Anchor = Anchor.BottomRight,
|
|
|
|
|
Origin = Anchor.BottomRight,
|
2023-07-21 14:27:42 +08:00
|
|
|
|
X = -10,
|
2022-03-02 18:19:39 +08:00
|
|
|
|
Current = Mode,
|
|
|
|
|
},
|
|
|
|
|
},
|
2019-11-07 21:51:49 +08:00
|
|
|
|
},
|
2022-05-25 22:10:58 +08:00
|
|
|
|
bottomBar = new BottomBar(),
|
2019-11-07 21:51:49 +08:00
|
|
|
|
}
|
2020-02-05 16:16:15 +08:00
|
|
|
|
});
|
2022-01-28 13:01:31 +08:00
|
|
|
|
changeHandler?.CanUndo.BindValueChanged(v => undoMenuItem.Action.Disabled = !v.NewValue, true);
|
|
|
|
|
changeHandler?.CanRedo.BindValueChanged(v => redoMenuItem.Action.Disabled = !v.NewValue, true);
|
2022-10-24 22:18:34 +08:00
|
|
|
|
|
2022-11-02 14:12:15 +08:00
|
|
|
|
editorBackgroundDim.BindValueChanged(_ => dimBackground());
|
2017-08-28 16:55:50 +08:00
|
|
|
|
}
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2022-08-01 15:30:45 +08:00
|
|
|
|
[Resolved]
|
|
|
|
|
private MusicController musicController { get; set; }
|
|
|
|
|
|
2021-11-08 21:54:31 +08:00
|
|
|
|
protected override void LoadComplete()
|
|
|
|
|
{
|
|
|
|
|
base.LoadComplete();
|
|
|
|
|
setUpClipboardActionAvailability();
|
2022-03-02 18:19:39 +08:00
|
|
|
|
|
|
|
|
|
Mode.Value = isNewBeatmap ? EditorScreenMode.SongSetup : EditorScreenMode.Compose;
|
|
|
|
|
Mode.BindValueChanged(onModeChanged, true);
|
2022-08-01 15:30:45 +08:00
|
|
|
|
|
|
|
|
|
musicController.TrackChanged += onTrackChanged;
|
2021-11-08 21:54:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
2022-08-01 15:30:45 +08:00
|
|
|
|
protected override void Dispose(bool isDisposing)
|
|
|
|
|
{
|
|
|
|
|
base.Dispose(isDisposing);
|
|
|
|
|
|
|
|
|
|
musicController.TrackChanged -= onTrackChanged;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void onTrackChanged(WorkingBeatmap working, TrackChangeDirection direction) => clock.ChangeSource(working.Track);
|
2020-09-24 17:55:49 +08:00
|
|
|
|
|
2021-11-12 19:50:38 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Creates an <see cref="EditorState"/> instance representing the current state of the editor.
|
|
|
|
|
/// </summary>
|
2022-02-15 03:56:05 +08:00
|
|
|
|
/// <param name="nextRuleset">
|
|
|
|
|
/// The ruleset of the next beatmap to be shown, in the case of difficulty switch.
|
2021-11-12 19:50:38 +08:00
|
|
|
|
/// <see langword="null"/> indicates that the beatmap will not be changing.
|
|
|
|
|
/// </param>
|
2022-02-15 03:56:05 +08:00
|
|
|
|
public EditorState GetState([CanBeNull] RulesetInfo nextRuleset = null) => new EditorState
|
2021-11-12 19:50:38 +08:00
|
|
|
|
{
|
|
|
|
|
Time = clock.CurrentTimeAccurate,
|
2022-02-15 03:56:05 +08:00
|
|
|
|
ClipboardContent = nextRuleset == null || editorBeatmap.BeatmapInfo.Ruleset.ShortName == nextRuleset.ShortName ? Clipboard.Content.Value : string.Empty
|
2021-11-12 19:50:38 +08:00
|
|
|
|
};
|
|
|
|
|
|
2021-09-14 22:36:17 +08:00
|
|
|
|
/// <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);
|
2021-11-10 19:36:23 +08:00
|
|
|
|
Clipboard.Content.Value = state.ClipboardContent;
|
2021-09-14 22:36:17 +08:00
|
|
|
|
});
|
|
|
|
|
|
2022-05-25 22:10:58 +08:00
|
|
|
|
public void TestGameplay()
|
|
|
|
|
{
|
|
|
|
|
if (HasUnsavedChanges)
|
|
|
|
|
{
|
2023-10-25 13:33:06 +08:00
|
|
|
|
dialogOverlay.Push(new SaveRequiredPopupDialog("The beatmap will be saved in order to test it.", () =>
|
2022-05-25 22:10:58 +08:00
|
|
|
|
{
|
2023-08-05 13:05:59 +08:00
|
|
|
|
if (!Save()) return;
|
|
|
|
|
|
2022-05-25 22:10:58 +08:00
|
|
|
|
pushEditorPlayer();
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
pushEditorPlayer();
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-10 15:19:38 +08:00
|
|
|
|
void pushEditorPlayer() => this.Push(new EditorPlayerLoader(this, playableBeatmap));
|
2022-05-25 22:10:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
2022-01-24 02:50:02 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Saves the currently edited beatmap.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <returns>Whether the save was successful.</returns>
|
|
|
|
|
protected bool Save()
|
2020-09-09 18:57:28 +08:00
|
|
|
|
{
|
2022-01-28 13:01:31 +08:00
|
|
|
|
if (!canSave)
|
|
|
|
|
{
|
|
|
|
|
notifications?.Post(new SimpleErrorNotification { Text = "Saving is not supported for this ruleset yet, sorry!" });
|
2022-01-24 02:50:02 +08:00
|
|
|
|
return false;
|
2022-01-28 13:01:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
2022-01-24 02:42:07 +08:00
|
|
|
|
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);
|
2022-01-24 02:50:02 +08:00
|
|
|
|
return false;
|
2022-01-24 02:42:07 +08:00
|
|
|
|
}
|
|
|
|
|
|
2020-10-04 22:47:16 +08:00
|
|
|
|
// 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()));
|
2022-01-24 02:50:02 +08:00
|
|
|
|
return true;
|
2020-09-09 18:57:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
2019-10-18 17:21:53 +08:00
|
|
|
|
protected override void Update()
|
|
|
|
|
{
|
|
|
|
|
base.Update();
|
|
|
|
|
clock.ProcessFrame();
|
|
|
|
|
}
|
|
|
|
|
|
2021-09-16 17:26:12 +08:00
|
|
|
|
public bool OnPressed(KeyBindingPressEvent<PlatformAction> e)
|
2020-04-22 16:41:24 +08:00
|
|
|
|
{
|
2021-09-16 17:26:12 +08:00
|
|
|
|
switch (e.Action)
|
2020-04-22 16:41:24 +08:00
|
|
|
|
{
|
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();
|
2020-04-22 16:41:24 +08:00
|
|
|
|
return true;
|
|
|
|
|
|
2021-07-20 13:23:34 +08:00
|
|
|
|
case PlatformAction.Redo:
|
2020-04-22 17:14:21 +08:00
|
|
|
|
Redo();
|
2020-04-22 16:41:24 +08:00
|
|
|
|
return true;
|
|
|
|
|
|
2021-07-20 13:23:34 +08:00
|
|
|
|
case PlatformAction.Save:
|
2021-11-18 11:36:15 +08:00
|
|
|
|
if (e.Repeat)
|
|
|
|
|
return false;
|
|
|
|
|
|
2020-09-09 18:57:28 +08:00
|
|
|
|
Save();
|
2020-04-22 16:41:24 +08:00
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2021-09-16 17:26:12 +08:00
|
|
|
|
public void OnReleased(KeyBindingReleaseEvent<PlatformAction> e)
|
2020-04-22 16:41:24 +08:00
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-30 13:57:25 +08:00
|
|
|
|
protected override bool OnKeyDown(KeyDownEvent e)
|
2017-12-21 18:56:12 +08:00
|
|
|
|
{
|
2022-06-14 17:48:57 +08:00
|
|
|
|
if (e.ControlPressed || e.AltPressed || e.SuperPressed) return false;
|
|
|
|
|
|
2018-11-30 13:57:25 +08:00
|
|
|
|
switch (e.Key)
|
2017-10-02 08:26:41 +08:00
|
|
|
|
{
|
2018-11-30 13:57:25 +08:00
|
|
|
|
case Key.Left:
|
2018-11-30 14:47:55 +08:00
|
|
|
|
seek(e, -1);
|
2018-11-30 13:57:25 +08:00
|
|
|
|
return true;
|
2019-04-01 11:16:05 +08:00
|
|
|
|
|
2018-11-30 13:57:25 +08:00
|
|
|
|
case Key.Right:
|
2018-11-30 14:47:55 +08:00
|
|
|
|
seek(e, 1);
|
2018-11-30 13:57:25 +08:00
|
|
|
|
return true;
|
2022-05-14 01:50:51 +08:00
|
|
|
|
|
2022-11-24 12:55:58 +08:00
|
|
|
|
// Of those, these two keys are reversed from stable because it feels more natural (and matches mouse wheel scroll directionality).
|
|
|
|
|
case Key.Up:
|
2022-11-23 14:59:31 +08:00
|
|
|
|
seekControlPoint(-1);
|
|
|
|
|
return true;
|
|
|
|
|
|
2022-11-24 12:55:58 +08:00
|
|
|
|
case Key.Down:
|
2022-11-23 14:59:31 +08:00
|
|
|
|
seekControlPoint(1);
|
|
|
|
|
return true;
|
|
|
|
|
|
2022-05-14 01:50:51 +08:00
|
|
|
|
// Track traversal keys.
|
|
|
|
|
// Matching osu-stable implementations.
|
|
|
|
|
case Key.Z:
|
2023-06-14 01:29:56 +08:00
|
|
|
|
if (e.Repeat)
|
|
|
|
|
return false;
|
|
|
|
|
|
2022-05-14 01:50:51 +08:00
|
|
|
|
// 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:
|
2023-06-14 01:29:56 +08:00
|
|
|
|
if (e.Repeat)
|
|
|
|
|
return false;
|
|
|
|
|
|
2022-05-14 01:50:51 +08:00
|
|
|
|
// Restart playback from beginning of track.
|
|
|
|
|
clock.Seek(0);
|
|
|
|
|
clock.Start();
|
|
|
|
|
return true;
|
|
|
|
|
|
|
|
|
|
case Key.C:
|
2023-06-14 01:29:56 +08:00
|
|
|
|
if (e.Repeat)
|
|
|
|
|
return false;
|
|
|
|
|
|
2022-05-14 01:50:51 +08:00
|
|
|
|
// Pause or resume.
|
|
|
|
|
if (clock.IsRunning)
|
|
|
|
|
clock.Stop();
|
|
|
|
|
else
|
|
|
|
|
clock.Start();
|
|
|
|
|
return true;
|
|
|
|
|
|
|
|
|
|
case Key.V:
|
2023-06-14 01:29:56 +08:00
|
|
|
|
if (e.Repeat)
|
|
|
|
|
return false;
|
|
|
|
|
|
2022-05-14 01:50:51 +08:00
|
|
|
|
// 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.
|
2022-12-02 15:07:24 +08:00
|
|
|
|
if (!editorBeatmap.HitObjects.Any())
|
|
|
|
|
{
|
2022-05-14 01:50:51 +08:00
|
|
|
|
clock.Seek(clock.TrackLength);
|
2022-12-02 15:07:24 +08:00
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
double lastObjectTime = editorBeatmap.GetLastObjectTime();
|
|
|
|
|
clock.Seek(clock.CurrentTime == lastObjectTime ? clock.TrackLength : lastObjectTime);
|
2022-05-14 01:50:51 +08:00
|
|
|
|
return true;
|
2017-10-02 08:26:41 +08:00
|
|
|
|
}
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2018-11-30 13:57:25 +08:00
|
|
|
|
return base.OnKeyDown(e);
|
2017-10-02 08:26:41 +08:00
|
|
|
|
}
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2018-11-22 18:40:44 +08:00
|
|
|
|
private double scrollAccumulation;
|
|
|
|
|
|
2018-10-02 11:02:47 +08:00
|
|
|
|
protected override bool OnScroll(ScrollEvent e)
|
2018-04-06 16:40:06 +08:00
|
|
|
|
{
|
2020-11-25 15:46:19 +08:00
|
|
|
|
if (e.ControlPressed || e.AltPressed || e.SuperPressed)
|
|
|
|
|
return false;
|
|
|
|
|
|
2020-07-17 15:03:13 +08:00
|
|
|
|
const double precision = 1;
|
2018-11-22 18:40:44 +08:00
|
|
|
|
|
2020-07-17 15:03:13 +08:00
|
|
|
|
double scrollComponent = e.ScrollDelta.X + e.ScrollDelta.Y;
|
2018-11-22 19:13:40 +08:00
|
|
|
|
|
2020-07-17 15:03:13 +08:00
|
|
|
|
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.
|
2020-08-31 17:01:16 +08:00
|
|
|
|
if (scrollAccumulation != 0 && Math.Sign(scrollAccumulation) != scrollDirection)
|
2020-07-17 15:03:13 +08:00
|
|
|
|
scrollAccumulation = scrollDirection * (precision - Math.Abs(scrollAccumulation));
|
|
|
|
|
|
2022-05-24 02:22:27 +08:00
|
|
|
|
scrollAccumulation += scrollComponent;
|
2020-07-17 15:03:13 +08:00
|
|
|
|
|
|
|
|
|
// 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)
|
2018-11-22 19:13:40 +08:00
|
|
|
|
{
|
|
|
|
|
if (scrollAccumulation > 0)
|
2018-11-30 14:47:55 +08:00
|
|
|
|
seek(e, -1);
|
2018-11-22 19:13:40 +08:00
|
|
|
|
else
|
2018-11-30 14:47:55 +08:00
|
|
|
|
seek(e, 1);
|
2018-11-22 19:13:40 +08:00
|
|
|
|
|
|
|
|
|
scrollAccumulation = scrollAccumulation < 0 ? Math.Min(0, scrollAccumulation + precision) : Math.Max(0, scrollAccumulation - precision);
|
|
|
|
|
}
|
2018-11-22 18:40:44 +08:00
|
|
|
|
|
2018-04-06 16:40:06 +08:00
|
|
|
|
return true;
|
|
|
|
|
}
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2021-09-16 17:26:12 +08:00
|
|
|
|
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
2019-06-30 18:31:31 +08:00
|
|
|
|
{
|
2021-11-18 11:36:15 +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
|
|
|
|
{
|
2020-09-22 14:55:25 +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
|
|
|
|
|
2022-10-25 10:43:23 +08:00
|
|
|
|
case GlobalAction.EditorCloneSelection:
|
|
|
|
|
Clone();
|
2022-10-24 12:58:11 +08:00
|
|
|
|
return true;
|
|
|
|
|
|
2020-09-22 14:55:25 +08:00
|
|
|
|
case GlobalAction.EditorComposeMode:
|
2022-03-02 18:19:39 +08:00
|
|
|
|
Mode.Value = EditorScreenMode.Compose;
|
2020-09-22 14:55:25 +08:00
|
|
|
|
return true;
|
|
|
|
|
|
|
|
|
|
case GlobalAction.EditorDesignMode:
|
2022-03-02 18:19:39 +08:00
|
|
|
|
Mode.Value = EditorScreenMode.Design;
|
2020-09-22 14:55:25 +08:00
|
|
|
|
return true;
|
|
|
|
|
|
|
|
|
|
case GlobalAction.EditorTimingMode:
|
2022-03-02 18:19:39 +08:00
|
|
|
|
Mode.Value = EditorScreenMode.Timing;
|
2020-09-22 14:55:25 +08:00
|
|
|
|
return true;
|
|
|
|
|
|
|
|
|
|
case GlobalAction.EditorSetupMode:
|
2022-03-02 18:19:39 +08:00
|
|
|
|
Mode.Value = EditorScreenMode.SongSetup;
|
2020-09-22 14:55:25 +08:00
|
|
|
|
return true;
|
|
|
|
|
|
2021-03-28 23:36:22 +08:00
|
|
|
|
case GlobalAction.EditorVerifyMode:
|
2022-03-02 18:19:39 +08:00
|
|
|
|
Mode.Value = EditorScreenMode.Verify;
|
2021-03-28 23:36:22 +08:00
|
|
|
|
return true;
|
|
|
|
|
|
2021-11-12 13:13:11 +08:00
|
|
|
|
case GlobalAction.EditorTestGameplay:
|
2022-05-25 22:10:58 +08:00
|
|
|
|
bottomBar.TestGameplayButton.TriggerClick();
|
2021-11-12 13:13:11 +08:00
|
|
|
|
return true;
|
|
|
|
|
|
2020-09-22 14:55:25 +08:00
|
|
|
|
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)
|
2020-01-22 12:22:34 +08:00
|
|
|
|
{
|
|
|
|
|
}
|
2019-06-30 18:31:31 +08:00
|
|
|
|
|
2022-04-21 23:52:44 +08:00
|
|
|
|
public override void OnEntering(ScreenTransitionEvent e)
|
2016-10-05 19:03:52 +08:00
|
|
|
|
{
|
2022-04-21 23:52:44 +08:00
|
|
|
|
base.OnEntering(e);
|
2021-11-13 20:50:57 +08:00
|
|
|
|
dimBackground();
|
|
|
|
|
resetTrack(true);
|
|
|
|
|
}
|
|
|
|
|
|
2022-04-21 23:52:44 +08:00
|
|
|
|
public override void OnResuming(ScreenTransitionEvent e)
|
2021-11-13 20:50:57 +08:00
|
|
|
|
{
|
2022-04-21 23:52:44 +08:00
|
|
|
|
base.OnResuming(e);
|
2021-11-13 20:50:57 +08:00
|
|
|
|
dimBackground();
|
|
|
|
|
}
|
2019-07-10 23:22:40 +08:00
|
|
|
|
|
2021-11-13 20:50:57 +08:00
|
|
|
|
private void dimBackground()
|
|
|
|
|
{
|
2021-01-04 17:32:23 +08:00
|
|
|
|
ApplyToBackground(b =>
|
|
|
|
|
{
|
2021-04-13 14:24:35 +08:00
|
|
|
|
b.IgnoreUserSettings.Value = true;
|
2022-11-02 16:49:52 +08:00
|
|
|
|
b.DimWhenUserSettingsIgnored.Value = editorBackgroundDim.Value;
|
2021-01-04 17:32:23 +08:00
|
|
|
|
b.BlurAmount.Value = 0;
|
|
|
|
|
});
|
2016-10-05 19:03:52 +08:00
|
|
|
|
}
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2022-04-21 23:52:44 +08:00
|
|
|
|
public override bool OnExiting(ScreenExitEvent e)
|
2016-10-05 19:03:52 +08:00
|
|
|
|
{
|
2022-03-22 14:28:31 +08:00
|
|
|
|
if (!ExitConfirmed)
|
2020-09-09 18:31:18 +08:00
|
|
|
|
{
|
2021-05-19 15:28:25 +08:00
|
|
|
|
// dialog overlay may not be available in visual tests.
|
|
|
|
|
if (dialogOverlay == null)
|
2020-10-04 22:57:28 +08:00
|
|
|
|
{
|
|
|
|
|
confirmExit();
|
2021-05-19 15:28:25 +08:00
|
|
|
|
return true;
|
2020-10-04 22:57:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
2022-03-22 14:28:31 +08:00
|
|
|
|
// if the dialog is already displayed, block exiting until the user explicitly makes a decision.
|
2023-09-28 14:15:09 +08:00
|
|
|
|
if (dialogOverlay.CurrentDialog is PromptForSaveDialog saveDialog)
|
|
|
|
|
{
|
|
|
|
|
saveDialog.Flash();
|
2021-05-19 15:28:25 +08:00
|
|
|
|
return true;
|
2023-09-28 14:15:09 +08:00
|
|
|
|
}
|
2020-10-28 12:32:39 +08:00
|
|
|
|
|
2021-05-19 15:28:25 +08:00
|
|
|
|
if (isNewBeatmap || HasUnsavedChanges)
|
|
|
|
|
{
|
2022-11-09 16:42:33 +08:00
|
|
|
|
updateSampleDisabledState();
|
2021-09-07 03:27:17 +08:00
|
|
|
|
dialogOverlay?.Push(new PromptForSaveDialog(confirmExit, confirmExitWithSave, cancelExit));
|
2020-10-04 22:57:28 +08:00
|
|
|
|
return true;
|
|
|
|
|
}
|
2020-09-09 18:31:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
2023-06-06 14:11:31 +08:00
|
|
|
|
realm.Write(r =>
|
|
|
|
|
{
|
|
|
|
|
var beatmap = r.Find<BeatmapInfo>(editorBeatmap.BeatmapInfo.ID);
|
|
|
|
|
if (beatmap != null)
|
|
|
|
|
beatmap.EditorTimestamp = clock.CurrentTime;
|
|
|
|
|
});
|
|
|
|
|
|
2022-10-24 22:18:34 +08:00
|
|
|
|
ApplyToBackground(b =>
|
|
|
|
|
{
|
2022-11-02 16:49:52 +08:00
|
|
|
|
b.DimWhenUserSettingsIgnored.Value = 0;
|
2022-10-24 22:18:34 +08:00
|
|
|
|
});
|
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
|
|
|
|
|
2021-11-11 06:11:25 +08:00
|
|
|
|
refetchBeatmap();
|
|
|
|
|
|
2022-04-21 23:52:44 +08:00
|
|
|
|
return base.OnExiting(e);
|
2021-11-11 06:11:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
2022-04-21 23:52:44 +08:00
|
|
|
|
public override void OnSuspending(ScreenTransitionEvent e)
|
2021-11-11 06:11:25 +08:00
|
|
|
|
{
|
2022-04-21 23:52:44 +08:00
|
|
|
|
base.OnSuspending(e);
|
2021-11-13 21:01:00 +08:00
|
|
|
|
clock.Stop();
|
|
|
|
|
refetchBeatmap();
|
2021-11-11 06:11:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void refetchBeatmap()
|
|
|
|
|
{
|
|
|
|
|
// To update the game-wide beatmap with any changes, perform a re-fetch on exit/suspend.
|
2021-05-20 14:39:29 +08:00
|
|
|
|
// This is required as the editor makes its local changes via EditorBeatmap
|
|
|
|
|
// (which are not propagated outwards to a potentially cached WorkingBeatmap).
|
2022-06-24 20:02:14 +08:00
|
|
|
|
var refetchedBeatmap = beatmapManager.GetWorkingBeatmap(Beatmap.Value.BeatmapInfo, true);
|
2021-05-19 16:58:28 +08:00
|
|
|
|
|
|
|
|
|
if (!(refetchedBeatmap is DummyWorkingBeatmap))
|
2021-10-14 12:58:36 +08:00
|
|
|
|
{
|
2023-01-15 06:50:41 +08:00
|
|
|
|
Logger.Log(@"Editor providing re-fetched beatmap post edit session");
|
2021-05-19 16:58:28 +08:00
|
|
|
|
Beatmap.Value = refetchedBeatmap;
|
2021-10-14 12:58:36 +08:00
|
|
|
|
}
|
2016-10-05 19:03:52 +08:00
|
|
|
|
}
|
2019-07-10 23:22:40 +08:00
|
|
|
|
|
2020-09-09 18:31:18 +08:00
|
|
|
|
private void confirmExitWithSave()
|
|
|
|
|
{
|
2023-08-05 13:05:59 +08:00
|
|
|
|
if (!Save()) return;
|
2021-05-19 15:28:25 +08:00
|
|
|
|
|
2022-03-22 14:28:31 +08:00
|
|
|
|
ExitConfirmed = true;
|
2021-05-19 15:28:25 +08:00
|
|
|
|
this.Exit();
|
2020-09-09 18:31:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void confirmExit()
|
|
|
|
|
{
|
2020-10-09 12:11:44 +08:00
|
|
|
|
// stop the track if playing to allow the parent screen to choose a suitable playback mode.
|
|
|
|
|
Beatmap.Value.Track.Stop();
|
|
|
|
|
|
2020-10-04 22:47:16 +08:00
|
|
|
|
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);
|
2020-10-09 12:11:44 +08:00
|
|
|
|
|
2020-11-08 03:31:44 +08:00
|
|
|
|
// eagerly clear contents before restoring default beatmap to prevent value change callbacks from firing.
|
|
|
|
|
ClearInternal();
|
|
|
|
|
|
2020-10-09 12:11:44 +08:00
|
|
|
|
// 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();
|
2020-10-04 22:47:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
2022-03-22 14:28:31 +08:00
|
|
|
|
ExitConfirmed = true;
|
2021-05-19 15:28:25 +08:00
|
|
|
|
this.Exit();
|
2020-09-09 18:31:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
2021-11-08 21:54:31 +08:00
|
|
|
|
#region Clipboard support
|
|
|
|
|
|
|
|
|
|
private EditorMenuItem cutMenuItem;
|
|
|
|
|
private EditorMenuItem copyMenuItem;
|
2022-10-26 03:34:41 +08:00
|
|
|
|
private EditorMenuItem cloneMenuItem;
|
2021-11-08 21:54:31 +08:00
|
|
|
|
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);
|
2022-10-24 12:58:11 +08:00
|
|
|
|
canCopy.Current.BindValueChanged(copy =>
|
|
|
|
|
{
|
|
|
|
|
copyMenuItem.Action.Disabled = !copy.NewValue;
|
2022-10-26 03:34:41 +08:00
|
|
|
|
cloneMenuItem.Action.Disabled = !copy.NewValue;
|
2022-10-24 12:58:11 +08:00
|
|
|
|
}, true);
|
2021-11-08 21:54:31 +08:00
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2021-11-08 21:24:39 +08:00
|
|
|
|
protected void Cut() => currentScreen?.Cut();
|
2020-09-12 14:33:13 +08:00
|
|
|
|
|
2021-11-08 21:24:39 +08:00
|
|
|
|
protected void Copy() => currentScreen?.Copy();
|
2020-09-11 18:54:20 +08:00
|
|
|
|
|
2022-10-25 10:43:23 +08:00
|
|
|
|
protected void Clone()
|
2022-10-24 12:58:11 +08:00
|
|
|
|
{
|
2022-10-25 10:42:12 +08:00
|
|
|
|
// Avoid attempting to clone if copying is not available (as it may result in pasting something unexpected).
|
|
|
|
|
if (!canCopy.Value)
|
|
|
|
|
return;
|
|
|
|
|
|
2022-10-24 12:58:11 +08:00
|
|
|
|
// 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();
|
|
|
|
|
}
|
|
|
|
|
|
2021-11-08 21:24:39 +08:00
|
|
|
|
protected void Paste() => currentScreen?.Paste();
|
2020-09-11 18:54:20 +08:00
|
|
|
|
|
2021-11-08 21:54:31 +08:00
|
|
|
|
#endregion
|
|
|
|
|
|
2022-01-28 13:01:31 +08:00
|
|
|
|
protected void Undo() => changeHandler?.RestoreState(-1);
|
2020-04-09 20:22:07 +08:00
|
|
|
|
|
2022-01-28 13:01:31 +08:00
|
|
|
|
protected void Redo() => changeHandler?.RestoreState(1);
|
2020-04-09 20:22:07 +08:00
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-08 17:51:01 +08:00
|
|
|
|
private void resetTrack(bool seekToStart = false)
|
2019-07-10 23:22:40 +08:00
|
|
|
|
{
|
2023-09-22 15:11:02 +08:00
|
|
|
|
clock.Stop();
|
2019-11-08 17:51:01 +08:00
|
|
|
|
|
|
|
|
|
if (seekToStart)
|
|
|
|
|
{
|
|
|
|
|
double targetTime = 0;
|
|
|
|
|
|
2023-06-06 14:11:31 +08:00
|
|
|
|
if (editorBeatmap.BeatmapInfo.EditorTimestamp != null)
|
2023-06-01 01:07:04 +08:00
|
|
|
|
{
|
2023-06-06 14:11:31 +08:00
|
|
|
|
targetTime = editorBeatmap.BeatmapInfo.EditorTimestamp.Value;
|
2023-06-01 01:07:04 +08:00
|
|
|
|
}
|
|
|
|
|
else if (Beatmap.Value.Beatmap.HitObjects.Count > 0)
|
2019-11-08 17:51:01 +08:00
|
|
|
|
{
|
|
|
|
|
// 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
|
|
|
|
}
|
2018-11-30 13:57:25 +08:00
|
|
|
|
|
2019-02-21 17:56:34 +08:00
|
|
|
|
private void onModeChanged(ValueChangedEvent<EditorScreenMode> e)
|
2018-11-30 13:57:25 +08:00
|
|
|
|
{
|
2020-09-24 16:03:54 +08:00
|
|
|
|
var lastScreen = currentScreen;
|
|
|
|
|
|
2021-08-25 21:58:06 +08:00
|
|
|
|
lastScreen?.Hide();
|
2020-09-24 16:03:54 +08:00
|
|
|
|
|
2020-10-27 13:31:56 +08:00
|
|
|
|
try
|
2020-09-24 16:03:54 +08:00
|
|
|
|
{
|
2020-10-27 13:31:56 +08:00
|
|
|
|
if ((currentScreen = screenContainer.SingleOrDefault(s => s.Type == e.NewValue)) != null)
|
|
|
|
|
{
|
|
|
|
|
screenContainer.ChangeChildDepth(currentScreen, lastScreen?.Depth + 1 ?? 0);
|
2022-05-24 16:23:42 +08:00
|
|
|
|
|
2021-08-25 21:58:06 +08:00
|
|
|
|
currentScreen.Show();
|
2020-10-27 13:31:56 +08:00
|
|
|
|
return;
|
|
|
|
|
}
|
2018-11-30 13:57:25 +08:00
|
|
|
|
|
2020-10-27 13:31:56 +08:00
|
|
|
|
switch (e.NewValue)
|
|
|
|
|
{
|
|
|
|
|
case EditorScreenMode.SongSetup:
|
|
|
|
|
currentScreen = new SetupScreen();
|
|
|
|
|
break;
|
2019-10-08 13:23:13 +08:00
|
|
|
|
|
2020-10-27 13:31:56 +08:00
|
|
|
|
case EditorScreenMode.Compose:
|
|
|
|
|
currentScreen = new ComposeScreen();
|
|
|
|
|
break;
|
2019-04-01 11:16:05 +08:00
|
|
|
|
|
2020-10-27 13:31:56 +08:00
|
|
|
|
case EditorScreenMode.Design:
|
|
|
|
|
currentScreen = new DesignScreen();
|
|
|
|
|
break;
|
2019-04-01 11:16:05 +08:00
|
|
|
|
|
2020-10-27 13:31:56 +08:00
|
|
|
|
case EditorScreenMode.Timing:
|
|
|
|
|
currentScreen = new TimingScreen();
|
|
|
|
|
break;
|
2021-03-28 23:36:22 +08:00
|
|
|
|
|
|
|
|
|
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");
|
2020-10-27 13:31:56 +08:00
|
|
|
|
}
|
2018-11-30 13:57:25 +08:00
|
|
|
|
|
2020-10-27 13:31:56 +08:00
|
|
|
|
LoadComponentAsync(currentScreen, newScreen =>
|
|
|
|
|
{
|
|
|
|
|
if (newScreen == currentScreen)
|
2021-08-25 21:58:06 +08:00
|
|
|
|
{
|
2020-10-27 13:31:56 +08:00
|
|
|
|
screenContainer.Add(newScreen);
|
2021-08-25 21:58:06 +08:00
|
|
|
|
newScreen.Show();
|
|
|
|
|
}
|
2020-10-27 13:31:56 +08:00
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
finally
|
2020-09-08 18:50:29 +08:00
|
|
|
|
{
|
2020-10-27 13:31:56 +08:00
|
|
|
|
updateSampleDisabledState();
|
2021-11-08 21:54:31 +08:00
|
|
|
|
rebindClipboardBindables();
|
2020-10-27 13:31:56 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-09 16:42:33 +08:00
|
|
|
|
[CanBeNull]
|
|
|
|
|
private ScheduledDelegate playbackDisabledDebounce;
|
|
|
|
|
|
2020-10-27 13:31:56 +08:00
|
|
|
|
private void updateSampleDisabledState()
|
|
|
|
|
{
|
2022-11-09 16:42:33 +08:00
|
|
|
|
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);
|
|
|
|
|
}
|
2018-11-30 13:57:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
2022-11-23 14:59:31 +08:00
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-30 14:47:55 +08:00
|
|
|
|
private void seek(UIEvent e, int direction)
|
2018-11-30 13:57:25 +08:00
|
|
|
|
{
|
2020-10-06 16:47:22 +08:00
|
|
|
|
double amount = e.ShiftPressed ? 4 : 1;
|
2018-11-30 13:57:25 +08:00
|
|
|
|
|
2020-10-16 12:07:00 +08:00
|
|
|
|
bool trackPlaying = clock.IsRunning;
|
|
|
|
|
|
|
|
|
|
if (trackPlaying)
|
|
|
|
|
{
|
2022-06-21 12:07:50 +08:00
|
|
|
|
// generally users are not looking to perform tiny seeks when the track is playing.
|
2020-10-16 12:07:00 +08:00
|
|
|
|
// this multiplication undoes the division that will be applied in the underlying seek operation.
|
2022-06-21 12:07:50 +08:00
|
|
|
|
// 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);
|
2020-10-16 12:07:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
2018-11-30 13:57:25 +08:00
|
|
|
|
if (direction < 1)
|
2020-10-16 12:07:00 +08:00
|
|
|
|
clock.SeekBackward(!trackPlaying, amount);
|
2018-11-30 13:57:25 +08:00
|
|
|
|
else
|
2020-10-16 12:07:00 +08:00
|
|
|
|
clock.SeekForward(!trackPlaying, amount);
|
2018-11-30 13:57:25 +08:00
|
|
|
|
}
|
2020-01-14 18:05:52 +08:00
|
|
|
|
|
2020-09-09 18:40:41 +08:00
|
|
|
|
private void updateLastSavedHash()
|
|
|
|
|
{
|
2022-01-28 13:01:31 +08:00
|
|
|
|
lastSavedHash = changeHandler?.CurrentStateHash;
|
2020-09-09 18:40:41 +08:00
|
|
|
|
}
|
|
|
|
|
|
2022-09-02 16:14:48 +08:00
|
|
|
|
private List<MenuItem> createFileMenuItems() => new List<MenuItem>
|
|
|
|
|
{
|
|
|
|
|
createDifficultyCreationMenu(),
|
|
|
|
|
createDifficultySwitchMenu(),
|
2023-11-21 13:24:10 +08:00
|
|
|
|
new OsuMenuItemSpacer(),
|
2023-01-15 06:50:41 +08:00
|
|
|
|
new EditorMenuItem(EditorStrings.DeleteDifficulty, MenuItemType.Standard, deleteDifficulty) { Action = { Disabled = Beatmap.Value.BeatmapSetInfo.Beatmaps.Count < 2 } },
|
2023-11-21 13:24:10 +08:00
|
|
|
|
new OsuMenuItemSpacer(),
|
2023-07-23 13:07:31 +08:00
|
|
|
|
new EditorMenuItem(WebCommonStrings.ButtonsSave, MenuItemType.Standard, () => Save()),
|
|
|
|
|
createExportMenu(),
|
2023-11-21 13:24:10 +08:00
|
|
|
|
new OsuMenuItemSpacer(),
|
2023-01-16 00:37:40 +08:00
|
|
|
|
new EditorMenuItem(CommonStrings.Exit, MenuItemType.Standard, this.Exit)
|
2022-09-02 16:14:48 +08:00
|
|
|
|
};
|
2022-01-23 23:29:00 +08:00
|
|
|
|
|
2023-07-23 13:05:44 +08:00
|
|
|
|
private EditorMenuItem createExportMenu()
|
2023-07-18 18:37:37 +08:00
|
|
|
|
{
|
|
|
|
|
var exportItems = new List<MenuItem>
|
|
|
|
|
{
|
2023-10-25 13:33:06 +08:00
|
|
|
|
new EditorMenuItem(EditorStrings.ExportForEditing, MenuItemType.Standard, () => exportBeatmap(false)) { Action = { Disabled = !RuntimeInfo.IsDesktop } },
|
|
|
|
|
new EditorMenuItem(EditorStrings.ExportForCompatibility, MenuItemType.Standard, () => exportBeatmap(true)) { Action = { Disabled = !RuntimeInfo.IsDesktop } },
|
2023-07-18 18:37:37 +08:00
|
|
|
|
};
|
|
|
|
|
|
2023-07-23 13:03:35 +08:00
|
|
|
|
return new EditorMenuItem(CommonStrings.Export) { Items = exportItems };
|
2023-07-18 18:37:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
2023-10-25 13:33:06 +08:00
|
|
|
|
private void exportBeatmap(bool legacy)
|
2022-09-02 16:07:09 +08:00
|
|
|
|
{
|
2023-10-25 13:33:06 +08:00
|
|
|
|
if (HasUnsavedChanges)
|
|
|
|
|
{
|
|
|
|
|
dialogOverlay.Push(new SaveRequiredPopupDialog("The beatmap will be saved in order to export it.", () =>
|
|
|
|
|
{
|
|
|
|
|
if (!Save()) return;
|
2022-09-02 16:07:09 +08:00
|
|
|
|
|
2023-10-25 13:33:06 +08:00
|
|
|
|
runExport();
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
runExport();
|
|
|
|
|
}
|
2023-08-05 13:05:59 +08:00
|
|
|
|
|
2023-10-25 13:33:06 +08:00
|
|
|
|
void runExport()
|
|
|
|
|
{
|
|
|
|
|
if (legacy)
|
|
|
|
|
beatmapManager.ExportLegacy(Beatmap.Value.BeatmapSetInfo);
|
|
|
|
|
else
|
|
|
|
|
beatmapManager.Export(Beatmap.Value.BeatmapSetInfo);
|
|
|
|
|
}
|
2023-07-11 08:30:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
2022-09-02 16:39:14 +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()
|
|
|
|
|
{
|
2022-09-02 16:39:14 +08:00
|
|
|
|
BeatmapInfo difficultyToDelete = playableBeatmap.BeatmapInfo;
|
|
|
|
|
|
|
|
|
|
var difficultiesBeforeDeletion = groupedOrderedBeatmaps.SelectMany(g => g).ToList();
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
2022-09-06 17:10:59 +08:00
|
|
|
|
Beatmap.Value = beatmapManager.GetWorkingBeatmap(nextToShow);
|
2022-09-02 16:39:14 +08:00
|
|
|
|
|
|
|
|
|
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>();
|
|
|
|
|
|
2022-02-04 10:06:18 +08:00
|
|
|
|
foreach (var ruleset in rulesets.AvailableRulesets)
|
2022-01-24 01:34:33 +08:00
|
|
|
|
rulesetItems.Add(new EditorMenuItem(ruleset.Name, MenuItemType.Standard, () => CreateNewDifficulty(ruleset)));
|
2022-01-23 23:34:02 +08:00
|
|
|
|
|
2023-01-15 06:50:41 +08:00
|
|
|
|
return new EditorMenuItem(EditorStrings.CreateNewDifficulty) { Items = rulesetItems };
|
2022-01-23 23:34:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
2022-01-24 01:34:33 +08:00
|
|
|
|
protected void CreateNewDifficulty(RulesetInfo rulesetInfo)
|
2022-02-07 00:52:59 +08:00
|
|
|
|
{
|
2023-11-07 02:52:46 +08:00
|
|
|
|
if (isNewBeatmap)
|
|
|
|
|
{
|
|
|
|
|
dialogOverlay.Push(new SaveRequiredPopupDialog("This beatmap will be saved in order to create another difficulty.", () =>
|
|
|
|
|
{
|
|
|
|
|
if (!Save())
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
CreateNewDifficulty(rulesetInfo);
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2022-02-07 00:52:59 +08:00
|
|
|
|
if (!rulesetInfo.Equals(editorBeatmap.BeatmapInfo.Ruleset))
|
|
|
|
|
{
|
2022-02-15 03:56:05 +08:00
|
|
|
|
switchToNewDifficulty(rulesetInfo, false);
|
2022-02-07 00:52:59 +08:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2022-02-15 03:56:05 +08:00
|
|
|
|
dialogOverlay.Push(new CreateNewDifficultyDialog(createCopy => switchToNewDifficulty(rulesetInfo, createCopy)));
|
2022-02-07 00:52:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
2022-02-15 03:56:05 +08:00
|
|
|
|
private void switchToNewDifficulty(RulesetInfo rulesetInfo, bool createCopy)
|
2022-06-15 16:49:09 +08:00
|
|
|
|
{
|
2022-06-15 17:44:02 +08:00
|
|
|
|
switchingDifficulty = true;
|
2022-06-15 16:49:09 +08:00
|
|
|
|
loader?.ScheduleSwitchToNewDifficulty(editorBeatmap.BeatmapInfo, rulesetInfo, createCopy, GetState(rulesetInfo));
|
|
|
|
|
}
|
2022-01-24 00:49:17 +08:00
|
|
|
|
|
2022-01-23 23:29:00 +08:00
|
|
|
|
private EditorMenuItem createDifficultySwitchMenu()
|
|
|
|
|
{
|
2021-09-06 03:00:19 +08:00
|
|
|
|
var difficultyItems = new List<MenuItem>();
|
|
|
|
|
|
2022-09-02 16:39:14 +08:00
|
|
|
|
foreach (var rulesetBeatmaps in groupedOrderedBeatmaps)
|
2021-09-06 03:00:19 +08:00
|
|
|
|
{
|
|
|
|
|
if (difficultyItems.Count > 0)
|
2023-11-21 13:24:10 +08:00
|
|
|
|
difficultyItems.Add(new OsuMenuItemSpacer());
|
2021-09-06 03:00:19 +08:00
|
|
|
|
|
2022-09-02 16:39:14 +08:00
|
|
|
|
foreach (var beatmap in rulesetBeatmaps)
|
2022-01-23 23:29:00 +08:00
|
|
|
|
{
|
|
|
|
|
bool isCurrentDifficulty = playableBeatmap.BeatmapInfo.Equals(beatmap);
|
|
|
|
|
difficultyItems.Add(new DifficultyMenuItem(beatmap, isCurrentDifficulty, SwitchToDifficulty));
|
|
|
|
|
}
|
2021-09-06 03:00:19 +08:00
|
|
|
|
}
|
2021-09-05 22:07:24 +08:00
|
|
|
|
|
2023-01-15 06:50:41 +08:00
|
|
|
|
return new EditorMenuItem(EditorStrings.ChangeDifficulty) { Items = difficultyItems };
|
2021-09-05 23:26:09 +08:00
|
|
|
|
}
|
2021-09-05 22:28:32 +08:00
|
|
|
|
|
2022-02-15 03:56:05 +08:00
|
|
|
|
protected void SwitchToDifficulty(BeatmapInfo nextBeatmap) => loader?.ScheduleSwitchToExistingDifficulty(nextBeatmap, GetState(nextBeatmap.Ruleset));
|
2021-09-06 01:11:46 +08:00
|
|
|
|
|
2021-09-29 19:29:27 +08:00
|
|
|
|
private void cancelExit()
|
|
|
|
|
{
|
2022-07-10 03:14:39 +08:00
|
|
|
|
updateSampleDisabledState();
|
2021-09-29 19:29:27 +08:00
|
|
|
|
loader?.CancelPendingDifficultySwitch();
|
|
|
|
|
}
|
2021-09-05 22:28:32 +08:00
|
|
|
|
|
2023-11-07 07:56:24 +08:00
|
|
|
|
public void HandleTimestamp(string timestamp)
|
2023-11-04 09:01:18 +08:00
|
|
|
|
{
|
2023-11-20 20:27:24 +08:00
|
|
|
|
if (!EditorTimestampParser.TryParse(timestamp, out var timeSpan, out string selection))
|
2023-11-05 04:17:58 +08:00
|
|
|
|
{
|
2023-11-21 14:09:33 +08:00
|
|
|
|
Schedule(() => notifications?.Post(new SimpleErrorNotification
|
2023-11-07 07:56:24 +08:00
|
|
|
|
{
|
|
|
|
|
Icon = FontAwesome.Solid.ExclamationTriangle,
|
2023-11-21 14:08:15 +08:00
|
|
|
|
Text = EditorStrings.FailedToParseEditorLink
|
2023-11-07 07:56:24 +08:00
|
|
|
|
}));
|
2023-11-05 04:17:58 +08:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-04 09:01:18 +08:00
|
|
|
|
editorBeatmap.SelectedHitObjects.Clear();
|
|
|
|
|
|
2023-11-07 19:23:22 +08:00
|
|
|
|
if (clock.IsRunning)
|
|
|
|
|
clock.Stop();
|
|
|
|
|
|
2023-11-20 20:27:24 +08:00
|
|
|
|
double position = timeSpan.Value.TotalMilliseconds;
|
2023-11-07 08:36:58 +08:00
|
|
|
|
|
2023-11-20 20:27:24 +08:00
|
|
|
|
if (string.IsNullOrEmpty(selection))
|
2023-11-04 09:01:18 +08:00
|
|
|
|
{
|
2023-11-07 08:36:58 +08:00
|
|
|
|
clock.SeekSmoothlyTo(position);
|
2023-11-04 09:01:18 +08:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-07 08:36:58 +08:00
|
|
|
|
// Seek to the next closest HitObject instead
|
2023-11-04 09:01:18 +08:00
|
|
|
|
HitObject nextObject = editorBeatmap.HitObjects.FirstOrDefault(x => x.StartTime >= position);
|
2023-11-05 04:17:58 +08:00
|
|
|
|
|
2023-11-07 08:36:58 +08:00
|
|
|
|
if (nextObject != null)
|
2023-11-04 09:01:18 +08:00
|
|
|
|
position = nextObject.StartTime;
|
|
|
|
|
|
2023-11-07 08:36:58 +08:00
|
|
|
|
clock.SeekSmoothlyTo(position);
|
2023-11-04 09:01:18 +08:00
|
|
|
|
|
2023-11-20 20:29:19 +08:00
|
|
|
|
Mode.Value = EditorScreenMode.Compose;
|
2023-11-04 09:01:18 +08:00
|
|
|
|
|
2023-11-20 20:27:24 +08:00
|
|
|
|
// Delegate handling the selection to the ruleset.
|
2023-11-20 20:37:29 +08:00
|
|
|
|
currentScreen.Dependencies.Get<HitObjectComposer>().SelectFromTimestamp(position, selection);
|
2023-11-04 09:01:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
2020-01-28 11:48:24 +08:00
|
|
|
|
public double SnapTime(double time, double? referenceTime) => editorBeatmap.SnapTime(time, referenceTime);
|
2020-01-23 13:39:56 +08:00
|
|
|
|
|
2020-01-23 14:31:56 +08:00
|
|
|
|
public double GetBeatLengthAtTime(double referenceTime) => editorBeatmap.GetBeatLengthAtTime(referenceTime);
|
2020-01-23 13:39:56 +08:00
|
|
|
|
|
|
|
|
|
public int BeatDivisor => beatDivisor.Value;
|
2022-05-22 21:15:53 +08:00
|
|
|
|
|
|
|
|
|
ControlPointInfo IBeatSyncProvider.ControlPoints => editorBeatmap.ControlPointInfo;
|
|
|
|
|
IClock IBeatSyncProvider.Clock => clock;
|
2022-08-02 16:54:42 +08:00
|
|
|
|
ChannelAmplitudes IHasAmplitudes.CurrentAmplitudes => Beatmap.Value.TrackLoaded ? Beatmap.Value.Track.CurrentAmplitudes : ChannelAmplitudes.Empty;
|
2022-07-20 03:38:23 +08:00
|
|
|
|
|
|
|
|
|
private partial class BeatmapEditorToast : Toast
|
|
|
|
|
{
|
|
|
|
|
public BeatmapEditorToast(LocalisableString value, string beatmapDisplayName)
|
2022-07-22 13:00:29 +08:00
|
|
|
|
: base(InputSettingsStrings.EditorSection, value, beatmapDisplayName)
|
|
|
|
|
{
|
|
|
|
|
}
|
2022-07-20 03:38:23 +08:00
|
|
|
|
}
|
2016-09-29 19:13:58 +08:00
|
|
|
|
}
|
|
|
|
|
}
|