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
2022-06-17 15:37:17 +08:00
#nullable disable
2020-01-23 12:33:55 +08:00
using System ;
2018-05-23 11:00:11 +08:00
using osu.Framework.Allocation ;
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 ;
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 class Timeline : ZoomableScrollContainer , IPositionSnapProvider
2018-05-18 12:05:58 +08:00
{
2022-01-25 15:43:43 +08:00
private const float timeline_height = 72 ;
private const float timeline_expanded_height = 94 ;
2021-04-14 19:55:12 +08:00
private readonly Drawable userContent ;
2022-01-25 15:43:43 +08:00
2018-05-18 12:05:58 +08:00
public readonly Bindable < bool > WaveformVisible = new Bindable < bool > ( ) ;
2020-10-01 16:19:35 +08:00
public readonly Bindable < bool > ControlPointsVisible = new Bindable < bool > ( ) ;
2020-10-01 17:14:10 +08:00
public readonly Bindable < bool > TicksVisible = new Bindable < bool > ( ) ;
2018-06-11 19:08:17 +08:00
public readonly IBindable < WorkingBeatmap > Beatmap = new Bindable < WorkingBeatmap > ( ) ;
2018-05-18 12:05:58 +08:00
2020-02-14 21:14:00 +08:00
[Resolved]
2020-05-22 15:37:28 +08:00
private EditorClock editorClock { get ; set ; }
2018-05-23 11:00:11 +08:00
2022-07-22 14:21:25 +08:00
[Resolved]
private EditorBeatmap editorBeatmap { get ; set ; }
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 ;
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
}
private WaveformGraph waveform ;
2018-05-18 16:53:09 +08:00
2020-10-01 17:14:10 +08:00
private TimelineTickDisplay ticks ;
2018-05-18 16:53:09 +08:00
2020-10-01 16:54:54 +08:00
private TimelineControlPointDisplay controlPoints ;
2021-04-14 19:11:16 +08:00
private Container mainContent ;
2020-11-03 15:07:01 +08:00
private Bindable < float > waveformOpacity ;
2022-08-26 17:25:48 +08:00
private double trackLengthForZoom ;
2018-06-11 19:08:17 +08:00
[BackgroundDependencyLoader]
2022-07-22 14:21:25 +08:00
private void load ( IBindable < WorkingBeatmap > beatmap , OsuColour colours , 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
{
2021-04-14 18:39:12 +08:00
controlPoints = new TimelineControlPointDisplay
{
RelativeSizeAxes = Axes . X ,
Height = timeline_expanded_height ,
} ,
2021-04-14 19:11:16 +08:00
mainContent = new Container
2020-10-01 16:19:35 +08:00
{
2021-04-14 18:39:12 +08:00
RelativeSizeAxes = Axes . X ,
Height = timeline_height ,
2020-10-01 17:54:59 +08:00
Depth = float . MaxValue ,
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 ,
} ,
2021-04-14 13:51:52 +08:00
centreMarker . CreateProxy ( ) ,
2020-10-01 17:54:59 +08:00
ticks = new TimelineTickDisplay ( ) ,
2021-04-06 13:24:22 +08:00
new Box
{
Name = "zero marker" ,
RelativeSizeAxes = Axes . Y ,
Width = 2 ,
Origin = Anchor . TopCentre ,
Colour = colours . YellowDarker ,
} ,
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 ) ;
2021-04-15 23:59:01 +08:00
2021-04-14 18:39:12 +08:00
Beatmap . BindTo ( beatmap ) ;
2021-04-15 23:59:01 +08:00
Beatmap . BindValueChanged ( b = >
{
waveform . Waveform = b . NewValue . Waveform ;
} , 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
}
protected override void LoadComplete ( )
{
base . LoadComplete ( ) ;
2022-05-24 15:02:01 +08:00
WaveformVisible . BindValueChanged ( _ = > updateWaveformOpacity ( ) ) ;
2020-11-03 15:07:01 +08:00
waveformOpacity . BindValueChanged ( _ = > updateWaveformOpacity ( ) , true ) ;
2022-05-24 15:02:01 +08:00
TicksVisible . BindValueChanged ( visible = > ticks . FadeTo ( visible . NewValue ? 1 : 0 , 200 , Easing . OutQuint ) , true ) ;
2021-04-14 18:39:12 +08:00
ControlPointsVisible . BindValueChanged ( visible = >
{
if ( visible . NewValue )
{
this . ResizeHeightTo ( timeline_expanded_height , 200 , Easing . OutQuint ) ;
2021-09-14 17:33:24 +08:00
mainContent . MoveToY ( 20 , 200 , Easing . OutQuint ) ;
2021-04-14 18:39:12 +08:00
// delay the fade in else masking looks weird.
controlPoints . Delay ( 180 ) . FadeIn ( 400 , Easing . OutQuint ) ;
}
else
{
controlPoints . FadeOut ( 200 , Easing . OutQuint ) ;
// likewise, delay the resize until the fade is complete.
this . Delay ( 180 ) . ResizeHeightTo ( timeline_height , 200 , Easing . OutQuint ) ;
2021-04-14 19:11:16 +08:00
mainContent . Delay ( 180 ) . MoveToY ( 0 , 200 , Easing . OutQuint ) ;
2021-04-14 18:39:12 +08:00
}
} , true ) ;
2018-05-23 11:00:11 +08:00
}
2020-11-03 15:07:01 +08:00
private void updateWaveformOpacity ( ) = >
waveform . FadeTo ( WaveformVisible . Value ? waveformOpacity . Value : 0 , 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 ) ;
2022-09-18 01:51:44 +08:00
float initialZoom = ( float ) ( defaultTimelineZoom * ( editorBeatmap . BeatmapInfo . TimelineZoom = = 0 ? 1 : editorBeatmap . BeatmapInfo . TimelineZoom ) ) ;
2022-08-26 17:25:48 +08:00
float minimumZoom = getZoomLevelForVisibleMilliseconds ( 10000 ) ;
float maximumZoom = getZoomLevelForVisibleMilliseconds ( 500 ) ;
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-08-26 17:25:48 +08:00
double target = Current / Content . DrawWidth * editorClock . TrackLength ;
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-08-26 17:25:48 +08:00
ScrollTo ( ( float ) ( editorClock . CurrentTime / editorClock . TrackLength ) * Content . DrawWidth , 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]
2020-01-23 12:33:55 +08:00
private IBeatSnapProvider beatSnapProvider { get ; set ; }
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
}
}