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 ;
2018-06-28 13:08:15 +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 ;
2020-01-21 17:00:36 +08:00
using osu.Game.Rulesets.Edit ;
using osuTK ;
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-05-20 16:48:43 +08:00
[Cached(typeof(IPositionSnapProvider))]
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
{
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
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 ;
2020-08-21 15:58:45 +08:00
private Track track ;
2018-05-23 11:00:11 +08:00
2018-05-18 12:05:58 +08:00
public Timeline ( )
{
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 ;
2020-11-03 15:07:01 +08:00
private Bindable < float > waveformOpacity ;
2018-06-11 19:08:17 +08:00
[BackgroundDependencyLoader]
2020-11-03 15:07:01 +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
{
2020-10-01 17:54:59 +08:00
new Container
2020-10-01 16:19:35 +08:00
{
RelativeSizeAxes = Axes . Both ,
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 ( ) ,
2020-10-01 18:31:41 +08:00
controlPoints = new TimelineControlPointDisplay ( ) ,
2021-04-06 13:24:22 +08:00
new Box
{
Name = "zero marker" ,
RelativeSizeAxes = Axes . Y ,
Width = 2 ,
Origin = Anchor . TopCentre ,
Colour = colours . YellowDarker ,
} ,
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 ) ;
waveformOpacity . BindValueChanged ( _ = > updateWaveformOpacity ( ) , true ) ;
WaveformVisible . ValueChanged + = _ = > updateWaveformOpacity ( ) ;
2020-10-01 16:19:35 +08:00
ControlPointsVisible . ValueChanged + = visible = > controlPoints . FadeTo ( visible . NewValue ? 1 : 0 , 200 , Easing . OutQuint ) ;
2020-10-01 17:14:10 +08:00
TicksVisible . ValueChanged + = visible = > ticks . FadeTo ( visible . NewValue ? 1 : 0 , 200 , Easing . OutQuint ) ;
2018-06-11 19:08:17 +08:00
Beatmap . BindTo ( beatmap ) ;
2019-02-22 19:13:38 +08:00
Beatmap . BindValueChanged ( b = >
2018-06-28 13:08:15 +08:00
{
2019-02-22 19:13:38 +08:00
waveform . Waveform = b . NewValue . Waveform ;
track = b . NewValue . Track ;
2020-01-28 12:02:29 +08:00
2020-08-21 13:46:23 +08:00
// todo: i don't think this is safe, the track may not be loaded yet.
2020-08-22 18:47:05 +08:00
if ( track . Length > 0 )
2020-04-24 09:27:32 +08:00
{
2020-08-05 20:10:38 +08:00
MaxZoom = getZoomLevelForVisibleMilliseconds ( 500 ) ;
MinZoom = getZoomLevelForVisibleMilliseconds ( 10000 ) ;
Zoom = getZoomLevelForVisibleMilliseconds ( 2000 ) ;
}
2018-06-28 13:08:15 +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
2020-08-24 19:00:24 +08:00
private float getZoomLevelForVisibleMilliseconds ( double milliseconds ) = > Math . Max ( 1 , ( float ) ( track . Length / milliseconds ) ) ;
2018-06-28 13:08:15 +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 ( ) ;
}
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 ) ;
}
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 ( )
{
2018-06-28 13:08:15 +08:00
if ( ! track . IsLoaded )
2018-06-25 19:31:06 +08:00
return ;
2018-06-18 18:27:08 +08:00
2020-03-24 13:37:53 +08:00
double target = Current / Content . DrawWidth * track . Length ;
2021-03-18 01:18:19 +08:00
editorClock . Seek ( Math . Min ( track . Length , 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 ( )
{
2020-08-21 15:58:45 +08:00
if ( ! track . IsLoaded | | track . Length = = 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 ( ) ;
2020-08-05 20:10:38 +08:00
ScrollTo ( ( float ) ( editorClock . CurrentTime / track . Length ) * 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-23 11:00:11 +08:00
{
2018-05-24 13:36:48 +08:00
beginUserDrag ( ) ;
2018-05-23 11:00:11 +08:00
return true ;
}
return false ;
}
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 EditorBeatmap beatmap { get ; set ; }
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>
public double VisibleRange = > track . Length / Zoom ;
2020-11-24 16:14:39 +08:00
public SnapResult SnapScreenSpacePositionToValidPosition ( Vector2 screenSpacePosition ) = >
new SnapResult ( screenSpacePosition , null ) ;
2020-02-05 16:16:37 +08:00
2020-05-22 18:23:07 +08:00
public SnapResult SnapScreenSpacePositionToValidTime ( Vector2 screenSpacePosition ) = >
new SnapResult ( screenSpacePosition , beatSnapProvider . SnapTime ( getTimeFromPosition ( Content . ToLocalSpace ( screenSpacePosition ) ) ) ) ;
2020-02-05 16:16:37 +08:00
private double getTimeFromPosition ( Vector2 localPosition ) = >
( localPosition . X / Content . DrawWidth ) * track . Length ;
2020-01-21 17:00:36 +08:00
2020-01-23 12:33:55 +08:00
public float GetBeatSnapDistanceAt ( double referenceTime ) = > throw new NotImplementedException ( ) ;
2020-01-21 17:00:36 +08:00
2020-01-23 12:33:55 +08:00
public float DurationToDistance ( double referenceTime , double duration ) = > throw new NotImplementedException ( ) ;
2020-01-21 17:00:36 +08:00
2020-01-23 12:33:55 +08:00
public double DistanceToDuration ( double referenceTime , float distance ) = > throw new NotImplementedException ( ) ;
2020-01-21 17:00:36 +08:00
2020-01-23 12:33:55 +08:00
public double GetSnappedDurationFromDistance ( double referenceTime , float distance ) = > throw new NotImplementedException ( ) ;
2020-01-21 17:00:36 +08:00
2020-01-23 12:33:55 +08:00
public float GetSnappedDistanceFromDistance ( double referenceTime , float distance ) = > throw new NotImplementedException ( ) ;
2018-05-18 12:05:58 +08:00
}
}