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-05-18 12:05:58 +08:00
2020-01-23 12:33:55 +08:00
using System ;
2018-05-23 11:00:11 +08:00
using osu.Framework.Allocation ;
2022-12-22 20:35:53 +08:00
using osu.Framework.Audio.Track ;
2019-02-21 18:04:31 +08:00
using osu.Framework.Bindables ;
2018-06-18 03:31:47 +08:00
using osu.Framework.Extensions.Color4Extensions ;
2018-05-18 12:05:58 +08:00
using osu.Framework.Graphics ;
2018-06-11 19:08:17 +08:00
using osu.Framework.Graphics.Audio ;
2020-10-01 17:54:59 +08:00
using osu.Framework.Graphics.Containers ;
2021-04-06 13:24:22 +08:00
using osu.Framework.Graphics.Shapes ;
2018-10-02 11:02:47 +08:00
using osu.Framework.Input.Events ;
2018-05-18 12:05:58 +08:00
using osu.Game.Beatmaps ;
2020-11-03 15:07:01 +08:00
using osu.Game.Configuration ;
2018-05-18 12:05:58 +08:00
using osu.Game.Graphics ;
2024-08-20 21:24:48 +08:00
using osu.Game.Overlays ;
2020-01-21 17:00:36 +08:00
using osu.Game.Rulesets.Edit ;
using osuTK ;
2022-05-29 19:56:51 +08:00
using osuTK.Input ;
2018-05-18 12:05:58 +08:00
2018-11-06 17:28:22 +08:00
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
2018-05-18 12:05:58 +08:00
{
2020-01-23 11:29:32 +08:00
[Cached]
2020-05-20 16:48:43 +08:00
public partial class Timeline : ZoomableScrollContainer , IPositionSnapProvider
2018-05-18 12:05:58 +08:00
{
2024-07-08 18:33:00 +08:00
private const float timeline_height = 80 ;
2022-01-25 15:43:43 +08:00
2021-04-14 19:55:12 +08:00
private readonly Drawable userContent ;
2022-01-25 15:43:43 +08:00
2024-08-22 00:05:47 +08:00
private bool alwaysShowControlPoints ;
public bool AlwaysShowControlPoints
{
get = > alwaysShowControlPoints ;
set
{
if ( value = = alwaysShowControlPoints )
return ;
alwaysShowControlPoints = value ;
controlPointsVisible . TriggerChange ( ) ;
}
}
2020-02-14 21:14:00 +08:00
[Resolved]
2024-07-31 18:28:30 +08:00
private EditorClock editorClock { get ; set ; } = null ! ;
2018-05-23 11:00:11 +08:00
2022-07-22 14:21:25 +08:00
[Resolved]
2024-07-31 18:28:30 +08:00
private EditorBeatmap editorBeatmap { get ; set ; } = null ! ;
2022-07-22 14:21:25 +08:00
2020-08-21 13:46:23 +08:00
/// <summary>
/// The timeline's scroll position in the last frame.
/// </summary>
private float lastScrollPosition ;
/// <summary>
/// The track time in the last frame.
/// </summary>
private double lastTrackTime ;
/// <summary>
/// Whether the user is currently dragging the timeline.
/// </summary>
private bool handlingDragInput ;
/// <summary>
/// Whether the track was playing before a user drag event.
/// </summary>
private bool trackWasPlaying ;
2022-01-25 15:43:43 +08:00
/// <summary>
/// The timeline zoom level at a 1x zoom scale.
/// </summary>
private float defaultTimelineZoom ;
2024-07-31 18:28:30 +08:00
private WaveformGraph waveform = null ! ;
private TimelineTickDisplay ticks = null ! ;
2024-08-22 00:05:47 +08:00
private TimelineTimingChangeDisplay controlPoints = null ! ;
2024-07-31 18:28:30 +08:00
private Bindable < float > waveformOpacity = null ! ;
2024-08-22 00:05:47 +08:00
private Bindable < bool > controlPointsVisible = null ! ;
2024-07-31 18:28:30 +08:00
private Bindable < bool > ticksVisible = null ! ;
private double trackLengthForZoom ;
private readonly IBindable < Track > track = new Bindable < Track > ( ) ;
2021-04-14 19:55:12 +08:00
public Timeline ( Drawable userContent )
2018-05-18 12:05:58 +08:00
{
2021-04-14 19:55:12 +08:00
this . userContent = userContent ;
2021-04-14 15:00:49 +08:00
RelativeSizeAxes = Axes . X ;
2021-04-15 23:58:28 +08:00
Height = timeline_height ;
2021-04-14 15:00:49 +08:00
2018-05-18 16:53:09 +08:00
ZoomDuration = 200 ;
ZoomEasing = Easing . OutQuint ;
2018-06-12 14:51:48 +08:00
ScrollbarVisible = false ;
2018-06-11 19:08:17 +08:00
}
[BackgroundDependencyLoader]
2024-08-20 21:24:48 +08:00
private void load ( IBindable < WorkingBeatmap > beatmap , OsuColour colours , OverlayColourProvider colourProvider , OsuConfigManager config )
2018-06-11 19:08:17 +08:00
{
2021-04-14 13:51:52 +08:00
CentreMarker centreMarker ;
// We don't want the centre marker to scroll
AddInternal ( centreMarker = new CentreMarker ( ) ) ;
2020-10-01 16:19:35 +08:00
AddRange ( new Drawable [ ]
2018-05-18 12:05:58 +08:00
{
2024-08-21 14:57:52 +08:00
ticks = new TimelineTickDisplay ( ) ,
2024-08-22 00:09:22 +08:00
new Box
{
Name = "zero marker" ,
RelativeSizeAxes = Axes . Y ,
Width = TimelineTickDisplay . TICK_WIDTH / 2 ,
Origin = Anchor . TopCentre ,
Colour = colourProvider . Background1 ,
} ,
2024-08-22 00:05:47 +08:00
controlPoints = new TimelineTimingChangeDisplay
2021-04-14 18:39:12 +08:00
{
2024-08-20 21:24:48 +08:00
RelativeSizeAxes = Axes . Both ,
Anchor = Anchor . CentreLeft ,
Origin = Anchor . CentreLeft ,
2021-04-14 18:39:12 +08:00
} ,
2024-08-21 14:57:52 +08:00
new Container
2020-10-01 16:19:35 +08:00
{
2021-04-14 18:39:12 +08:00
RelativeSizeAxes = Axes . X ,
Height = timeline_height ,
2021-04-14 13:51:52 +08:00
Children = new [ ]
2020-10-01 17:54:59 +08:00
{
waveform = new WaveformGraph
{
RelativeSizeAxes = Axes . Both ,
2020-10-19 15:57:08 +08:00
BaseColour = colours . Blue . Opacity ( 0.2f ) ,
2020-10-01 17:54:59 +08:00
LowColour = colours . BlueLighter ,
MidColour = colours . BlueDark ,
HighColour = colours . BlueDarker ,
} ,
2024-07-12 15:26:38 +08:00
centreMarker . CreateProxy ( ) ,
2024-08-20 21:24:48 +08:00
ticks . CreateProxy ( ) ,
2021-04-14 19:55:12 +08:00
userContent ,
2020-10-01 17:54:59 +08:00
}
2020-10-01 16:19:35 +08:00
} ,
2019-12-06 10:26:50 +08:00
} ) ;
2018-05-18 12:05:58 +08:00
2020-11-03 15:07:01 +08:00
waveformOpacity = config . GetBindable < float > ( OsuSetting . EditorWaveformOpacity ) ;
2024-08-22 00:05:47 +08:00
controlPointsVisible = config . GetBindable < bool > ( OsuSetting . EditorTimelineShowTimingChanges ) ;
2024-06-17 16:16:40 +08:00
ticksVisible = config . GetBindable < bool > ( OsuSetting . EditorTimelineShowTicks ) ;
2021-04-15 23:59:01 +08:00
2022-12-22 20:35:53 +08:00
track . BindTo ( editorClock . Track ) ;
2023-12-29 18:07:45 +08:00
track . BindValueChanged ( _ = >
2023-12-26 16:44:49 +08:00
{
waveform . Waveform = beatmap . Value . Waveform ;
2023-12-29 18:07:45 +08:00
Scheduler . AddOnce ( applyVisualOffset , beatmap ) ;
} , true ) ;
2022-01-25 15:43:43 +08:00
2022-07-22 14:21:25 +08:00
Zoom = ( float ) ( defaultTimelineZoom * editorBeatmap . BeatmapInfo . TimelineZoom ) ;
2021-04-14 18:39:12 +08:00
}
2023-12-29 18:07:45 +08:00
private void applyVisualOffset ( IBindable < WorkingBeatmap > beatmap )
{
waveform . RelativePositionAxes = Axes . X ;
if ( beatmap . Value . Track . Length > 0 )
waveform . X = - ( float ) ( Editor . WAVEFORM_VISUAL_OFFSET / beatmap . Value . Track . Length ) ;
else
{
// sometimes this can be the case immediately after a track switch.
// reschedule with the hope that the track length eventually populates.
Scheduler . AddOnce ( applyVisualOffset , beatmap ) ;
}
}
2021-04-14 18:39:12 +08:00
protected override void LoadComplete ( )
{
base . LoadComplete ( ) ;
2020-11-03 15:07:01 +08:00
waveformOpacity . BindValueChanged ( _ = > updateWaveformOpacity ( ) , true ) ;
2024-06-17 16:16:40 +08:00
ticksVisible . BindValueChanged ( visible = > ticks . FadeTo ( visible . NewValue ? 1 : 0 , 200 , Easing . OutQuint ) , true ) ;
2024-08-22 00:05:47 +08:00
controlPointsVisible . BindValueChanged ( visible = >
{
if ( visible . NewValue | | alwaysShowControlPoints )
controlPoints . FadeIn ( 400 , Easing . OutQuint ) ;
else
controlPoints . FadeOut ( 200 , Easing . OutQuint ) ;
} , true ) ;
2018-05-23 11:00:11 +08:00
}
2020-11-03 15:07:01 +08:00
private void updateWaveformOpacity ( ) = >
2024-06-17 16:16:40 +08:00
waveform . FadeTo ( waveformOpacity . Value , 200 , Easing . OutQuint ) ;
2018-05-23 13:14:32 +08:00
2018-05-18 12:05:58 +08:00
protected override void Update ( )
{
base . Update ( ) ;
2018-05-23 13:14:32 +08:00
// The extrema of track time should be positioned at the centre of the container when scrolled to the start or end
2018-05-18 12:05:58 +08:00
Content . Margin = new MarginPadding { Horizontal = DrawWidth / 2 } ;
2018-05-23 11:00:11 +08:00
2018-06-25 19:31:06 +08:00
// This needs to happen after transforms are updated, but before the scroll position is updated in base.UpdateAfterChildren
2020-05-22 15:37:28 +08:00
if ( editorClock . IsRunning )
2018-06-25 19:31:06 +08:00
scrollToTrackTime ( ) ;
2022-08-26 17:25:48 +08:00
if ( editorClock . TrackLength ! = trackLengthForZoom )
2022-07-25 16:54:47 +08:00
{
2022-08-26 17:25:48 +08:00
defaultTimelineZoom = getZoomLevelForVisibleMilliseconds ( 6000 ) ;
float minimumZoom = getZoomLevelForVisibleMilliseconds ( 10000 ) ;
float maximumZoom = getZoomLevelForVisibleMilliseconds ( 500 ) ;
2022-11-07 11:54:02 +08:00
float initialZoom = ( float ) Math . Clamp ( defaultTimelineZoom * ( editorBeatmap . BeatmapInfo . TimelineZoom = = 0 ? 1 : editorBeatmap . BeatmapInfo . TimelineZoom ) , minimumZoom , maximumZoom ) ;
2022-08-26 17:25:48 +08:00
SetupZoom ( initialZoom , minimumZoom , maximumZoom ) ;
2022-07-25 16:54:47 +08:00
2022-08-26 17:25:48 +08:00
float getZoomLevelForVisibleMilliseconds ( double milliseconds ) = > Math . Max ( 1 , ( float ) ( editorClock . TrackLength / milliseconds ) ) ;
2022-07-25 16:54:47 +08:00
2022-08-26 17:25:48 +08:00
trackLengthForZoom = editorClock . TrackLength ;
}
2022-07-25 16:54:47 +08:00
}
2021-01-15 14:47:41 +08:00
protected override bool OnScroll ( ScrollEvent e )
{
// if this is not a precision scroll event, let the editor handle the seek itself (for snapping support)
if ( ! e . AltPressed & & ! e . IsPrecise )
return false ;
return base . OnScroll ( e ) ;
}
2022-01-26 00:36:19 +08:00
protected override void OnZoomChanged ( )
2022-01-25 15:43:43 +08:00
{
2022-01-26 00:36:19 +08:00
base . OnZoomChanged ( ) ;
2022-07-22 14:21:25 +08:00
editorBeatmap . BeatmapInfo . TimelineZoom = Zoom / defaultTimelineZoom ;
2022-01-25 15:43:43 +08:00
}
2018-06-25 19:31:06 +08:00
protected override void UpdateAfterChildren ( )
{
base . UpdateAfterChildren ( ) ;
2018-05-23 13:14:32 +08:00
if ( handlingDragInput )
2018-05-23 13:32:00 +08:00
seekTrackToCurrent ( ) ;
2020-05-22 15:37:28 +08:00
else if ( ! editorClock . IsRunning )
2018-05-23 13:14:32 +08:00
{
2021-01-15 15:14:38 +08:00
// The track isn't running. There are three cases we have to be wary of:
// 1) The user flick-drags on this timeline and we are applying an interpolated seek on the clock, until interrupted by 2 or 3.
// 2) The user changes the track time through some other means (scrolling in the editor or overview timeline; clicking a hitobject etc.). We want the timeline to track the clock's time.
// 3) An ongoing seek transform is running from an external seek. We want the timeline to track the clock's time.
2018-05-23 13:14:32 +08:00
2021-01-21 16:40:15 +08:00
// The simplest way to cover the first two cases is by checking whether the scroll position has changed and the audio hasn't been changed externally
2021-01-15 15:14:38 +08:00
// Checking IsSeeking covers the third case, where the transform may not have been applied yet.
if ( Current ! = lastScrollPosition & & editorClock . CurrentTime = = lastTrackTime & & ! editorClock . IsSeeking )
2018-05-23 13:32:00 +08:00
seekTrackToCurrent ( ) ;
2018-05-23 13:14:32 +08:00
else
2018-05-23 13:32:00 +08:00
scrollToTrackTime ( ) ;
2018-05-23 13:14:32 +08:00
}
2018-06-18 17:02:26 +08:00
lastScrollPosition = Current ;
2020-05-22 15:37:28 +08:00
lastTrackTime = editorClock . CurrentTime ;
2018-06-25 19:31:06 +08:00
}
2018-05-23 13:32:00 +08:00
2018-06-25 19:31:06 +08:00
private void seekTrackToCurrent ( )
{
2022-10-30 17:21:50 +08:00
double target = TimeAtPosition ( Current ) ;
2022-08-26 17:25:48 +08:00
editorClock . Seek ( Math . Min ( editorClock . TrackLength , target ) ) ;
2018-06-25 19:31:06 +08:00
}
2018-05-24 13:26:53 +08:00
2018-06-25 19:31:06 +08:00
private void scrollToTrackTime ( )
{
2022-08-26 17:25:48 +08:00
if ( editorClock . TrackLength = = 0 )
2018-06-25 19:31:06 +08:00
return ;
2018-06-18 18:27:08 +08:00
2020-11-03 13:58:55 +08:00
// covers the case where the user starts playback after a drag is in progress.
// we want to ensure the clock is always stopped during drags to avoid weird audio playback.
if ( handlingDragInput )
editorClock . Stop ( ) ;
2022-10-30 17:21:50 +08:00
float position = PositionAtTime ( editorClock . CurrentTime ) ;
ScrollTo ( position , false ) ;
2018-05-23 11:00:11 +08:00
}
2018-10-02 11:02:47 +08:00
protected override bool OnMouseDown ( MouseDownEvent e )
2018-05-23 11:00:11 +08:00
{
2018-10-02 11:02:47 +08:00
if ( base . OnMouseDown ( e ) )
2018-05-24 13:36:48 +08:00
beginUserDrag ( ) ;
2018-05-23 11:00:11 +08:00
2022-05-29 19:56:51 +08:00
// handling right button as well breaks context menus inside the timeline, only handle left button for now.
return e . Button = = MouseButton . Left ;
2018-05-23 11:00:11 +08:00
}
2020-01-20 17:17:21 +08:00
protected override void OnMouseUp ( MouseUpEvent e )
2018-05-23 11:00:11 +08:00
{
2018-05-24 13:36:48 +08:00
endUserDrag ( ) ;
2020-01-20 17:17:21 +08:00
base . OnMouseUp ( e ) ;
2018-05-23 11:00:11 +08:00
}
2018-05-24 13:36:48 +08:00
private void beginUserDrag ( )
2018-05-23 11:00:11 +08:00
{
2018-05-23 13:14:32 +08:00
handlingDragInput = true ;
2020-05-22 15:37:28 +08:00
trackWasPlaying = editorClock . IsRunning ;
editorClock . Stop ( ) ;
2018-05-23 11:00:11 +08:00
}
2018-05-24 13:36:48 +08:00
private void endUserDrag ( )
2018-05-23 11:00:11 +08:00
{
2018-05-23 13:14:32 +08:00
handlingDragInput = false ;
2018-05-23 11:00:11 +08:00
if ( trackWasPlaying )
2020-05-22 15:37:28 +08:00
editorClock . Start ( ) ;
2018-05-23 11:00:11 +08:00
}
2020-01-21 17:00:36 +08:00
[Resolved]
2024-07-31 18:28:30 +08:00
private IBeatSnapProvider beatSnapProvider { get ; set ; } = null ! ;
2020-01-21 17:00:36 +08:00
2020-11-13 16:10:29 +08:00
/// <summary>
/// The total amount of time visible on the timeline.
/// </summary>
2022-08-26 17:25:48 +08:00
public double VisibleRange = > editorClock . TrackLength / Zoom ;
2020-11-13 16:10:29 +08:00
2022-10-05 19:43:02 +08:00
public double TimeAtPosition ( float x )
{
return x / Content . DrawWidth * editorClock . TrackLength ;
}
2020-02-05 16:16:37 +08:00
2022-10-05 19:43:02 +08:00
public float PositionAtTime ( double time )
{
return ( float ) ( time / editorClock . TrackLength * Content . DrawWidth ) ;
}
public SnapResult FindSnappedPositionAndTime ( Vector2 screenSpacePosition , SnapType snapType = SnapType . All )
{
double time = TimeAtPosition ( Content . ToLocalSpace ( screenSpacePosition ) . X ) ;
return new SnapResult ( screenSpacePosition , beatSnapProvider . SnapTime ( time ) ) ;
}
2018-05-18 12:05:58 +08:00
}
}