2019-01-24 17:43:03 +09: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 13:05:58 +09:00
2020-01-23 13:33:55 +09:00
using System ;
2018-05-23 12:00:11 +09:00
using osu.Framework.Allocation ;
2018-06-28 14:08:15 +09:00
using osu.Framework.Audio.Track ;
2019-02-21 19:04:31 +09:00
using osu.Framework.Bindables ;
2018-06-18 04:31:47 +09:00
using osu.Framework.Extensions.Color4Extensions ;
2018-05-18 13:05:58 +09:00
using osu.Framework.Graphics ;
2018-06-11 20:08:17 +09:00
using osu.Framework.Graphics.Audio ;
2020-10-01 18:54:59 +09:00
using osu.Framework.Graphics.Containers ;
2021-04-06 14:24:22 +09:00
using osu.Framework.Graphics.Shapes ;
2018-10-02 12:02:47 +09:00
using osu.Framework.Input.Events ;
2018-05-18 13:05:58 +09:00
using osu.Game.Beatmaps ;
2020-11-03 16:07:01 +09:00
using osu.Game.Configuration ;
2018-05-18 13:05:58 +09:00
using osu.Game.Graphics ;
2020-01-21 18:00:36 +09:00
using osu.Game.Rulesets.Edit ;
2021-09-01 18:05:10 +09:00
using osu.Game.Rulesets.Objects ;
2020-01-21 18:00:36 +09:00
using osuTK ;
2018-05-18 13:05:58 +09:00
2018-11-06 18:28:22 +09:00
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
2018-05-18 13:05:58 +09:00
{
2020-05-20 17:48:43 +09:00
[Cached(typeof(IPositionSnapProvider))]
2020-01-23 12:29:32 +09:00
[Cached]
2020-05-20 17:48:43 +09:00
public class Timeline : ZoomableScrollContainer , IPositionSnapProvider
2018-05-18 13:05:58 +09:00
{
2021-04-14 20:55:12 +09:00
private readonly Drawable userContent ;
2018-05-18 13:05:58 +09:00
public readonly Bindable < bool > WaveformVisible = new Bindable < bool > ( ) ;
2020-10-01 17:19:35 +09:00
public readonly Bindable < bool > ControlPointsVisible = new Bindable < bool > ( ) ;
2020-10-01 18:14:10 +09:00
public readonly Bindable < bool > TicksVisible = new Bindable < bool > ( ) ;
2018-06-11 20:08:17 +09:00
public readonly IBindable < WorkingBeatmap > Beatmap = new Bindable < WorkingBeatmap > ( ) ;
2018-05-18 13:05:58 +09:00
2020-02-14 20:14:00 +07:00
[Resolved]
2020-05-22 16:37:28 +09:00
private EditorClock editorClock { get ; set ; }
2018-05-23 12:00:11 +09:00
2020-08-21 14:46:23 +09: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 ;
2020-08-21 16:58:45 +09:00
private Track track ;
2018-05-23 12:00:11 +09:00
2021-04-14 20:11:16 +09:00
private const float timeline_height = 72 ;
2021-09-14 18:33:24 +09:00
private const float timeline_expanded_height = 94 ;
2021-04-14 16:00:49 +09:00
2021-04-14 20:55:12 +09:00
public Timeline ( Drawable userContent )
2018-05-18 13:05:58 +09:00
{
2021-04-14 20:55:12 +09:00
this . userContent = userContent ;
2021-04-14 16:00:49 +09:00
RelativeSizeAxes = Axes . X ;
2021-04-16 00:58:28 +09:00
Height = timeline_height ;
2021-04-14 16:00:49 +09:00
2018-05-18 17:53:09 +09:00
ZoomDuration = 200 ;
ZoomEasing = Easing . OutQuint ;
2018-06-12 15:51:48 +09:00
ScrollbarVisible = false ;
2018-06-11 20:08:17 +09:00
}
private WaveformGraph waveform ;
2018-05-18 17:53:09 +09:00
2020-10-01 18:14:10 +09:00
private TimelineTickDisplay ticks ;
2018-05-18 17:53:09 +09:00
2020-10-01 17:54:54 +09:00
private TimelineControlPointDisplay controlPoints ;
2021-04-14 20:11:16 +09:00
private Container mainContent ;
2020-11-03 16:07:01 +09:00
private Bindable < float > waveformOpacity ;
2018-06-11 20:08:17 +09:00
[BackgroundDependencyLoader]
2020-11-03 16:07:01 +09:00
private void load ( IBindable < WorkingBeatmap > beatmap , OsuColour colours , OsuConfigManager config )
2018-06-11 20:08:17 +09:00
{
2021-04-14 14:51:52 +09:00
CentreMarker centreMarker ;
// We don't want the centre marker to scroll
AddInternal ( centreMarker = new CentreMarker ( ) ) ;
2020-10-01 17:19:35 +09:00
AddRange ( new Drawable [ ]
2018-05-18 13:05:58 +09:00
{
2021-04-14 19:39:12 +09:00
controlPoints = new TimelineControlPointDisplay
{
RelativeSizeAxes = Axes . X ,
Height = timeline_expanded_height ,
} ,
2021-04-14 20:11:16 +09:00
mainContent = new Container
2020-10-01 17:19:35 +09:00
{
2021-04-14 19:39:12 +09:00
RelativeSizeAxes = Axes . X ,
Height = timeline_height ,
2020-10-01 18:54:59 +09:00
Depth = float . MaxValue ,
2021-04-14 14:51:52 +09:00
Children = new [ ]
2020-10-01 18:54:59 +09:00
{
waveform = new WaveformGraph
{
RelativeSizeAxes = Axes . Both ,
2020-10-19 16:57:08 +09:00
BaseColour = colours . Blue . Opacity ( 0.2f ) ,
2020-10-01 18:54:59 +09:00
LowColour = colours . BlueLighter ,
MidColour = colours . BlueDark ,
HighColour = colours . BlueDarker ,
} ,
2021-04-14 14:51:52 +09:00
centreMarker . CreateProxy ( ) ,
2020-10-01 18:54:59 +09:00
ticks = new TimelineTickDisplay ( ) ,
2021-04-06 14:24:22 +09:00
new Box
{
Name = "zero marker" ,
RelativeSizeAxes = Axes . Y ,
Width = 2 ,
Origin = Anchor . TopCentre ,
Colour = colours . YellowDarker ,
} ,
2021-04-14 20:55:12 +09:00
userContent ,
2020-10-01 18:54:59 +09:00
}
2020-10-01 17:19:35 +09:00
} ,
2019-12-06 11:26:50 +09:00
} ) ;
2018-05-18 13:05:58 +09:00
2020-11-03 16:07:01 +09:00
waveformOpacity = config . GetBindable < float > ( OsuSetting . EditorWaveformOpacity ) ;
2021-04-16 00:59:01 +09:00
2021-04-14 19:39:12 +09:00
Beatmap . BindTo ( beatmap ) ;
2021-04-16 00:59:01 +09:00
Beatmap . BindValueChanged ( b = >
{
waveform . Waveform = b . NewValue . Waveform ;
track = b . NewValue . Track ;
// todo: i don't think this is safe, the track may not be loaded yet.
if ( track . Length > 0 )
{
MaxZoom = getZoomLevelForVisibleMilliseconds ( 500 ) ;
MinZoom = getZoomLevelForVisibleMilliseconds ( 10000 ) ;
Zoom = getZoomLevelForVisibleMilliseconds ( 2000 ) ;
}
} , true ) ;
2021-04-14 19:39:12 +09:00
}
protected override void LoadComplete ( )
{
base . LoadComplete ( ) ;
2020-11-03 16:07:01 +09:00
waveformOpacity . BindValueChanged ( _ = > updateWaveformOpacity ( ) , true ) ;
WaveformVisible . ValueChanged + = _ = > updateWaveformOpacity ( ) ;
2020-10-01 18:14:10 +09:00
TicksVisible . ValueChanged + = visible = > ticks . FadeTo ( visible . NewValue ? 1 : 0 , 200 , Easing . OutQuint ) ;
2021-04-14 19:39:12 +09:00
ControlPointsVisible . BindValueChanged ( visible = >
{
if ( visible . NewValue )
{
this . ResizeHeightTo ( timeline_expanded_height , 200 , Easing . OutQuint ) ;
2021-09-14 18:33:24 +09:00
mainContent . MoveToY ( 20 , 200 , Easing . OutQuint ) ;
2021-04-14 19:39:12 +09: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 20:11:16 +09:00
mainContent . Delay ( 180 ) . MoveToY ( 0 , 200 , Easing . OutQuint ) ;
2021-04-14 19:39:12 +09:00
}
} , true ) ;
2018-05-23 12:00:11 +09:00
}
2020-11-03 16:07:01 +09:00
private void updateWaveformOpacity ( ) = >
waveform . FadeTo ( WaveformVisible . Value ? waveformOpacity . Value : 0 , 200 , Easing . OutQuint ) ;
2018-05-23 14:14:32 +09:00
2020-08-24 20:00:24 +09:00
private float getZoomLevelForVisibleMilliseconds ( double milliseconds ) = > Math . Max ( 1 , ( float ) ( track . Length / milliseconds ) ) ;
2018-06-28 14:08:15 +09:00
2018-05-18 13:05:58 +09:00
protected override void Update ( )
{
base . Update ( ) ;
2018-05-23 14:14:32 +09: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 13:05:58 +09:00
Content . Margin = new MarginPadding { Horizontal = DrawWidth / 2 } ;
2018-05-23 12:00:11 +09:00
2018-06-25 20:31:06 +09:00
// This needs to happen after transforms are updated, but before the scroll position is updated in base.UpdateAfterChildren
2020-05-22 16:37:28 +09:00
if ( editorClock . IsRunning )
2018-06-25 20:31:06 +09:00
scrollToTrackTime ( ) ;
}
2021-01-15 15:47:41 +09: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 ) ;
}
2018-06-25 20:31:06 +09:00
protected override void UpdateAfterChildren ( )
{
base . UpdateAfterChildren ( ) ;
2018-05-23 14:14:32 +09:00
if ( handlingDragInput )
2018-05-23 14:32:00 +09:00
seekTrackToCurrent ( ) ;
2020-05-22 16:37:28 +09:00
else if ( ! editorClock . IsRunning )
2018-05-23 14:14:32 +09:00
{
2021-01-15 16:14:38 +09: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 14:14:32 +09:00
2021-01-21 17:40:15 +09: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 16:14:38 +09: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 14:32:00 +09:00
seekTrackToCurrent ( ) ;
2018-05-23 14:14:32 +09:00
else
2018-05-23 14:32:00 +09:00
scrollToTrackTime ( ) ;
2018-05-23 14:14:32 +09:00
}
2018-06-18 18:02:26 +09:00
lastScrollPosition = Current ;
2020-05-22 16:37:28 +09:00
lastTrackTime = editorClock . CurrentTime ;
2018-06-25 20:31:06 +09:00
}
2018-05-23 14:32:00 +09:00
2018-06-25 20:31:06 +09:00
private void seekTrackToCurrent ( )
{
2018-06-28 14:08:15 +09:00
if ( ! track . IsLoaded )
2018-06-25 20:31:06 +09:00
return ;
2018-06-18 19:27:08 +09:00
2020-03-23 22:37:53 -07:00
double target = Current / Content . DrawWidth * track . Length ;
2021-03-17 18:18:19 +01:00
editorClock . Seek ( Math . Min ( track . Length , target ) ) ;
2018-06-25 20:31:06 +09:00
}
2018-05-24 14:26:53 +09:00
2018-06-25 20:31:06 +09:00
private void scrollToTrackTime ( )
{
2020-08-21 16:58:45 +09:00
if ( ! track . IsLoaded | | track . Length = = 0 )
2018-06-25 20:31:06 +09:00
return ;
2018-06-18 19:27:08 +09:00
2020-11-03 14:58:55 +09: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 ( ) ;
2020-08-05 21:10:38 +09:00
ScrollTo ( ( float ) ( editorClock . CurrentTime / track . Length ) * Content . DrawWidth , false ) ;
2018-05-23 12:00:11 +09:00
}
2018-10-02 12:02:47 +09:00
protected override bool OnMouseDown ( MouseDownEvent e )
2018-05-23 12:00:11 +09:00
{
2018-10-02 12:02:47 +09:00
if ( base . OnMouseDown ( e ) )
2018-05-23 12:00:11 +09:00
{
2018-05-24 14:36:48 +09:00
beginUserDrag ( ) ;
2018-05-23 12:00:11 +09:00
return true ;
}
return false ;
}
2020-01-20 18:17:21 +09:00
protected override void OnMouseUp ( MouseUpEvent e )
2018-05-23 12:00:11 +09:00
{
2018-05-24 14:36:48 +09:00
endUserDrag ( ) ;
2020-01-20 18:17:21 +09:00
base . OnMouseUp ( e ) ;
2018-05-23 12:00:11 +09:00
}
2018-05-24 14:36:48 +09:00
private void beginUserDrag ( )
2018-05-23 12:00:11 +09:00
{
2018-05-23 14:14:32 +09:00
handlingDragInput = true ;
2020-05-22 16:37:28 +09:00
trackWasPlaying = editorClock . IsRunning ;
editorClock . Stop ( ) ;
2018-05-23 12:00:11 +09:00
}
2018-05-24 14:36:48 +09:00
private void endUserDrag ( )
2018-05-23 12:00:11 +09:00
{
2018-05-23 14:14:32 +09:00
handlingDragInput = false ;
2018-05-23 12:00:11 +09:00
if ( trackWasPlaying )
2020-05-22 16:37:28 +09:00
editorClock . Start ( ) ;
2018-05-23 12:00:11 +09:00
}
2020-01-21 18:00:36 +09:00
[Resolved]
2020-01-23 13:33:55 +09:00
private IBeatSnapProvider beatSnapProvider { get ; set ; }
2020-01-21 18:00:36 +09:00
2020-11-13 17:10:29 +09:00
/// <summary>
/// The total amount of time visible on the timeline.
/// </summary>
public double VisibleRange = > track . Length / Zoom ;
2020-11-24 17:14:39 +09:00
public SnapResult SnapScreenSpacePositionToValidPosition ( Vector2 screenSpacePosition ) = >
new SnapResult ( screenSpacePosition , null ) ;
2020-02-05 17:16:37 +09:00
2020-05-22 19:23:07 +09:00
public SnapResult SnapScreenSpacePositionToValidTime ( Vector2 screenSpacePosition ) = >
new SnapResult ( screenSpacePosition , beatSnapProvider . SnapTime ( getTimeFromPosition ( Content . ToLocalSpace ( screenSpacePosition ) ) ) ) ;
2020-02-05 17:16:37 +09:00
private double getTimeFromPosition ( Vector2 localPosition ) = >
( localPosition . X / Content . DrawWidth ) * track . Length ;
2020-01-21 18:00:36 +09:00
2021-09-01 18:05:10 +09:00
public float GetBeatSnapDistanceAt ( HitObject referenceObject ) = > throw new NotImplementedException ( ) ;
2020-01-21 18:00:36 +09:00
2021-09-01 18:05:10 +09:00
public float DurationToDistance ( HitObject referenceObject , double duration ) = > throw new NotImplementedException ( ) ;
2020-01-21 18:00:36 +09:00
2021-09-01 18:05:10 +09:00
public double DistanceToDuration ( HitObject referenceObject , float distance ) = > throw new NotImplementedException ( ) ;
2020-01-21 18:00:36 +09:00
2021-09-01 18:05:10 +09:00
public double GetSnappedDurationFromDistance ( HitObject referenceObject , float distance ) = > throw new NotImplementedException ( ) ;
2020-01-21 18:00:36 +09:00
2021-09-01 18:05:10 +09:00
public float GetSnappedDistanceFromDistance ( HitObject referenceObject , float distance ) = > throw new NotImplementedException ( ) ;
2018-05-18 13:05:58 +09:00
}
}