// Copyright Epic Games, Inc. All Rights Reserved. #include "AnimTimeline/AnimTimeSliderController.h" #include "Fonts/SlateFontInfo.h" #include "Rendering/DrawElements.h" #include "Misc/Paths.h" #include "Layout/WidgetPath.h" #include "Framework/Application/MenuStack.h" #include "Fonts/FontMeasure.h" #include "Styling/CoreStyle.h" #include "Framework/Application/SlateApplication.h" #include "Textures/SlateIcon.h" #include "Framework/Commands/UIAction.h" #include "Framework/MultiBox/MultiBoxBuilder.h" #include "Styling/AppStyle.h" #include "MovieSceneTimeHelpers.h" #include "CommonFrameRates.h" #include "AnimTimeline/AnimModel.h" #include "Preferences/PersonaOptions.h" #include "Animation/AnimMontage.h" #define LOCTEXT_NAMESPACE "AnimTimeSliderController" namespace ScrubConstants { /** The minimum amount of pixels between each major ticks on the widget */ const int32 MinPixelsPerDisplayTick = 12; /**The smallest number of units between between major tick marks */ const float MinDisplayTickSpacing = 0.001f; /**The fraction of the current view range to scroll per unit delta */ const float ScrollPanFraction = 0.1f; /** The margin we use for snapping editable times */ const double SnapMarginInPixels = 5.0; } /** Utility struct for converting between scrub range space and local/absolute screen space */ struct FAnimTimeSliderController::FScrubRangeToScreen { double ViewStart; float PixelsPerInput; FScrubRangeToScreen(const TRange& InViewInput, const FVector2D& InWidgetSize ) { const double ViewInputRange = InViewInput.Size(); ViewStart = InViewInput.GetLowerBoundValue(); PixelsPerInput = ViewInputRange > 0 ? static_cast( InWidgetSize.X / ViewInputRange ) : 0; } /** Local Widget Space -> Curve Input domain. */ double LocalXToInput(float ScreenX) const { return PixelsPerInput > 0 ? (ScreenX/PixelsPerInput) + ViewStart : ViewStart; } /** Curve Input domain -> local Widget Space */ float InputToLocalX(double Input) const { return static_cast((Input - ViewStart) * PixelsPerInput); } }; /** * Gets the the next spacing value in the series * to determine a good spacing value * E.g, .001,.005,.010,.050,.100,.500,1.000,etc */ static float GetNextSpacing( uint32 CurrentStep ) { if(CurrentStep & 0x01) { // Odd numbers return FMath::Pow( 10.f, 0.5f*((float)(CurrentStep-1)) + 1.f ); } else { // Even numbers return 0.5f * FMath::Pow( 10.f, 0.5f*((float)(CurrentStep)) + 1.f ); } } FAnimTimeSliderController::FAnimTimeSliderController( const FTimeSliderArgs& InArgs, TWeakPtr InWeakModel, TWeakPtr InWeakTimeline, TSharedPtr> InSecondaryNumericTypeInterface ) : WeakModel(InWeakModel) , WeakTimeline(InWeakTimeline) , TimeSliderArgs( InArgs ) , DistanceDragged( 0.0f ) , MouseDragType( DRAG_NONE ) , bPanning( false ) , DraggedTimeIndex(INDEX_NONE) , SecondaryNumericTypeInterface(InSecondaryNumericTypeInterface) { ScrubFillBrush = FAppStyle::GetBrush( TEXT( "Sequencer.Timeline.ScrubFill" ) ); ScrubHandleUpBrush = FAppStyle::GetBrush( TEXT( "Sequencer.Timeline.VanillaScrubHandleUp" ) ); ScrubHandleDownBrush = FAppStyle::GetBrush( TEXT( "Sequencer.Timeline.VanillaScrubHandleDown" ) ); EditableTimeBrush = FAppStyle::GetBrush( TEXT( "AnimTimeline.SectionMarker" ) ); } FFrameTime FAnimTimeSliderController::ComputeFrameTimeFromMouse(const FGeometry& Geometry, FVector2D ScreenSpacePosition, FScrubRangeToScreen RangeToScreen, bool CheckSnapping) const { const FVector2D CursorPos = Geometry.AbsoluteToLocal( ScreenSpacePosition ); const double MouseValue = RangeToScreen.LocalXToInput( static_cast(CursorPos.X) ); return MouseValue * GetTickResolution(); } FAnimTimeSliderController::FScrubPixelRange FAnimTimeSliderController::GetHitTestScrubberPixelRange(FFrameTime ScrubTime, const FScrubRangeToScreen& RangeToScreen) const { constexpr float DragToleranceSlateUnits = 2.f, MouseTolerance = 2.f; return GetScrubberPixelRange(ScrubTime, GetTickResolution(), GetDisplayRate(), RangeToScreen, DragToleranceSlateUnits + MouseTolerance); } FAnimTimeSliderController::FScrubPixelRange FAnimTimeSliderController::GetScrubberPixelRange(FFrameTime ScrubTime, const FScrubRangeToScreen& RangeToScreen) const { return GetScrubberPixelRange(ScrubTime, GetTickResolution(), GetDisplayRate(), RangeToScreen); } FAnimTimeSliderController::FScrubPixelRange FAnimTimeSliderController::GetScrubberPixelRange(FFrameTime ScrubTime, FFrameRate Resolution, FFrameRate PlayRate, const FScrubRangeToScreen& RangeToScreen, float DilationPixels) const { const FFrameNumber Frame = ScrubTime.FloorToFrame();// ConvertFrameTime(ScrubTime, Resolution, PlayRate).FloorToFrame(); float StartPixel = RangeToScreen.InputToLocalX( Frame / Resolution); float EndPixel = RangeToScreen.InputToLocalX( (Frame+1) / Resolution); { const float RoundedStartPixel = static_cast(FMath::RoundToInt(StartPixel)); EndPixel -= (StartPixel - RoundedStartPixel); StartPixel = RoundedStartPixel; EndPixel = FMath::Max(EndPixel, StartPixel + 1); } FScrubPixelRange Range; float MinScrubSize = 14.f; Range.bClamped = EndPixel - StartPixel < MinScrubSize; Range.Range = TRange(StartPixel, EndPixel); if (Range.bClamped) { Range.HandleRange = TRange( (StartPixel + EndPixel - MinScrubSize) * .5f, (StartPixel + EndPixel + MinScrubSize) * .5f); } else { Range.HandleRange = Range.Range; } return Range; } struct FAnimTimeSliderController::FDrawTickArgs { /** Geometry of the area */ FGeometry AllottedGeometry; /** Culling rect of the area */ FSlateRect CullingRect; /** Color of each tick */ FLinearColor TickColor; /** Offset in Y where to start the tick */ float TickOffset; /** Height in of major ticks */ float MajorTickHeight; /** Start layer for elements */ int32 StartLayer; /** Draw effects to apply */ ESlateDrawEffect DrawEffects; /** Whether or not to only draw major ticks */ bool bOnlyDrawMajorTicks; /** Whether or not to mirror labels */ bool bMirrorLabels; }; void FAnimTimeSliderController::DrawTicks( FSlateWindowElementList& OutDrawElements, const TRange& ViewRange, const FScrubRangeToScreen& RangeToScreen, FDrawTickArgs& InArgs ) const { TSharedPtr Timeline = WeakTimeline.Pin(); if(!Timeline.IsValid()) { return; } if(!FMath::IsFinite(ViewRange.GetLowerBoundValue()) || !FMath::IsFinite(ViewRange.GetUpperBoundValue())) { return; } FFrameRate FrameResolution = GetTickResolution(); FPaintGeometry PaintGeometry = InArgs.AllottedGeometry.ToPaintGeometry(); FSlateFontInfo SmallLayoutFont = FCoreStyle::GetDefaultFontStyle("Regular", 8); double MajorGridStep = 0.0; int32 MinorDivisions = 0; if (!Timeline->GetGridMetrics(static_cast(InArgs.AllottedGeometry.Size.X), MajorGridStep, MinorDivisions)) { return; } if (InArgs.bOnlyDrawMajorTicks) { MinorDivisions = 0; } TArray LinePoints; LinePoints.SetNumUninitialized(2); const bool bAntiAliasLines = false; const double FirstMajorLine = FMath::FloorToDouble(ViewRange.GetLowerBoundValue() / MajorGridStep) * MajorGridStep; const double LastMajorLine = FMath::CeilToDouble(ViewRange.GetUpperBoundValue() / MajorGridStep) * MajorGridStep; for (double CurrentMajorLine = FirstMajorLine; CurrentMajorLine < LastMajorLine; CurrentMajorLine += MajorGridStep) { float MajorLinePx = RangeToScreen.InputToLocalX( CurrentMajorLine ); LinePoints[0] = FVector2D( MajorLinePx, InArgs.TickOffset ); LinePoints[1] = FVector2D( MajorLinePx, InArgs.TickOffset + InArgs.MajorTickHeight ); // Draw each tick mark FSlateDrawElement::MakeLines( OutDrawElements, InArgs.StartLayer, PaintGeometry, LinePoints, InArgs.DrawEffects, InArgs.TickColor, bAntiAliasLines ); if (!InArgs.bOnlyDrawMajorTicks) { FString FrameString = TimeSliderArgs.NumericTypeInterface->ToString((CurrentMajorLine * FrameResolution).RoundToFrame().Value); // Space the text between the tick mark but slightly above FVector2D TextOffset( MajorLinePx + 5.f, InArgs.bMirrorLabels ? 3.f : FMath::Abs( InArgs.AllottedGeometry.Size.Y - (InArgs.MajorTickHeight+3.f) ) ); FSlateDrawElement::MakeText( OutDrawElements, InArgs.StartLayer+1, InArgs.AllottedGeometry.ToPaintGeometry( InArgs.AllottedGeometry.Size, FSlateLayoutTransform(TextOffset) ), FrameString, SmallLayoutFont, InArgs.DrawEffects, InArgs.TickColor*0.65f ); } for (int32 Step = 1; Step < MinorDivisions; ++Step) { // Compute the size of each tick mark. If we are half way between to visible values display a slightly larger tick mark const float MinorTickHeight = ( (MinorDivisions % 2 == 0) && (Step % (MinorDivisions/2)) == 0 ) ? 6.0f : 2.0f; const float MinorLinePx = RangeToScreen.InputToLocalX( CurrentMajorLine + Step*MajorGridStep/MinorDivisions ); LinePoints[0] = FVector2D(MinorLinePx, InArgs.bMirrorLabels ? 0.0f : FMath::Abs( InArgs.AllottedGeometry.Size.Y - MinorTickHeight ) ); LinePoints[1] = FVector2D(MinorLinePx, LinePoints[0].Y + MinorTickHeight); // Draw each sub mark FSlateDrawElement::MakeLines( OutDrawElements, InArgs.StartLayer, PaintGeometry, LinePoints, InArgs.DrawEffects, InArgs.TickColor, bAntiAliasLines ); } } } int32 FAnimTimeSliderController::OnPaintTimeSlider( bool bMirrorLabels, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled ) const { const bool bEnabled = bParentEnabled; const ESlateDrawEffect DrawEffects = bEnabled ? ESlateDrawEffect::None : ESlateDrawEffect::DisabledEffect; const TRange LocalViewRange = TimeSliderArgs.ViewRange.Get(); const float LocalViewRangeMin = static_cast(LocalViewRange.GetLowerBoundValue()); const float LocalViewRangeMax = static_cast(LocalViewRange.GetUpperBoundValue()); const float LocalSequenceLength = LocalViewRangeMax-LocalViewRangeMin; const TRange LocalPlaybackRange = TimeSliderArgs.PlaybackRange.Get(); if ( LocalSequenceLength > 0) { FScrubRangeToScreen RangeToScreen( LocalViewRange, AllottedGeometry.Size ); // draw tick marks constexpr float MajorTickHeight = 9.0f; FDrawTickArgs Args; { Args.AllottedGeometry = AllottedGeometry; Args.bMirrorLabels = bMirrorLabels; Args.bOnlyDrawMajorTicks = false; Args.TickColor = FLinearColor::White; Args.CullingRect = MyCullingRect; Args.DrawEffects = DrawEffects; Args.StartLayer = LayerId; Args.TickOffset = bMirrorLabels ? 0.0f : FMath::Abs( static_cast(AllottedGeometry.Size.Y) - MajorTickHeight ); Args.MajorTickHeight = MajorTickHeight; } DrawTicks( OutDrawElements, LocalViewRange, RangeToScreen, Args ); // draw playback & selection range FPaintPlaybackRangeArgs PlaybackRangeArgs( bMirrorLabels ? FAppStyle::GetBrush("Sequencer.Timeline.PlayRange_Bottom_L") : FAppStyle::GetBrush("Sequencer.Timeline.PlayRange_Top_L"), bMirrorLabels ? FAppStyle::GetBrush("Sequencer.Timeline.PlayRange_Bottom_R") : FAppStyle::GetBrush("Sequencer.Timeline.PlayRange_Top_R"), 6.f ); LayerId = DrawPlaybackRange(AllottedGeometry, MyCullingRect, OutDrawElements, LayerId, RangeToScreen, PlaybackRangeArgs); PlaybackRangeArgs.SolidFillOpacity = 0.05f; LayerId = DrawSelectionRange(AllottedGeometry, MyCullingRect, OutDrawElements, LayerId, RangeToScreen, PlaybackRangeArgs); LayerId = DrawEditableTimes(AllottedGeometry, MyCullingRect, OutDrawElements, LayerId, RangeToScreen); // Draw the scrub handle const float HandleStart = RangeToScreen.InputToLocalX(TimeSliderArgs.ScrubPosition.Get().AsDecimal() / GetTickResolution().AsDecimal()) - 7.0f; const float HandleEnd = HandleStart + 13.0f; const int32 ArrowLayer = LayerId + 2; FPaintGeometry MyGeometry = AllottedGeometry.ToPaintGeometry( FVector2f( HandleEnd - HandleStart, AllottedGeometry.Size.Y ), FSlateLayoutTransform(FVector2f( HandleStart, 0.f )) ); FLinearColor ScrubColor = InWidgetStyle.GetColorAndOpacityTint(); { ScrubColor.A = ScrubColor.A * 0.75f; ScrubColor.B *= 0.1f; ScrubColor.G *= 0.2f; } const FSlateBrush* Brush = (bMirrorLabels ? ScrubHandleUpBrush : ScrubHandleDownBrush); FSlateDrawElement::MakeBox( OutDrawElements, ArrowLayer, MyGeometry, Brush, DrawEffects, ScrubColor ); { // Draw the current time next to the scrub handle FString FrameString = TimeSliderArgs.NumericTypeInterface->ToString(TimeSliderArgs.ScrubPosition.Get().GetFrame().Value); if(GetDefault()->bTimelineDisplayFormatSecondary) { // @TODO: need another numeric type interface?? FString SecondaryString = SecondaryNumericTypeInterface->ToString(TimeSliderArgs.ScrubPosition.Get().GetFrame().Value); FrameString += TEXT(" (") + SecondaryString + TEXT(")"); } if(GetDefault()->bTimelineDisplayPercentage) { double Percentage = FMath::Clamp(TimeSliderArgs.ScrubPosition.Get().AsDecimal() / FFrameTime(LocalPlaybackRange.Size()).AsDecimal(), 0.0, 1.0); FNumberFormattingOptions Options; Options.MaximumFractionalDigits = 2; FrameString += TEXT(" (") + FText::AsPercent(Percentage, &Options).ToString() + TEXT(")"); } FSlateFontInfo SmallLayoutFont = FCoreStyle::GetDefaultFontStyle("Regular", 10); const TSharedRef< FSlateFontMeasure > FontMeasureService = FSlateApplication::Get().GetRenderer()->GetFontMeasureService(); const FVector2D TextSize = FontMeasureService->Measure(FrameString, SmallLayoutFont); // Flip the text position if getting near the end of the view range constexpr float TextOffsetPx = 2.f; const bool bDrawLeft = (static_cast(AllottedGeometry.Size.X) - HandleEnd) < (TextSize.X + 14.f) - TextOffsetPx; const float TextPosition = bDrawLeft ? HandleStart - static_cast(TextSize.X) - TextOffsetPx : HandleEnd + TextOffsetPx; FVector2D TextOffset( TextPosition, Args.bMirrorLabels ? TextSize.Y-6.f : Args.AllottedGeometry.Size.Y - (Args.MajorTickHeight+TextSize.Y) ); FSlateDrawElement::MakeText( OutDrawElements, Args.StartLayer+1, Args.AllottedGeometry.ToPaintGeometry( TextSize, FSlateLayoutTransform(TextOffset) ), FrameString, SmallLayoutFont, Args.DrawEffects, Args.TickColor ); } if (MouseDragType == DRAG_SETTING_RANGE) { FFrameRate Resolution = GetTickResolution(); FFrameTime MouseDownTime[2]; FScrubRangeToScreen MouseDownRange(TimeSliderArgs.ViewRange.Get(), MouseDownGeometry.Size); MouseDownTime[0] = ComputeFrameTimeFromMouse(MouseDownGeometry, MouseDownPosition[0], MouseDownRange); MouseDownTime[1] = ComputeFrameTimeFromMouse(MouseDownGeometry, MouseDownPosition[1], MouseDownRange); float MouseStartPosX = RangeToScreen.InputToLocalX(MouseDownTime[0] / Resolution); float MouseEndPosX = RangeToScreen.InputToLocalX(MouseDownTime[1] / Resolution); float RangePosX = MouseStartPosX < MouseEndPosX ? MouseStartPosX : MouseEndPosX; float RangeSizeX = FMath::Abs(MouseStartPosX - MouseEndPosX); FSlateDrawElement::MakeBox( OutDrawElements, LayerId+1, AllottedGeometry.ToPaintGeometry( FVector2f(RangeSizeX, AllottedGeometry.Size.Y), FSlateLayoutTransform(FVector2f(RangePosX, 0.f)) ), bMirrorLabels ? ScrubHandleDownBrush : ScrubHandleUpBrush, DrawEffects, MouseStartPosX < MouseEndPosX ? FLinearColor(0.5f, 0.5f, 0.5f) : FLinearColor(0.25f, 0.3f, 0.3f) ); } return ArrowLayer; } return LayerId; } int32 FAnimTimeSliderController::DrawSelectionRange(const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FScrubRangeToScreen& RangeToScreen, const FPaintPlaybackRangeArgs& Args) const { TRange SelectionRange = TimeSliderArgs.SelectionRange.Get() / GetTickResolution(); if (!SelectionRange.IsEmpty() && SelectionRange.HasLowerBound() && SelectionRange.HasUpperBound()) { const float SelectionRangeL = RangeToScreen.InputToLocalX(SelectionRange.GetLowerBoundValue()) - 1; const float SelectionRangeR = RangeToScreen.InputToLocalX(SelectionRange.GetUpperBoundValue()) + 1; const auto DrawColor = FAppStyle::GetSlateColor("SelectionColor").GetColor(FWidgetStyle()); if (Args.SolidFillOpacity > 0.f) { FSlateDrawElement::MakeBox( OutDrawElements, LayerId + 1, AllottedGeometry.ToPaintGeometry(FVector2f(SelectionRangeR - SelectionRangeL, AllottedGeometry.Size.Y), FSlateLayoutTransform(FVector2D(SelectionRangeL, 0.f))), FAppStyle::GetBrush("WhiteBrush"), ESlateDrawEffect::None, DrawColor.CopyWithNewOpacity(Args.SolidFillOpacity) ); } FSlateDrawElement::MakeBox( OutDrawElements, LayerId + 1, AllottedGeometry.ToPaintGeometry(FVector2f(Args.BrushWidth, AllottedGeometry.Size.Y), FSlateLayoutTransform(FVector2D(SelectionRangeL, 0.f))), Args.StartBrush, ESlateDrawEffect::None, DrawColor ); FSlateDrawElement::MakeBox( OutDrawElements, LayerId + 1, AllottedGeometry.ToPaintGeometry(FVector2f(Args.BrushWidth, AllottedGeometry.Size.Y), FSlateLayoutTransform(FVector2D(SelectionRangeR - Args.BrushWidth, 0.f))), Args.EndBrush, ESlateDrawEffect::None, DrawColor ); } return LayerId + 1; } int32 FAnimTimeSliderController::DrawPlaybackRange(const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FScrubRangeToScreen& RangeToScreen, const FPaintPlaybackRangeArgs& Args) const { if (!TimeSliderArgs.PlaybackRange.IsSet()) { return LayerId; } const uint8 OpacityBlend = TimeSliderArgs.SubSequenceRange.Get().IsSet() ? 128 : 255; TRange PlaybackRange = TimeSliderArgs.PlaybackRange.Get(); FFrameRate TickResolution = GetTickResolution(); const float PlaybackRangeL = RangeToScreen.InputToLocalX(PlaybackRange.GetLowerBoundValue() / TickResolution); const float PlaybackRangeR = RangeToScreen.InputToLocalX(PlaybackRange.GetUpperBoundValue() / TickResolution) - 1; FSlateDrawElement::MakeBox( OutDrawElements, LayerId+1, AllottedGeometry.ToPaintGeometry(FVector2f(Args.BrushWidth, AllottedGeometry.Size.Y), FSlateLayoutTransform(FVector2D(PlaybackRangeL, 0.f))), Args.StartBrush, ESlateDrawEffect::None, FColor(32, 128, 32, OpacityBlend) // 120, 75, 50 (HSV) ); FSlateDrawElement::MakeBox( OutDrawElements, LayerId+1, AllottedGeometry.ToPaintGeometry(FVector2f(Args.BrushWidth, AllottedGeometry.Size.Y), FSlateLayoutTransform(FVector2D(PlaybackRangeR - Args.BrushWidth, 0.f))), Args.EndBrush, ESlateDrawEffect::None, FColor(128, 32, 32, OpacityBlend) // 0, 75, 50 (HSV) ); // Black tint for excluded regions FSlateDrawElement::MakeBox( OutDrawElements, LayerId+1, AllottedGeometry.ToPaintGeometry(FVector2f(PlaybackRangeL, AllottedGeometry.Size.Y), FSlateLayoutTransform(FVector2D(0.f, 0.f))), FAppStyle::GetBrush("WhiteBrush"), ESlateDrawEffect::None, FLinearColor::Black.CopyWithNewOpacity(0.3f * OpacityBlend / 255.f) ); FSlateDrawElement::MakeBox( OutDrawElements, LayerId+1, AllottedGeometry.ToPaintGeometry(FVector2f(AllottedGeometry.Size.X - PlaybackRangeR, AllottedGeometry.Size.Y), FSlateLayoutTransform(FVector2D(PlaybackRangeR, 0.f))), FAppStyle::GetBrush("WhiteBrush"), ESlateDrawEffect::None, FLinearColor::Black.CopyWithNewOpacity(0.3f * OpacityBlend / 255.f) ); return LayerId + 1; } int32 FAnimTimeSliderController::DrawEditableTimes(const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FScrubRangeToScreen& RangeToScreen) const { const FLinearColor TimeColor = GetDefault()->SectionTimingNodeColor; // Draw all the times that we can drag in the timeline for(double Time : WeakModel.Pin()->GetEditableTimes()) { const float LinePos = RangeToScreen.InputToLocalX(Time); FSlateDrawElement::MakeBox( OutDrawElements, LayerId + 1, AllottedGeometry.ToPaintGeometry(FVector2f(11.0f, 12.0f), FSlateLayoutTransform(FVector2f(LinePos - 6.0f, AllottedGeometry.Size.Y - 12.0f))), EditableTimeBrush, ESlateDrawEffect::None, TimeColor ); } return LayerId + 1; } FReply FAnimTimeSliderController::OnMouseButtonDown( SWidget& WidgetOwner, const FGeometry& MyGeometry, const FPointerEvent& MouseEvent ) { DistanceDragged = 0; MouseDownPosition[0] = MouseDownPosition[1] = MouseEvent.GetScreenSpacePosition(); MouseDownGeometry = MyGeometry; return FReply::Unhandled(); } FReply FAnimTimeSliderController::OnMouseButtonUp( SWidget& WidgetOwner, const FGeometry& MyGeometry, const FPointerEvent& MouseEvent ) { bool bHandleLeftMouseButton = MouseEvent.GetEffectingButton() == EKeys::LeftMouseButton && WidgetOwner.HasMouseCapture(); bool bHandleRightMouseButton = MouseEvent.GetEffectingButton() == EKeys::RightMouseButton && WidgetOwner.HasMouseCapture() && TimeSliderArgs.AllowZoom ; FScrubRangeToScreen RangeToScreen = FScrubRangeToScreen(TimeSliderArgs.ViewRange.Get(), MyGeometry.Size); FFrameTime MouseTime = ComputeFrameTimeFromMouse(MyGeometry, MouseEvent.GetScreenSpacePosition(), RangeToScreen); if ( bHandleRightMouseButton ) { if (!bPanning && DistanceDragged == 0.0f) { return WeakTimeline.Pin()->OnMouseButtonUp(MyGeometry, MouseEvent).ReleaseMouseCapture(); } bPanning = false; DistanceDragged = 0.f; return FReply::Handled().ReleaseMouseCapture(); } else if ( bHandleLeftMouseButton ) { switch (MouseDragType) { case DRAG_PLAYBACK_START: { TimeSliderArgs.OnPlaybackRangeEndDrag.ExecuteIfBound(); break; } case DRAG_PLAYBACK_END: { TimeSliderArgs.OnPlaybackRangeEndDrag.ExecuteIfBound(); break; } case DRAG_SELECTION_START: { TimeSliderArgs.OnSelectionRangeEndDrag.ExecuteIfBound(); break; } case DRAG_SELECTION_END: { TimeSliderArgs.OnSelectionRangeEndDrag.ExecuteIfBound(); break; } case DRAG_SETTING_RANGE: { // Zooming FFrameTime MouseDownStart = ComputeFrameTimeFromMouse(MyGeometry, MouseDownPosition[0], RangeToScreen); const bool bCanZoomIn = MouseTime > MouseDownStart; const bool bCanZoomOut = ViewRangeStack.Num() > 0; if (bCanZoomIn || bCanZoomOut) { TRange ViewRange = TimeSliderArgs.ViewRange.Get(); if (!bCanZoomIn) { ViewRange = ViewRangeStack.Pop(); } if (bCanZoomIn) { // push the current value onto the stack ViewRangeStack.Add(ViewRange); ViewRange = TRange(MouseDownStart.FrameNumber / GetTickResolution(), MouseTime.FrameNumber / GetTickResolution()); } TimeSliderArgs.OnViewRangeChanged.ExecuteIfBound(ViewRange, EViewRangeInterpolation::Immediate); if( !TimeSliderArgs.ViewRange.IsBound() ) { // The output is not bound to a delegate so we'll manage the value ourselves TimeSliderArgs.ViewRange.Set(ViewRange); } } break; } case DRAG_TIME: { double Time = (double)MouseTime.AsDecimal() / GetTickResolution().AsDecimal(); if(!MouseEvent.IsControlDown()) { const double SnapMargin = (ScrubConstants::SnapMarginInPixels / static_cast(RangeToScreen.PixelsPerInput)); FName SnapTypeUnused; WeakModel.Pin()->Snap(Time, SnapMargin, { FName("MontageSection") }, SnapTypeUnused); } SetEditableTime(DraggedTimeIndex, static_cast(Time), false); DraggedTimeIndex = INDEX_NONE; break; } case DRAG_SCRUBBING_TIME: { TimeSliderArgs.OnEndScrubberMovement.ExecuteIfBound(); CommitScrubPosition(MouseTime, /*bIsScrubbing=*/false); break; } case DRAG_NONE: { // DRAG_NONE is an expected case because Controller's OnMouseButtonUp // is called from SSequenceTimeSliders's OnMouseButtonUp break; } default: { // if default case is called, then MouseDragType was not properly set checkNoEntry(); break; } } MouseDragType = DRAG_NONE; DistanceDragged = 0.f; return FReply::Handled().ReleaseMouseCapture(); } // reset distance dragged regardless if OnMouseButtonUp was handled DistanceDragged = 0.f; return FReply::Unhandled(); } FReply FAnimTimeSliderController::OnMouseButtonDoubleClick(SWidget& OwnerWidget, const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) { return FReply::Unhandled(); } FReply FAnimTimeSliderController::OnMouseMove( SWidget& WidgetOwner, const FGeometry& MyGeometry, const FPointerEvent& MouseEvent ) { const bool bHandleLeftMouseButton = MouseEvent.IsMouseButtonDown( EKeys::LeftMouseButton ); const bool bHandleRightMouseButton = MouseEvent.IsMouseButtonDown( EKeys::RightMouseButton ) && TimeSliderArgs.AllowZoom; if (bHandleRightMouseButton) { if (!bPanning) { DistanceDragged += static_cast(FMath::Abs( MouseEvent.GetCursorDelta().X )); if ( DistanceDragged > FSlateApplication::Get().GetDragTriggerDistance() ) { bPanning = true; } } else { const TRange LocalViewRange = TimeSliderArgs.ViewRange.Get(); const double LocalViewRangeMin = LocalViewRange.GetLowerBoundValue(); const double LocalViewRangeMax = LocalViewRange.GetUpperBoundValue(); const FScrubRangeToScreen ScaleInfo( LocalViewRange, MyGeometry.Size ); const FVector2D ScreenDelta = MouseEvent.GetCursorDelta(); FVector2D InputDelta; InputDelta.X = ScreenDelta.X/ScaleInfo.PixelsPerInput; double NewViewOutputMin = LocalViewRangeMin - InputDelta.X; double NewViewOutputMax = LocalViewRangeMax - InputDelta.X; ClampViewRange(NewViewOutputMin, NewViewOutputMax); SetViewRange(NewViewOutputMin, NewViewOutputMax, EViewRangeInterpolation::Immediate); } if ( DistanceDragged != 0.f ) { DistanceDragged = 0.f; return FReply::Handled().CaptureMouse(WidgetOwner.AsShared()); } } else if (bHandleLeftMouseButton) { const TRange LocalViewRange = TimeSliderArgs.ViewRange.Get(); const FScrubRangeToScreen RangeToScreen(LocalViewRange, MyGeometry.Size); DistanceDragged += static_cast(FMath::Abs( MouseEvent.GetCursorDelta().X )); if ( MouseDragType == DRAG_NONE ) { if ( DistanceDragged > FSlateApplication::Get().GetDragTriggerDistance() ) { UAnimMontage* AnimMontage = Cast(WeakModel.Pin()->GetAnimSequenceBase()); const bool bChildAnimMontage = AnimMontage && AnimMontage->HasParentAsset(); const FFrameTime MouseDownFree = ComputeFrameTimeFromMouse(MyGeometry, MouseDownPosition[0], RangeToScreen, false); const FFrameRate FrameResolution = GetTickResolution(); const bool bLockedPlayRange = TimeSliderArgs.IsPlaybackRangeLocked.Get(); const float MouseDownPixel = RangeToScreen.InputToLocalX(MouseDownFree / FrameResolution); const bool bHitScrubber = GetHitTestScrubberPixelRange(TimeSliderArgs.ScrubPosition.Get(), RangeToScreen).HandleRange.Contains(MouseDownPixel); const int32 HitTimeIndex = HitTestTimes(RangeToScreen, MouseDownPixel); const bool bHitTime = !bChildAnimMontage && HitTimeIndex != INDEX_NONE; const TRange SelectionRange = TimeSliderArgs.SelectionRange.Get() / FrameResolution; const TRange PlaybackRange = TimeSliderArgs.PlaybackRange.Get() / FrameResolution; // Disable selection range test if it's empty so that the playback range scrubbing gets priority if (!SelectionRange.IsEmpty() && !bHitScrubber && HitTestRangeEnd(RangeToScreen, SelectionRange, MouseDownPixel)) { // selection range end scrubber MouseDragType = DRAG_SELECTION_END; TimeSliderArgs.OnSelectionRangeBeginDrag.ExecuteIfBound(); } else if (!SelectionRange.IsEmpty() && !bHitScrubber && HitTestRangeStart(RangeToScreen, SelectionRange, MouseDownPixel)) { // selection range start scrubber MouseDragType = DRAG_SELECTION_START; TimeSliderArgs.OnSelectionRangeBeginDrag.ExecuteIfBound(); } else if (!bLockedPlayRange && !bHitScrubber && HitTestRangeEnd(RangeToScreen, PlaybackRange, MouseDownPixel)) { // playback range end scrubber MouseDragType = DRAG_PLAYBACK_END; TimeSliderArgs.OnPlaybackRangeBeginDrag.ExecuteIfBound(); } else if (!bLockedPlayRange && !bHitScrubber && HitTestRangeStart(RangeToScreen, PlaybackRange, MouseDownPixel)) { // playback range start scrubber MouseDragType = DRAG_PLAYBACK_START; TimeSliderArgs.OnPlaybackRangeBeginDrag.ExecuteIfBound(); } else if (FSlateApplication::Get().GetModifierKeys().AreModifersDown(EModifierKey::Control)) { MouseDragType = DRAG_SETTING_RANGE; } else if(bHitTime) { MouseDragType = DRAG_TIME; DraggedTimeIndex = HitTimeIndex; } else { MouseDragType = DRAG_SCRUBBING_TIME; TimeSliderArgs.OnBeginScrubberMovement.ExecuteIfBound(); } } } else { FFrameTime MouseTime = ComputeFrameTimeFromMouse(MyGeometry, MouseEvent.GetScreenSpacePosition(), RangeToScreen); // Set the start range time? if (MouseDragType == DRAG_PLAYBACK_START) { SetPlaybackRangeStart(MouseTime.FrameNumber); } // Set the end range time? else if(MouseDragType == DRAG_PLAYBACK_END) { SetPlaybackRangeEnd(MouseTime.FrameNumber-1); } else if (MouseDragType == DRAG_SELECTION_START) { SetSelectionRangeStart(MouseTime.FrameNumber); } // Set the end range time? else if(MouseDragType == DRAG_SELECTION_END) { SetSelectionRangeEnd(MouseTime.FrameNumber); } else if (MouseDragType == DRAG_SCRUBBING_TIME) { // Delegate responsibility for clamping to the current viewrange to the client CommitScrubPosition(MouseTime, /*bIsScrubbing=*/true); } else if (MouseDragType == DRAG_SETTING_RANGE) { MouseDownPosition[1] = MouseEvent.GetScreenSpacePosition(); } else if(MouseDragType == DRAG_TIME) { double Time = (double)MouseTime.AsDecimal() / GetTickResolution().AsDecimal(); if(!MouseEvent.IsControlDown()) { const double SnapMargin = (ScrubConstants::SnapMarginInPixels / static_cast(RangeToScreen.PixelsPerInput)); FName SnapTypeUnused; WeakModel.Pin()->Snap(Time, SnapMargin, { FName("MontageSection") }, SnapTypeUnused); } SetEditableTime(DraggedTimeIndex, static_cast(Time), true); } } // only capture mouse if MouseDragType is valid if ( MouseDragType != DRAG_NONE && DistanceDragged != 0.f ) { DistanceDragged = 0.f; return FReply::Handled().CaptureMouse(WidgetOwner.AsShared()); } } DistanceDragged = 0.f; return FReply::Handled(); } void FAnimTimeSliderController::CommitScrubPosition( FFrameTime NewValue, bool bIsScrubbing ) { // Manage the scrub position ourselves if its not bound to a delegate if ( !TimeSliderArgs.ScrubPosition.IsBound() ) { TimeSliderArgs.ScrubPosition.Set( NewValue ); } TimeSliderArgs.OnScrubPositionChanged.ExecuteIfBound( NewValue, bIsScrubbing, /*bEvaluate*/ true); //todo change if anim timeline needs to handle sequencer style middle mouse manipulation which changes time but doesn't evaluate } FReply FAnimTimeSliderController::OnMouseWheel( SWidget& WidgetOwner, const FGeometry& MyGeometry, const FPointerEvent& MouseEvent ) { TOptional> NewTargetRange; if ( TimeSliderArgs.AllowZoom && MouseEvent.IsControlDown() ) { const float MouseFractionX = static_cast(MyGeometry.AbsoluteToLocal(MouseEvent.GetScreenSpacePosition()).X / MyGeometry.GetLocalSize().X); const float ZoomDelta = -0.2f * MouseEvent.GetWheelDelta(); if (ZoomByDelta(ZoomDelta, MouseFractionX)) { return FReply::Handled(); } } else if (MouseEvent.IsShiftDown()) { PanByDelta(-MouseEvent.GetWheelDelta()); return FReply::Handled(); } return FReply::Unhandled(); } FCursorReply FAnimTimeSliderController::OnCursorQuery( TSharedRef WidgetOwner, const FGeometry& MyGeometry, const FPointerEvent& CursorEvent ) const { const FScrubRangeToScreen RangeToScreen(TimeSliderArgs.ViewRange.Get(), MyGeometry.Size); UAnimMontage* AnimMontage = Cast(WeakModel.Pin()->GetAnimSequenceBase()); const bool bChildAnimMontage = AnimMontage && AnimMontage->HasParentAsset(); const FFrameRate FrameResolution = GetTickResolution(); const bool bLockedPlayRange = TimeSliderArgs.IsPlaybackRangeLocked.Get(); const float HitTestPixel = static_cast(MyGeometry.AbsoluteToLocal(CursorEvent.GetScreenSpacePosition()).X); const bool bHitScrubber = GetHitTestScrubberPixelRange(TimeSliderArgs.ScrubPosition.Get(), RangeToScreen).HandleRange.Contains(HitTestPixel); const bool bHitTime = !bChildAnimMontage && (HitTestTimes(RangeToScreen, HitTestPixel) != INDEX_NONE); const TRange SelectionRange = TimeSliderArgs.SelectionRange.Get() / FrameResolution; const TRange PlaybackRange = TimeSliderArgs.PlaybackRange.Get() / FrameResolution; if (MouseDragType == DRAG_SCRUBBING_TIME) { return FCursorReply::Unhandled(); } // Use L/R resize cursor if we're dragging or hovering a playback range bound if (bHitTime || (MouseDragType == DRAG_PLAYBACK_END) || (MouseDragType == DRAG_PLAYBACK_START) || (MouseDragType == DRAG_SELECTION_START) || (MouseDragType == DRAG_SELECTION_END) || (MouseDragType == DRAG_TIME) || (!bLockedPlayRange && !bHitScrubber && HitTestRangeStart(RangeToScreen, PlaybackRange, HitTestPixel)) || (!bLockedPlayRange && !bHitScrubber && HitTestRangeEnd( RangeToScreen, PlaybackRange, HitTestPixel)) || (!SelectionRange.IsEmpty() && !bHitScrubber && HitTestRangeStart(RangeToScreen, SelectionRange, HitTestPixel)) || (!SelectionRange.IsEmpty() && !bHitScrubber && HitTestRangeEnd( RangeToScreen, SelectionRange, HitTestPixel))) { return FCursorReply::Cursor(EMouseCursor::ResizeLeftRight); } return FCursorReply::Unhandled(); } int32 FAnimTimeSliderController::OnPaintViewArea( const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, bool bEnabled, const FPaintViewAreaArgs& Args ) const { const ESlateDrawEffect DrawEffects = bEnabled ? ESlateDrawEffect::None : ESlateDrawEffect::DisabledEffect; TRange LocalViewRange = TimeSliderArgs.ViewRange.Get(); FScrubRangeToScreen RangeToScreen( LocalViewRange, AllottedGeometry.Size ); if (Args.PlaybackRangeArgs.IsSet()) { FPaintPlaybackRangeArgs PaintArgs = Args.PlaybackRangeArgs.GetValue(); LayerId = DrawPlaybackRange(AllottedGeometry, MyCullingRect, OutDrawElements, LayerId, RangeToScreen, PaintArgs); PaintArgs.SolidFillOpacity = 0.2f; LayerId = DrawSelectionRange(AllottedGeometry, MyCullingRect, OutDrawElements, LayerId, RangeToScreen, PaintArgs); } if( Args.bDisplayTickLines ) { static FLinearColor TickColor(0.1f, 0.1f, 0.1f, 0.3f); // Draw major tick lines in the section area FDrawTickArgs DrawTickArgs; { DrawTickArgs.AllottedGeometry = AllottedGeometry; DrawTickArgs.bMirrorLabels = false; DrawTickArgs.bOnlyDrawMajorTicks = true; DrawTickArgs.TickColor = TickColor; DrawTickArgs.CullingRect = MyCullingRect; DrawTickArgs.DrawEffects = DrawEffects; // Draw major ticks under sections DrawTickArgs.StartLayer = LayerId-1; // Draw the tick the entire height of the section area DrawTickArgs.TickOffset = 0.0f; DrawTickArgs.MajorTickHeight = static_cast(AllottedGeometry.Size.Y); } DrawTicks( OutDrawElements, LocalViewRange, RangeToScreen, DrawTickArgs ); } if( Args.bDisplayScrubPosition ) { // Draw a line for the scrub position const float LinePos = RangeToScreen.InputToLocalX(TimeSliderArgs.ScrubPosition.Get().AsDecimal() / GetTickResolution().AsDecimal()); TArray LinePoints; { LinePoints.AddUninitialized(2); LinePoints[0] = FVector2D( 0.0f, 0.0f ); LinePoints[1] = FVector2D( 0.0f, FMath::FloorToFloat( AllottedGeometry.Size.Y ) ); } FSlateDrawElement::MakeLines( OutDrawElements, LayerId+1, AllottedGeometry.ToPaintGeometry( FVector2f(1.0f,1.0f), FSlateLayoutTransform(FVector2f(LinePos, 0.0f )) ), LinePoints, DrawEffects, FLinearColor(1.f, 1.f, 1.f, .5f), false ); } TSharedPtr AnimModel = WeakModel.Pin(); if (AnimModel.IsValid()) { const FLinearColor LineColor = GetDefault()->SectionTimingNodeColor; // Draw all the times that we can drag in the timeline for(double Time : AnimModel->GetEditableTimes()) { const float LinePos = RangeToScreen.InputToLocalX(Time); TArray LinePoints; { LinePoints.AddUninitialized(2); LinePoints[0] = FVector2D( 0.0f, 0.0f ); LinePoints[1] = FVector2D( 0.0f, FMath::FloorToFloat( AllottedGeometry.Size.Y ) ); } FSlateDrawElement::MakeLines( OutDrawElements, LayerId+1, AllottedGeometry.ToPaintGeometry( FVector2f(1.0f,1.0f), FSlateLayoutTransform(FVector2f(LinePos, 0.0f )) ), LinePoints, DrawEffects, LineColor, false ); } } return LayerId; } FFrameTime FAnimTimeSliderController::GetFrameTimeFromMouse(const FGeometry& Geometry, FVector2D ScreenSpacePosition) const { FScrubRangeToScreen ScrubRangeToScreen(TimeSliderArgs.ViewRange.Get(), Geometry.Size); return ComputeFrameTimeFromMouse(Geometry, ScreenSpacePosition, ScrubRangeToScreen); } TSharedRef FAnimTimeSliderController::OpenSetPlaybackRangeMenu(FFrameNumber FrameNumber) { const bool bShouldCloseWindowAfterMenuSelection = true; FMenuBuilder MenuBuilder(bShouldCloseWindowAfterMenuSelection, nullptr); FText CurrentTimeText; CurrentTimeText = FText::FromString(TimeSliderArgs.NumericTypeInterface->ToString(FrameNumber.Value)); TRange PlaybackRange = TimeSliderArgs.PlaybackRange.Get(); TRange SelectionRange = TimeSliderArgs.SelectionRange.Get(); MenuBuilder.BeginSection("AnimSelectionRangeMenu", FText::Format(LOCTEXT("SelectionRangeTextFormat", "Selection Range ({0}):"), CurrentTimeText)); { MenuBuilder.AddMenuEntry( LOCTEXT("SetSelectionStart", "Set Selection Start"), FText(), FSlateIcon(), FUIAction( FExecuteAction::CreateLambda([this, FrameNumber]{ SetSelectionRangeStart(FrameNumber); }), FCanExecuteAction::CreateLambda([FrameNumber, SelectionRange]{ return SelectionRange.IsEmpty() || FrameNumber < UE::MovieScene::DiscreteExclusiveUpper(SelectionRange); }) ) ); MenuBuilder.AddMenuEntry( LOCTEXT("SetSelectionEnd", "Set Selection End"), FText(), FSlateIcon(), FUIAction( FExecuteAction::CreateLambda([this, FrameNumber]{ SetSelectionRangeEnd(FrameNumber); }), FCanExecuteAction::CreateLambda([FrameNumber, SelectionRange]{ return SelectionRange.IsEmpty() || FrameNumber >= UE::MovieScene::DiscreteInclusiveLower(SelectionRange); }) ) ); MenuBuilder.AddMenuEntry( LOCTEXT("ClearSelectionRange", "Clear Selection Range"), FText(), FSlateIcon(), FUIAction( FExecuteAction::CreateLambda([this, FrameNumber]{ TimeSliderArgs.OnSelectionRangeChanged.ExecuteIfBound(TRange::Empty()); }), FCanExecuteAction::CreateLambda([FrameNumber, SelectionRange]{ return !SelectionRange.IsEmpty(); }) ) ); } MenuBuilder.EndSection(); return MenuBuilder.MakeWidget(); } void FAnimTimeSliderController::ClampViewRange(double& NewRangeMin, double& NewRangeMax) { bool bNeedsClampSet = false; double NewClampRangeMin = TimeSliderArgs.ClampRange.Get().GetLowerBoundValue(); if ( NewRangeMin < TimeSliderArgs.ClampRange.Get().GetLowerBoundValue() ) { NewClampRangeMin = NewRangeMin; bNeedsClampSet = true; } double NewClampRangeMax = TimeSliderArgs.ClampRange.Get().GetUpperBoundValue(); if ( NewRangeMax > TimeSliderArgs.ClampRange.Get().GetUpperBoundValue() ) { NewClampRangeMax = NewRangeMax; bNeedsClampSet = true; } if (bNeedsClampSet) { SetClampRange(NewClampRangeMin, NewClampRangeMax); } } void FAnimTimeSliderController::SetViewRange( double NewRangeMin, double NewRangeMax, EViewRangeInterpolation Interpolation ) { // Clamp to a minimum size to avoid zero-sized or negative visible ranges double MinVisibleTimeRange = FFrameNumber(1) / GetTickResolution(); TRange ExistingViewRange = TimeSliderArgs.ViewRange.Get(); if (NewRangeMax == ExistingViewRange.GetUpperBoundValue()) { if (NewRangeMin > NewRangeMax - MinVisibleTimeRange) { NewRangeMin = NewRangeMax - MinVisibleTimeRange; } } else if (NewRangeMax < NewRangeMin + MinVisibleTimeRange) { NewRangeMax = NewRangeMin + MinVisibleTimeRange; } // Clamp to the clamp range const TRange NewRange = TRange(NewRangeMin, NewRangeMax); TimeSliderArgs.OnViewRangeChanged.ExecuteIfBound( NewRange, Interpolation ); if( !TimeSliderArgs.ViewRange.IsBound() ) { // The output is not bound to a delegate so we'll manage the value ourselves (no animation) TimeSliderArgs.ViewRange.Set( NewRange ); } } void FAnimTimeSliderController::SetClampRange( double NewRangeMin, double NewRangeMax ) { const TRange NewRange(NewRangeMin, NewRangeMax); TimeSliderArgs.OnClampRangeChanged.ExecuteIfBound(NewRange); if( !TimeSliderArgs.ClampRange.IsBound() ) { // The output is not bound to a delegate so we'll manage the value ourselves (no animation) TimeSliderArgs.ClampRange.Set(NewRange); } } void FAnimTimeSliderController::SetPlayRange( FFrameNumber RangeStart, int32 RangeDuration ) { check(RangeDuration >= 0); const TRange NewRange(RangeStart, RangeStart + RangeDuration); TimeSliderArgs.OnPlaybackRangeChanged.ExecuteIfBound(NewRange); if( !TimeSliderArgs.PlaybackRange.IsBound() ) { // The output is not bound to a delegate so we'll manage the value ourselves (no animation) TimeSliderArgs.PlaybackRange.Set(NewRange); } } bool FAnimTimeSliderController::ZoomByDelta( float InDelta, float MousePositionFraction ) { TRange LocalViewRange = TimeSliderArgs.ViewRange.Get().GetAnimationTarget(); double LocalViewRangeMax = LocalViewRange.GetUpperBoundValue(); double LocalViewRangeMin = LocalViewRange.GetLowerBoundValue(); const double OutputViewSize = LocalViewRangeMax - LocalViewRangeMin; const double OutputChange = OutputViewSize * InDelta; double NewViewOutputMin = LocalViewRangeMin - (OutputChange * MousePositionFraction); double NewViewOutputMax = LocalViewRangeMax + (OutputChange * (1.f - MousePositionFraction)); if( NewViewOutputMin < NewViewOutputMax ) { ClampViewRange(NewViewOutputMin, NewViewOutputMax); SetViewRange(NewViewOutputMin, NewViewOutputMax, EViewRangeInterpolation::Animated); return true; } return false; } void FAnimTimeSliderController::PanByDelta( float InDelta ) { const TRange LocalViewRange = TimeSliderArgs.ViewRange.Get().GetAnimationTarget(); const double CurrentMin = LocalViewRange.GetLowerBoundValue(); const double CurrentMax = LocalViewRange.GetUpperBoundValue(); // Adjust the delta to be a percentage of the current range InDelta *= ScrubConstants::ScrollPanFraction * static_cast(CurrentMax - CurrentMin); double NewViewOutputMin = CurrentMin + InDelta; double NewViewOutputMax = CurrentMax + InDelta; ClampViewRange(NewViewOutputMin, NewViewOutputMax); SetViewRange(NewViewOutputMin, NewViewOutputMax, EViewRangeInterpolation::Animated); } bool FAnimTimeSliderController::HitTestRangeStart(const FScrubRangeToScreen& RangeToScreen, const TRange& Range, float HitPixel) const { if(Range.HasLowerBound()) { static float BrushSizeInStateUnits = 6.f, DragToleranceSlateUnits = 2.f, MouseTolerance = 2.f; const float RangeStartPixel = RangeToScreen.InputToLocalX(Range.GetLowerBoundValue()); // Hit test against the brush region to the right of the playback start position, +/- DragToleranceSlateUnits return HitPixel >= RangeStartPixel - MouseTolerance - DragToleranceSlateUnits && HitPixel <= RangeStartPixel + MouseTolerance + BrushSizeInStateUnits + DragToleranceSlateUnits; } return false; } bool FAnimTimeSliderController::HitTestRangeEnd(const FScrubRangeToScreen& RangeToScreen, const TRange& Range, float HitPixel) const { if(Range.HasUpperBound()) { static float BrushSizeInStateUnits = 6.f, DragToleranceSlateUnits = 2.f, MouseTolerance = 2.f; const float RangeEndPixel = RangeToScreen.InputToLocalX(Range.GetUpperBoundValue()); // Hit test against the brush region to the left of the playback end position, +/- DragToleranceSlateUnits return HitPixel >= RangeEndPixel - MouseTolerance - BrushSizeInStateUnits - DragToleranceSlateUnits && HitPixel <= RangeEndPixel + MouseTolerance + DragToleranceSlateUnits; } return false; } bool FAnimTimeSliderController::HitTestTime(const FScrubRangeToScreen& RangeToScreen, double Time, float HitPixel) const { static float HalfBrushSizeInStateUnits = 5.5f, DragToleranceSlateUnits = 2.f, MouseTolerance = 2.f; const float TimePixel = RangeToScreen.InputToLocalX(Time); // Hit test against the time position, +/- DragToleranceSlateUnits return HitPixel >= TimePixel - MouseTolerance - HalfBrushSizeInStateUnits - DragToleranceSlateUnits && HitPixel <= TimePixel + MouseTolerance + HalfBrushSizeInStateUnits + DragToleranceSlateUnits; } void FAnimTimeSliderController::SetPlaybackRangeStart(FFrameNumber NewStart) { TRange PlaybackRange = TimeSliderArgs.PlaybackRange.Get(); if (NewStart <= UE::MovieScene::DiscreteExclusiveUpper(PlaybackRange)) { TimeSliderArgs.OnPlaybackRangeChanged.ExecuteIfBound(TRange(NewStart, PlaybackRange.GetUpperBound())); } } void FAnimTimeSliderController::SetPlaybackRangeEnd(FFrameNumber NewEnd) { TRange PlaybackRange = TimeSliderArgs.PlaybackRange.Get(); if (NewEnd >= UE::MovieScene::DiscreteInclusiveLower(PlaybackRange)) { TimeSliderArgs.OnPlaybackRangeChanged.ExecuteIfBound(TRange(PlaybackRange.GetLowerBound(), NewEnd)); } } void FAnimTimeSliderController::SetSelectionRangeStart(FFrameNumber NewStart) { TRange SelectionRange = TimeSliderArgs.SelectionRange.Get(); if (SelectionRange.IsEmpty()) { TimeSliderArgs.OnSelectionRangeChanged.ExecuteIfBound(TRange(NewStart, NewStart + 1)); } else if (NewStart <= UE::MovieScene::DiscreteExclusiveUpper(SelectionRange)) { TimeSliderArgs.OnSelectionRangeChanged.ExecuteIfBound(TRange(NewStart, SelectionRange.GetUpperBound())); } } void FAnimTimeSliderController::SetSelectionRangeEnd(FFrameNumber NewEnd) { TRange SelectionRange = TimeSliderArgs.SelectionRange.Get(); if (SelectionRange.IsEmpty()) { TimeSliderArgs.OnSelectionRangeChanged.ExecuteIfBound(TRange(NewEnd - 1, NewEnd)); } else if (NewEnd >= UE::MovieScene::DiscreteInclusiveLower(SelectionRange)) { TimeSliderArgs.OnSelectionRangeChanged.ExecuteIfBound(TRange(SelectionRange.GetLowerBound(), NewEnd)); } } int32 FAnimTimeSliderController::HitTestTimes(const FScrubRangeToScreen& RangeToScreen, float HitPixel) const { const TArray& Times = WeakModel.Pin()->GetEditableTimes(); int32 NumTimes = Times.Num(); for(int32 TimeIndex = 0; TimeIndex < NumTimes; ++TimeIndex) { const double Time = WeakModel.Pin()->GetEditableTimes()[TimeIndex]; if(HitTestTime(RangeToScreen, Time, HitPixel)) { return TimeIndex; } } return INDEX_NONE; } void FAnimTimeSliderController::SetEditableTime(int32 TimeIndex, float Time, bool bIsDragging) { TSharedPtr Model = WeakModel.Pin(); Model->SetEditableTime(TimeIndex, Time, bIsDragging); } #undef LOCTEXT_NAMESPACE