// Copyright Epic Games, Inc. All Rights Reserved. #include "SCurveEditor.h" #include "Fonts/SlateFontInfo.h" #include "Rendering/DrawElements.h" #include "Widgets/SBoxPanel.h" #include "Styling/SlateTypes.h" #include "Styling/CoreStyle.h" #include "Layout/WidgetPath.h" #include "Framework/Application/MenuStack.h" #include "Fonts/FontMeasure.h" #include "Framework/Application/SlateApplication.h" #include "Textures/SlateIcon.h" #include "Framework/Commands/UIAction.h" #include "Framework/Commands/UICommandList.h" #include "Widgets/Layout/SBorder.h" #include "Widgets/Images/SImage.h" #include "Widgets/Text/STextBlock.h" #include "Widgets/Layout/SBox.h" #include "Framework/MultiBox/MultiBoxBuilder.h" #include "Widgets/Input/SButton.h" #include "Widgets/SToolTip.h" #include "Widgets/Notifications/SErrorText.h" #include "Widgets/Input/SCheckBox.h" #include "Styling/AppStyle.h" #include "Factories/Factory.h" #include "Factories/CurveFactory.h" #include "Editor.h" #include "Curves/SimpleCurve.h" #include "CurveEditorCommands.h" #include "CurveEditorSettings.h" #include "ScopedTransaction.h" #include "Framework/Commands/GenericCommands.h" #include "Widgets/Input/SNumericEntryBox.h" #include "Widgets/Input/STextEntryPopup.h" #include "Widgets/Notifications/SNotificationList.h" #include "Framework/Notifications/NotificationManager.h" #include "IPropertyUtilities.h" #include "PropertyHandle.h" #define LOCTEXT_NAMESPACE "SCurveEditor" DEFINE_LOG_CATEGORY(LogCurveEditor); const static FVector2D CONST_KeySize = FVector2D(11,11); const static FVector2D CONST_TangentSize = FVector2D(7,7); const static FVector2D CONST_CurveSize = FVector2D(12,12); const static float CONST_FitMargin = 0.05f; const static float CONST_MinViewRange = 0.01f; const static float CONST_DefaultZoomRange = 1.0f; const static float CONST_KeyTangentOffset = 60.0f; ////////////////////////////////////////////////////////////////////////// // SCurveEditor void SCurveEditor::Construct(const FArguments& InArgs) { CurveFactory = NULL; Commands = TSharedPtr< FUICommandList >(new FUICommandList); CurveOwner = NULL; // view input ViewMinInput = InArgs._ViewMinInput; ViewMaxInput = InArgs._ViewMaxInput; // data input - only used when it's set DataMinInput = InArgs._DataMinInput; DataMaxInput = InArgs._DataMaxInput; ViewMinOutput = InArgs._ViewMinOutput; ViewMaxOutput = InArgs._ViewMaxOutput; InputSnap = InArgs._InputSnap; OutputSnap = InArgs._OutputSnap; bInputSnappingEnabled = InArgs._InputSnappingEnabled; bOutputSnappingEnabled = InArgs._OutputSnappingEnabled; bShowTimeInFrames = InArgs._ShowTimeInFrames; bZoomToFitVertical = InArgs._ZoomToFitVertical; bZoomToFitHorizontal = InArgs._ZoomToFitHorizontal; DesiredSize = InArgs._DesiredSize; GridColor = InArgs._GridColor; bIsUsingSlider = false; bAllowAutoFrame = true; bRequireFocusToZoom = false; bIsPendingRebuilt = false; // if editor size is set, use it, otherwise, use default value if (DesiredSize.Get().IsZero()) { DesiredSize.Set(FVector2D(128, 64)); } TimelineLength = InArgs._TimelineLength; SetInputViewRangeHandler = InArgs._OnSetInputViewRange; SetOutputViewRangeHandler = InArgs._OnSetOutputViewRange; bDrawCurve = InArgs._DrawCurve; bHideUI = InArgs._HideUI; bAllowZoomOutput = InArgs._AllowZoomOutput; bAlwaysDisplayColorCurves = InArgs._AlwaysDisplayColorCurves; bAlwaysHideGradientEditor = InArgs._AlwaysHideGradientEditor; bShowZoomButtons = InArgs._ShowZoomButtons; bShowCurveSelector = InArgs._ShowCurveSelector; bDrawInputGridNumbers = InArgs._ShowInputGridNumbers; bDrawOutputGridNumbers = InArgs._ShowOutputGridNumbers; bAreCurvesVisible = InArgs._AreCurvesVisible; SetAreCurvesVisibleHandler = InArgs._OnSetAreCurvesVisible; OnCreateAsset = InArgs._OnCreateAsset; DragState = EDragState::None; DragThreshold = 4; MovementAxisLock = EMovementAxisLock::None; TransactionIndex = -1; ReduceTolerance = 0.001; Settings = GetMutableDefault(); Commands->MapAction(FGenericCommands::Get().Undo, FExecuteAction::CreateSP(this, &SCurveEditor::UndoAction)); Commands->MapAction(FGenericCommands::Get().Redo, FExecuteAction::CreateSP(this, &SCurveEditor::RedoAction)); Commands->MapAction(FCurveEditorCommands::Get().ZoomToFitHorizontal, FExecuteAction::CreateSP(this, &SCurveEditor::ZoomToFitHorizontal, false)); Commands->MapAction(FCurveEditorCommands::Get().ZoomToFitVertical, FExecuteAction::CreateSP(this, &SCurveEditor::ZoomToFitVertical, false)); Commands->MapAction(FCurveEditorCommands::Get().ZoomToFit, FExecuteAction::CreateSP(this, &SCurveEditor::ZoomToFit, false)); Commands->MapAction(FCurveEditorCommands::Get().ZoomToFitAll, FExecuteAction::CreateSP(this, &SCurveEditor::ZoomToFit, true)); Commands->MapAction(FCurveEditorCommands::Get().ToggleInputSnapping, FExecuteAction::CreateSP(this, &SCurveEditor::ToggleInputSnapping), FCanExecuteAction(), FIsActionChecked::CreateSP(this, &SCurveEditor::IsInputSnappingEnabled)); Commands->MapAction(FCurveEditorCommands::Get().ToggleOutputSnapping, FExecuteAction::CreateSP(this, &SCurveEditor::ToggleOutputSnapping), FCanExecuteAction(), FIsActionChecked::CreateSP(this, &SCurveEditor::IsOutputSnappingEnabled)); // Interpolation Commands->MapAction(FCurveEditorCommands::Get().InterpolationConstant, FExecuteAction::CreateSP(this, &SCurveEditor::OnSelectInterpolationMode, RCIM_Constant, RCTM_Auto), FCanExecuteAction(), FIsActionChecked::CreateSP(this, &SCurveEditor::IsInterpolationModeSelected, RCIM_Constant, RCTM_Auto)); Commands->MapAction(FCurveEditorCommands::Get().InterpolationLinear, FExecuteAction::CreateSP(this, &SCurveEditor::OnSelectInterpolationMode, RCIM_Linear, RCTM_Auto), FCanExecuteAction(), FIsActionChecked::CreateSP(this, &SCurveEditor::IsInterpolationModeSelected, RCIM_Linear, RCTM_Auto)); Commands->MapAction(FCurveEditorCommands::Get().InterpolationCubicAuto, FExecuteAction::CreateSP(this, &SCurveEditor::OnSelectInterpolationMode, RCIM_Cubic, RCTM_Auto), FCanExecuteAction::CreateSP(this, &SCurveEditor::HasRichCurves), FIsActionChecked::CreateSP(this, &SCurveEditor::IsInterpolationModeSelected, RCIM_Cubic, RCTM_Auto)); Commands->MapAction(FCurveEditorCommands::Get().InterpolationCubicSmartAuto, FExecuteAction::CreateSP(this, &SCurveEditor::OnSelectInterpolationMode, RCIM_Cubic, RCTM_SmartAuto), FCanExecuteAction::CreateSP(this, &SCurveEditor::HasRichCurves), FIsActionChecked::CreateSP(this, &SCurveEditor::IsInterpolationModeSelected, RCIM_Cubic, RCTM_SmartAuto)); Commands->MapAction(FCurveEditorCommands::Get().InterpolationCubicUser, FExecuteAction::CreateSP(this, &SCurveEditor::OnSelectInterpolationMode, RCIM_Cubic, RCTM_User), FCanExecuteAction::CreateSP(this, &SCurveEditor::HasRichCurves), FIsActionChecked::CreateSP(this, &SCurveEditor::IsInterpolationModeSelected, RCIM_Cubic, RCTM_User)); Commands->MapAction(FCurveEditorCommands::Get().InterpolationCubicBreak, FExecuteAction::CreateSP(this, &SCurveEditor::OnSelectInterpolationMode, RCIM_Cubic, RCTM_Break), FCanExecuteAction::CreateSP(this, &SCurveEditor::HasRichCurves), FIsActionChecked::CreateSP(this, &SCurveEditor::IsInterpolationModeSelected, RCIM_Cubic, RCTM_Break)); // Tangents Commands->MapAction(FCurveEditorCommands::Get().FlattenTangents, FExecuteAction::CreateSP(this, &SCurveEditor::OnFlattenOrStraightenTangents, true), FCanExecuteAction::CreateSP(this, &SCurveEditor::HasRichCurves)); Commands->MapAction(FCurveEditorCommands::Get().StraightenTangents, FExecuteAction::CreateSP(this, &SCurveEditor::OnFlattenOrStraightenTangents, false), FCanExecuteAction::CreateSP(this, &SCurveEditor::HasRichCurves)); // Bake and reduce Commands->MapAction(FCurveEditorCommands::Get().BakeCurve, FExecuteAction::CreateSP(this, &SCurveEditor::OnBakeCurve)); Commands->MapAction(FCurveEditorCommands::Get().ReduceCurve, FExecuteAction::CreateSP(this, &SCurveEditor::OnReduceCurve)); // Pre infinity extrapolation Commands->MapAction(FCurveEditorCommands::Get().SetPreInfinityExtrapCycle, FExecuteAction::CreateSP(this, &SCurveEditor::OnSelectPreInfinityExtrap, RCCE_Cycle), FCanExecuteAction(), FIsActionChecked::CreateSP(this, &SCurveEditor::IsPreInfinityExtrapSelected, RCCE_Cycle)); Commands->MapAction(FCurveEditorCommands::Get().SetPreInfinityExtrapCycleWithOffset, FExecuteAction::CreateSP(this, &SCurveEditor::OnSelectPreInfinityExtrap, RCCE_CycleWithOffset), FCanExecuteAction(), FIsActionChecked::CreateSP(this, &SCurveEditor::IsPreInfinityExtrapSelected, RCCE_CycleWithOffset)); Commands->MapAction(FCurveEditorCommands::Get().SetPreInfinityExtrapOscillate, FExecuteAction::CreateSP(this, &SCurveEditor::OnSelectPreInfinityExtrap, RCCE_Oscillate), FCanExecuteAction(), FIsActionChecked::CreateSP(this, &SCurveEditor::IsPreInfinityExtrapSelected, RCCE_Oscillate)); Commands->MapAction(FCurveEditorCommands::Get().SetPreInfinityExtrapLinear, FExecuteAction::CreateSP(this, &SCurveEditor::OnSelectPreInfinityExtrap, RCCE_Linear), FCanExecuteAction(), FIsActionChecked::CreateSP(this, &SCurveEditor::IsPreInfinityExtrapSelected, RCCE_Linear)); Commands->MapAction(FCurveEditorCommands::Get().SetPreInfinityExtrapConstant, FExecuteAction::CreateSP(this, &SCurveEditor::OnSelectPreInfinityExtrap, RCCE_Constant), FCanExecuteAction(), FIsActionChecked::CreateSP(this, &SCurveEditor::IsPreInfinityExtrapSelected, RCCE_Constant)); // Post infinity extrapolation Commands->MapAction(FCurveEditorCommands::Get().SetPostInfinityExtrapCycle, FExecuteAction::CreateSP(this, &SCurveEditor::OnSelectPostInfinityExtrap, RCCE_Cycle), FCanExecuteAction(), FIsActionChecked::CreateSP(this, &SCurveEditor::IsPostInfinityExtrapSelected, RCCE_Cycle)); Commands->MapAction(FCurveEditorCommands::Get().SetPostInfinityExtrapCycleWithOffset, FExecuteAction::CreateSP(this, &SCurveEditor::OnSelectPostInfinityExtrap, RCCE_CycleWithOffset), FCanExecuteAction(), FIsActionChecked::CreateSP(this, &SCurveEditor::IsPostInfinityExtrapSelected, RCCE_CycleWithOffset)); Commands->MapAction(FCurveEditorCommands::Get().SetPostInfinityExtrapOscillate, FExecuteAction::CreateSP(this, &SCurveEditor::OnSelectPostInfinityExtrap, RCCE_Oscillate), FCanExecuteAction(), FIsActionChecked::CreateSP(this, &SCurveEditor::IsPostInfinityExtrapSelected, RCCE_Oscillate)); Commands->MapAction(FCurveEditorCommands::Get().SetPostInfinityExtrapLinear, FExecuteAction::CreateSP(this, &SCurveEditor::OnSelectPostInfinityExtrap, RCCE_Linear), FCanExecuteAction(), FIsActionChecked::CreateSP(this, &SCurveEditor::IsPostInfinityExtrapSelected, RCCE_Linear)); Commands->MapAction(FCurveEditorCommands::Get().SetPostInfinityExtrapConstant, FExecuteAction::CreateSP(this, &SCurveEditor::OnSelectPostInfinityExtrap, RCCE_Constant), FCanExecuteAction(), FIsActionChecked::CreateSP(this, &SCurveEditor::IsPostInfinityExtrapSelected, RCCE_Constant)); // Tangent Visibility Commands->MapAction(FCurveEditorCommands::Get().SetAllTangentsVisibility, FExecuteAction::CreateLambda( [this]{ Settings->SetTangentVisibility( ECurveEditorTangentVisibility::AllTangents ); } ), FCanExecuteAction::CreateLambda( []{ return true; } ), FIsActionChecked::CreateLambda( [this]{ return Settings->GetTangentVisibility() == ECurveEditorTangentVisibility::AllTangents; } ) ); Commands->MapAction(FCurveEditorCommands::Get().SetUserTangentsVisibility, FExecuteAction::CreateLambda( [this]{ Settings->SetTangentVisibility( ECurveEditorTangentVisibility::UserTangents ); } ), FCanExecuteAction::CreateLambda( []{ return true; } ), FIsActionChecked::CreateLambda( [this]{ return Settings->GetTangentVisibility() == ECurveEditorTangentVisibility::UserTangents; } ) ); Commands->MapAction(FCurveEditorCommands::Get().SetSelectedKeysTangentVisibility, FExecuteAction::CreateLambda( [this]{ Settings->SetTangentVisibility( ECurveEditorTangentVisibility::SelectedKeys ); } ), FCanExecuteAction::CreateLambda( []{ return true; } ), FIsActionChecked::CreateLambda( [this]{ return Settings->GetTangentVisibility() == ECurveEditorTangentVisibility::SelectedKeys; } ) ); Commands->MapAction(FCurveEditorCommands::Get().SetNoTangentsVisibility, FExecuteAction::CreateLambda( [this]{ Settings->SetTangentVisibility( ECurveEditorTangentVisibility::NoTangents ); } ), FCanExecuteAction::CreateLambda( []{ return true; } ), FIsActionChecked::CreateLambda( [this]{ return Settings->GetTangentVisibility() == ECurveEditorTangentVisibility::NoTangents; } ) ); Commands->MapAction(FCurveEditorCommands::Get().ToggleAutoFrameCurveEditor, FExecuteAction::CreateLambda( [this]{ Settings->SetAutoFrameCurveEditor( !Settings->GetAutoFrameCurveEditor() ); } ), FCanExecuteAction::CreateLambda( []{ return true; } ), FIsActionChecked::CreateLambda( [this]{ return Settings->GetAutoFrameCurveEditor(); } ) ); Commands->MapAction(FCurveEditorCommands::Get().ToggleShowCurveEditorCurveToolTips, FExecuteAction::CreateLambda( [this]{ Settings->SetShowCurveEditorCurveToolTips( !Settings->GetShowCurveEditorCurveToolTips() ); if (!Settings->GetShowCurveEditorCurveToolTips()) { CurveToolTip.Reset(); SetToolTip(CurveToolTip); } } ), FCanExecuteAction::CreateLambda( []{ return true; } ), FIsActionChecked::CreateLambda( [this]{ return Settings->GetShowCurveEditorCurveToolTips(); } ) ); FCoreUObjectDelegates::OnPackageReloaded.AddSP(this, &SCurveEditor::HandlePackageReloaded); SAssignNew(WarningMessageText, SErrorText); TSharedRef CurveSelector = SNew(SBox) .VAlign(VAlign_Top) .Visibility(this, &SCurveEditor::GetCurveSelectorVisibility) [ CreateCurveSelectionWidget() ]; CurveSelectionWidget = CurveSelector; InputAxisName = InArgs._XAxisName.IsSet() ? FText::FromString(InArgs._XAxisName.GetValue()) : LOCTEXT("Time", "Time"); InputFrameAxisName = InArgs._XAxisName.IsSet() ? FText::FromString(InArgs._XAxisName.GetValue()) : LOCTEXT("Frame", "Frame"); OutputAxisName = InArgs._YAxisName.IsSet() ? FText::FromString(InArgs._YAxisName.GetValue()) : LOCTEXT("Value", "Value"); ChildSlot [ SNew( SVerticalBox ) + SVerticalBox::Slot() .FillHeight(1.0f) [ SNew(SHorizontalBox) .Visibility( this, &SCurveEditor::GetCurveAreaVisibility ) + SHorizontalBox::Slot() .AutoWidth() .Padding(FMargin(30, 12, 0, 0)) [ CurveSelector ] + SHorizontalBox::Slot() .AutoWidth() [ SNew(SBorder) .VAlign(VAlign_Top) .HAlign(HAlign_Left) .BorderImage( FAppStyle::GetBrush("NoBorder") ) .DesiredSizeScale(FVector2D(256.0f,32.0f)) .Padding(FMargin(2, 12, 0, 0)) [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() [ SNew(SButton) .ToolTipText(LOCTEXT("ZoomToFitHorizontal", "Zoom To Fit Horizontal")) .Visibility(this, &SCurveEditor::GetZoomButtonVisibility) .OnClicked(this, &SCurveEditor::ZoomToFitHorizontalClicked) .ContentPadding(1) [ SNew(SImage) .Image( FAppStyle::GetBrush("CurveEd.FitHorizontal") ) .ColorAndOpacity( FSlateColor::UseForeground() ) ] ] + SHorizontalBox::Slot() .AutoWidth() [ SNew(SButton) .ToolTipText(LOCTEXT("ZoomToFitVertical", "Zoom To Fit Vertical")) .Visibility(this, &SCurveEditor::GetZoomButtonVisibility) .OnClicked(this, &SCurveEditor::ZoomToFitVerticalClicked) .ContentPadding(1) [ SNew(SImage) .Image( FAppStyle::GetBrush("CurveEd.FitVertical") ) .ColorAndOpacity( FSlateColor::UseForeground() ) ] ] + SHorizontalBox::Slot() .AutoWidth() [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .Padding(6.0f, 0.0, 3.0f, 0.0f) .VAlign(VAlign_Center) [ SNew(STextBlock) .Visibility(this, &SCurveEditor::GetEditVisibility) .Text(this, &SCurveEditor::GetInputAxisName) .ShadowOffset(FVector2D(1,1)) ] + SHorizontalBox::Slot() .VAlign(VAlign_Center) .AutoWidth() [ SNew(SNumericEntryBox) .IsEnabled(this, &SCurveEditor::GetInputEditEnabled) .Value(this, &SCurveEditor::OnGetTime) .UndeterminedString(LOCTEXT("MultipleValues", "Multiple Values")) .OnValueCommitted(this, &SCurveEditor::OnTimeComitted) .OnValueChanged(this, &SCurveEditor::OnTimeChanged) .OnBeginSliderMovement(this, &SCurveEditor::OnBeginSliderMovement, LOCTEXT("SetTime", "Set New Time")) .OnEndSliderMovement(this, &SCurveEditor::OnEndSliderMovement) .AllowSpin(true) .MinValue(TOptional()) .MaxValue(TOptional()) .MaxSliderValue(TOptional()) .MinSliderValue(TOptional()) .Delta(this, &SCurveEditor::GetInputNumericEntryBoxDelta) .MinDesiredValueWidth(60.0f) .Visibility(this, &SCurveEditor::GetTimeEditVisibility) ] + SHorizontalBox::Slot() .Padding(3.0f, 0.0f) .VAlign(VAlign_Center) .AutoWidth() [ SNew(SNumericEntryBox) .IsEnabled(this, &SCurveEditor::GetInputEditEnabled) .Value(this, &SCurveEditor::OnGetTimeInFrames) .UndeterminedString(LOCTEXT("MultipleValues", "Multiple Values")) .OnValueCommitted(this, &SCurveEditor::OnTimeInFramesComitted) .OnValueChanged(this, &SCurveEditor::OnTimeInFramesChanged) .OnBeginSliderMovement(this, &SCurveEditor::OnBeginSliderMovement, LOCTEXT("SetFrame", "Set New Frame")) .OnEndSliderMovement(this, &SCurveEditor::OnEndSliderMovement) .LabelVAlign(VAlign_Center) .AllowSpin(true) .MinValue(TOptional()) .MaxValue(TOptional()) .MaxSliderValue(TOptional()) .MinSliderValue(TOptional()) .Delta(1) .MinDesiredValueWidth(60.0f) .Visibility(this, &SCurveEditor::GetFrameEditVisibility) ] ] + SHorizontalBox::Slot() .Padding(3.0f, 0.0f) .VAlign(VAlign_Center) [ SNew(STextBlock) .Visibility(this, &SCurveEditor::GetEditVisibility) .Text(OutputAxisName) .ShadowOffset(FVector2D(1, 1)) ] + SHorizontalBox::Slot() .VAlign(VAlign_Center) .AutoWidth() [ SNew(SNumericEntryBox) .Visibility(this, &SCurveEditor::GetEditVisibility) .Value(this, &SCurveEditor::OnGetValue) .UndeterminedString(LOCTEXT("MultipleValues", "Multiple Values")) .OnValueCommitted(this, &SCurveEditor::OnValueComitted) .OnValueChanged(this, &SCurveEditor::OnValueChanged) .OnBeginSliderMovement(this, &SCurveEditor::OnBeginSliderMovement, LOCTEXT("SetValue", "Set New Value")) .OnEndSliderMovement(this, &SCurveEditor::OnEndSliderMovement) .AllowSpin(true) .MinValue(TOptional()) .MaxValue(TOptional()) .MaxSliderValue(TOptional()) .MinSliderValue(TOptional()) .Delta(this, &SCurveEditor::GetOutputNumericEntryBoxDelta) .MinDesiredValueWidth(60.0f) ] ] ] ] + SVerticalBox::Slot() .VAlign(VAlign_Bottom) .FillHeight(.75f) [ SNew( SBorder ) .Visibility( this, &SCurveEditor::GetColorGradientVisibility ) .BorderImage( FAppStyle::GetBrush("ToolPanel.GroupBorder") ) .BorderBackgroundColor( FLinearColor( .8f, .8f, .8f, .60f ) ) .Padding(1.0f) [ SAssignNew( GradientViewer, SColorGradientEditor ) .ViewMinInput( ViewMinInput ) .ViewMaxInput( ViewMaxInput ) .IsEditingEnabled( this, &SCurveEditor::IsEditingEnabled ) ] ] ]; if (GEditor != NULL) { GEditor->RegisterForUndo(this); } FCoreUObjectDelegates::OnObjectPropertyChanged.AddSP(this, &SCurveEditor::OnObjectPropertyChanged); } FText SCurveEditor::GetIsCurveVisibleToolTip(TSharedPtr CurveViewModel) const { return CurveViewModel->bIsVisible ? FText::Format(LOCTEXT("HideFormat", "Hide {0} curve"), FText::FromName(CurveViewModel->CurveInfo.CurveName)) : FText::Format(LOCTEXT("ShowFormat", "Show {0} curve"), FText::FromName(CurveViewModel->CurveInfo.CurveName)); } ECheckBoxState SCurveEditor::IsCurveVisible(TSharedPtr CurveViewModel) const { return CurveViewModel->bIsVisible ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; } void SCurveEditor::OnCurveIsVisibleChanged(ECheckBoxState NewCheckboxState, TSharedPtr CurveViewModel) { if (NewCheckboxState == ECheckBoxState::Checked) { CurveViewModel->bIsVisible = true; } else { CurveViewModel->bIsVisible = false; RemoveCurveKeysFromSelection(CurveViewModel); } } FText SCurveEditor::GetIsCurveLockedToolTip(TSharedPtr CurveViewModel) const { return CurveViewModel->bIsLocked ? FText::Format(LOCTEXT("UnlockFormat", "Unlock {0} curve for editing"), FText::FromName(CurveViewModel->CurveInfo.CurveName)) : FText::Format(LOCTEXT("LockFormat", "Lock {0} curve for editing"), FText::FromName(CurveViewModel->CurveInfo.CurveName)); } ECheckBoxState SCurveEditor::IsCurveLocked(TSharedPtr CurveViewModel) const { return CurveViewModel->bIsLocked ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; } void SCurveEditor::OnCurveIsLockedChanged(ECheckBoxState NewCheckboxState, TSharedPtr CurveViewModel) { if (NewCheckboxState == ECheckBoxState::Checked) { CurveViewModel->bIsLocked = true; RemoveCurveKeysFromSelection(CurveViewModel); } else { CurveViewModel->bIsLocked = false; } } void SCurveEditor::RemoveCurveKeysFromSelection(TSharedPtr CurveViewModel) { TArray SelectedKeysForLockedCurve; for (auto SelectedKey : SelectedKeys) { if (SelectedKey.Curve == CurveViewModel->CurveInfo.CurveToEdit) { SelectedKeysForLockedCurve.Add(SelectedKey); } } for (auto KeyToDeselect : SelectedKeysForLockedCurve) { RemoveFromKeySelection(KeyToDeselect); } } FText SCurveEditor::GetCurveToolTipNameText() const { return CurveToolTipNameText; } FText SCurveEditor::GetCurveToolTipInputText() const { return CurveToolTipInputText; } FText SCurveEditor::GetCurveToolTipOutputText() const { return CurveToolTipOutputText; } FText SCurveEditor::GetInputAxisName() const { return ShowTimeInFrames() ? InputFrameAxisName : InputAxisName; } SCurveEditor::~SCurveEditor() { if (GEditor != NULL) { GEditor->UnregisterForUndo(this); } FCoreUObjectDelegates::OnObjectPropertyChanged.RemoveAll(this); } TSharedRef SCurveEditor::CreateCurveSelectionWidget() const { TSharedRef CurveBox = SNew(SVerticalBox); if (CurveViewModels.Num() > 1) { // Only create curve controls if there are more than one. for (auto CurveViewModel : CurveViewModels) { CurveBox->AddSlot() .AutoHeight() [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .Padding(0, 0, 5, 0) .FillWidth(1.0f) [ SNew(STextBlock) .Font(FAppStyle::GetFontStyle("CurveEd.LabelFont")) .ColorAndOpacity(CurveViewModel->Color) .Text(FText::FromName(CurveViewModel->CurveInfo.CurveName)) ] + SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Fill) [ SNew(SCheckBox) .Style(&FAppStyle::Get().GetWidgetStyle("ToggleButtonCheckbox")) .IsChecked(this, &SCurveEditor::IsCurveVisible, CurveViewModel) .OnCheckStateChanged(const_cast(this), &SCurveEditor::OnCurveIsVisibleChanged, CurveViewModel) .ToolTipText(this, &SCurveEditor::GetIsCurveVisibleToolTip, CurveViewModel) .CheckedImage(FAppStyle::Get().GetBrush("Icons.Visible")) .CheckedHoveredImage(FAppStyle::Get().GetBrush("Icons.Visible")) .CheckedPressedImage(FAppStyle::Get().GetBrush("Icons.Visible")) .UncheckedImage(FAppStyle::Get().GetBrush("Icons.Hidden")) .UncheckedHoveredImage(FAppStyle::Get().GetBrush("Icons.Hidden")) .UncheckedPressedImage(FAppStyle::Get().GetBrush("Icons.Hidden")) .Padding(8.0f) ] + SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Fill) .Padding(2, 0, 0, 0) [ SNew(SCheckBox) .Style(&FAppStyle::Get().GetWidgetStyle("ToggleButtonCheckbox")) .IsChecked(this, &SCurveEditor::IsCurveLocked, CurveViewModel) .OnCheckStateChanged(const_cast(this), &SCurveEditor::OnCurveIsLockedChanged, CurveViewModel) .ToolTipText(this, &SCurveEditor::GetIsCurveLockedToolTip, CurveViewModel) .CheckedImage(FAppStyle::Get().GetBrush("Icons.Lock")) .CheckedHoveredImage(FAppStyle::Get().GetBrush("Icons.Lock")) .CheckedPressedImage(FAppStyle::Get().GetBrush("Icons.Lock")) .UncheckedImage(FAppStyle::Get().GetBrush("Icons.Unlock")) .UncheckedHoveredImage(FAppStyle::Get().GetBrush("Icons.Unlock")) .UncheckedPressedImage(FAppStyle::Get().GetBrush("Icons.Unlock")) .Padding(8.0f) .Visibility(bCanEditTrack ? EVisibility::Visible : EVisibility::Collapsed) ] ]; } } TSharedRef Border = SNew(SBorder) .Padding(FMargin(3, 2, 2, 2)) .BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder")) .BorderBackgroundColor(FLinearColor(0.0f, 0.0f, 0.0f, 0.3f)) [ CurveBox ]; return Border; } void SCurveEditor::PushWarningMenu( FVector2D Position, const FText& Message ) { WarningMessageText->SetError(Message); FSlateApplication::Get().PushMenu( SharedThis( this ), FWidgetPath(), WarningMessageText->AsWidget(), Position, FPopupTransitionEffect( FPopupTransitionEffect::ContextMenu)); } void SCurveEditor::PushKeyMenu(const FGeometry& InMyGeometry, const FPointerEvent& InMouseEvent) { FMenuBuilder MenuBuilder(true, Commands.ToSharedRef()); MenuBuilder.BeginSection("CurveEditorInterpolation", LOCTEXT("KeyInterpolationMode", "Key Interpolation")); { MenuBuilder.AddMenuEntry(FCurveEditorCommands::Get().InterpolationCubicAuto); MenuBuilder.AddMenuEntry(FCurveEditorCommands::Get().InterpolationCubicUser); MenuBuilder.AddMenuEntry(FCurveEditorCommands::Get().InterpolationCubicBreak); MenuBuilder.AddMenuEntry(FCurveEditorCommands::Get().InterpolationLinear); MenuBuilder.AddMenuEntry(FCurveEditorCommands::Get().InterpolationConstant); } MenuBuilder.EndSection(); //CurveEditorInterpolation MenuBuilder.BeginSection("CurveEditorTangents", LOCTEXT("Tangents", "Tangents")); { MenuBuilder.AddMenuEntry(FCurveEditorCommands::Get().FlattenTangents); MenuBuilder.AddMenuEntry(FCurveEditorCommands::Get().StraightenTangents); } MenuBuilder.EndSection(); //CurveEditorTangents FWidgetPath WidgetPath = InMouseEvent.GetEventPath() != nullptr ? *InMouseEvent.GetEventPath() : FWidgetPath(); FVector2D Position = InMouseEvent.GetScreenSpacePosition(); FSlateApplication::Get().PushMenu( SharedThis( this ), WidgetPath, MenuBuilder.MakeWidget(), Position, FPopupTransitionEffect( FPopupTransitionEffect::ContextMenu)); } FVector2D SCurveEditor::ComputeDesiredSize( float ) const { return DesiredSize.Get(); } EVisibility SCurveEditor::GetCurveAreaVisibility() const { return AreCurvesVisible() ? EVisibility::Visible : EVisibility::Collapsed; } EVisibility SCurveEditor::GetCurveSelectorVisibility() const { return (IsHovered() || (false == bHideUI)) && bShowCurveSelector ? EVisibility::Visible : EVisibility::Collapsed; } EVisibility SCurveEditor::GetEditVisibility() const { return (SelectedKeys.Num() > 0) && (IsHovered() || (false == bHideUI)) ? EVisibility::Visible : EVisibility::Collapsed; } EVisibility SCurveEditor::GetColorGradientVisibility() const { return IsGradientEditorVisible() && IsLinearColorCurve() ? EVisibility::Visible : EVisibility::Collapsed; } EVisibility SCurveEditor::GetZoomButtonVisibility() const { return (IsHovered() || (false == bHideUI)) && bShowZoomButtons ? EVisibility::Visible : EVisibility::Collapsed; } EVisibility SCurveEditor::GetTimeEditVisibility() const { if (GetEditVisibility().IsVisible()) { return ShowTimeInFrames() ? EVisibility::Collapsed : EVisibility::Visible; } return EVisibility::Collapsed; } EVisibility SCurveEditor::GetFrameEditVisibility() const { if (GetEditVisibility().IsVisible()) { return ShowTimeInFrames() ? EVisibility::Visible : EVisibility::Collapsed; } return EVisibility::Collapsed; } bool SCurveEditor::GetInputEditEnabled() const { bool bKeysOnSameCurves = false; for (int32 SelectedIndex = 0; SelectedIndex < SelectedKeys.Num() - 1; SelectedIndex++) { for (int32 CompareIndex = SelectedIndex + 1; CompareIndex < SelectedKeys.Num(); CompareIndex++) { if (SelectedKeys[SelectedIndex].Curve == SelectedKeys[CompareIndex].Curve) { bKeysOnSameCurves = true; break; } } } return (SelectedKeys.Num() == 1) || !bKeysOnSameCurves; } int32 SCurveEditor::OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled ) const { // Rendering info bool bEnabled = ShouldBeEnabled( bParentEnabled ); ESlateDrawEffect DrawEffects = bEnabled ? ESlateDrawEffect::None : ESlateDrawEffect::DisabledEffect; const FSlateBrush* TimelineAreaBrush = FAppStyle::GetBrush("CurveEd.TimelineArea"); const FSlateBrush* WhiteBrush = FAppStyle::GetBrush("WhiteTexture"); FGeometry CurveAreaGeometry = AllottedGeometry; // Positioning info FTrackScaleInfo ScaleInfo(ViewMinInput.Get(), ViewMaxInput.Get(), ViewMinOutput.Get(), ViewMaxOutput.Get(), CurveAreaGeometry.GetLocalSize()); if (FMath::IsNearlyEqual(ViewMinInput.Get(), ViewMaxInput.Get()) || FMath::IsNearlyEqual(ViewMinOutput.Get(), ViewMaxOutput.Get())) { return 0; } // Draw background to indicate valid timeline area float ZeroInputX = ScaleInfo.InputToLocalX(0.f); float ZeroOutputY = ScaleInfo.OutputToLocalY(0.f); // timeline background int32 BackgroundLayerId = LayerId; float TimelineMaxX = ScaleInfo.InputToLocalX(TimelineLength.Get()); FSlateDrawElement::MakeBox ( OutDrawElements, BackgroundLayerId, CurveAreaGeometry.ToPaintGeometry(FVector2D(TimelineMaxX - ZeroInputX, CurveAreaGeometry.GetLocalSize().Y), FSlateLayoutTransform(FVector2D(ZeroInputX, 0.f))), TimelineAreaBrush, DrawEffects, TimelineAreaBrush->GetTint(InWidgetStyle) * InWidgetStyle.GetColorAndOpacityTint() ); // grid lines. int32 GridLineLayerId = BackgroundLayerId + 1; PaintGridLines(CurveAreaGeometry, ScaleInfo, OutDrawElements, GridLineLayerId, MyCullingRect, DrawEffects); // time=0 line int32 ZeroLineLayerId = GridLineLayerId + 1; TArray ZeroLinePoints; ZeroLinePoints.Add( FVector2D( ZeroInputX, 0 ) ); ZeroLinePoints.Add( FVector2D( ZeroInputX, CurveAreaGeometry.GetLocalSize().Y ) ); FSlateDrawElement::MakeLines( OutDrawElements, ZeroLineLayerId, AllottedGeometry.ToPaintGeometry(), ZeroLinePoints, DrawEffects, FLinearColor::White, false ); // value=0 line if( AreCurvesVisible() ) { FSlateDrawElement::MakeBox ( OutDrawElements, ZeroLineLayerId, CurveAreaGeometry.ToPaintGeometry( FVector2D(CurveAreaGeometry.Size.X, 1), FSlateLayoutTransform(FVector2D(0.f, ZeroOutputY)) ), WhiteBrush, DrawEffects, WhiteBrush->GetTint( InWidgetStyle ) * InWidgetStyle.GetColorAndOpacityTint() ); } int32 LockedCurveLayerID = ZeroLineLayerId + 1; int32 CurveLayerId = LockedCurveLayerID + 1; int32 KeyLayerId = CurveLayerId + 1; int32 SelectedKeyLayerId = KeyLayerId + 1; bool bAnyCurveViewModelsSelected = AnyCurveViewModelsSelected(); if( AreCurvesVisible() ) { //Paint the curves, unlocked curves will be on top for ( auto CurveViewModel : CurveViewModels ) { if (CurveViewModel->bIsVisible) { PaintCurve(CurveViewModel, CurveAreaGeometry, ScaleInfo, OutDrawElements, CurveViewModel->bIsLocked ? LockedCurveLayerID : CurveLayerId, MyCullingRect, DrawEffects, InWidgetStyle, bAnyCurveViewModelsSelected); } } //Paint the keys on top of the curve for (auto CurveViewModel : CurveViewModels) { if (CurveViewModel->bIsVisible) { PaintKeys(CurveViewModel, ScaleInfo, OutDrawElements, KeyLayerId, SelectedKeyLayerId, CurveAreaGeometry, MyCullingRect, DrawEffects, InWidgetStyle, bAnyCurveViewModelsSelected); } } } // Paint children int32 ChildrenLayerId = SelectedKeyLayerId + 1; int32 MarqueeLayerId = SCompoundWidget::OnPaint(Args, CurveAreaGeometry, MyCullingRect, OutDrawElements, ChildrenLayerId, InWidgetStyle, bParentEnabled); // Paint marquee if (DragState == EDragState::MarqueeSelect) { PaintMarquee(AllottedGeometry, MyCullingRect, OutDrawElements, MarqueeLayerId); } return MarqueeLayerId + 1; } void SCurveEditor::PaintCurve(TSharedPtr CurveViewModel, const FGeometry &AllottedGeometry, FTrackScaleInfo &ScaleInfo, FSlateWindowElementList &OutDrawElements, int32 LayerId, const FSlateRect& MyCullingRect, ESlateDrawEffect DrawEffects, const FWidgetStyle &InWidgetStyle, bool bAnyCurveViewModelsSelected )const { if (CurveViewModel.IsValid()) { if (bDrawCurve) { FLinearColor Color = InWidgetStyle.GetColorAndOpacityTint() * CurveViewModel->Color; // Fade out curves that are not selected. if (!CurveViewModel->bIsSelected && bAnyCurveViewModelsSelected) { Color *= FLinearColor(1.0f,1.0f,1.0f,0.2f); } // Fade out curves which are locked. if(CurveViewModel->bIsLocked) { Color *= FLinearColor(1.0f,1.0f,1.0f,0.35f); } TArray LinePoints; int32 CurveDrawInterval = 1; FRealCurve* Curve = CurveViewModel->CurveInfo.CurveToEdit; const float NumKeys = Curve->GetNumKeys(); if (NumKeys < 2) { //Not enough point, just draw flat line float Value = Curve->Eval(0.0f); float Y = ScaleInfo.OutputToLocalY(Value); LinePoints.Add(FVector2D(0.0f, Y)); LinePoints.Add(FVector2D(AllottedGeometry.GetLocalSize().X, Y)); FSlateDrawElement::MakeLines(OutDrawElements, LayerId, AllottedGeometry.ToPaintGeometry(), LinePoints, DrawEffects, Color); LinePoints.Empty(); } else { TArray KeyHandles; TArray> Key_TimeValuePairs; KeyHandles.Reserve(NumKeys); Key_TimeValuePairs.Reserve(NumKeys); for (auto It = Curve->GetKeyHandleIterator(); It; ++It) { const FKeyHandle& KeyHandle = *It; KeyHandles.Add(KeyHandle); Key_TimeValuePairs.Emplace(Curve->GetKeyTimeValuePair(KeyHandle)); } //Add arrive and exit lines { float ArriveX = ScaleInfo.InputToLocalX(Key_TimeValuePairs[0].Key); float ArriveY = ScaleInfo.OutputToLocalY(Key_TimeValuePairs[0].Value); float LeaveY = ScaleInfo.OutputToLocalY(Key_TimeValuePairs.Last().Value); float LeaveX = ScaleInfo.InputToLocalX(Key_TimeValuePairs.Last().Key); //Arrival line LinePoints.Add(FVector2D(0.0f, ArriveY)); LinePoints.Add(FVector2D(ArriveX, ArriveY)); FSlateDrawElement::MakeLines(OutDrawElements, LayerId, AllottedGeometry.ToPaintGeometry(), LinePoints, DrawEffects, Color); LinePoints.Empty(); //Leave line LinePoints.Add(FVector2D(AllottedGeometry.GetLocalSize().X, LeaveY)); LinePoints.Add(FVector2D(LeaveX, LeaveY)); FSlateDrawElement::MakeLines(OutDrawElements, LayerId, AllottedGeometry.ToPaintGeometry(), LinePoints, DrawEffects, Color); LinePoints.Empty(); } //Add enclosed segments for (int32 i = 0;iGetKeyInterpMode(KeyHandles[i]), Key_TimeValuePairs[i], Key_TimeValuePairs[i+1], LinePoints, ScaleInfo); FSlateDrawElement::MakeLines(OutDrawElements, LayerId, AllottedGeometry.ToPaintGeometry(), LinePoints, DrawEffects, Color); LinePoints.Empty(); } } } } } void SCurveEditor::CreateLinesForSegment( FRealCurve* Curve, ERichCurveInterpMode InterpMode, const TPair& Key1_TimeValue, const TPair& Key2_TimeValue, TArray& Points, FTrackScaleInfo &ScaleInfo ) const { switch(InterpMode) { case RCIM_Constant: { //@todo: should really only need 3 points here but something about the line rendering isn't quite behaving as I'd expect, so need extras Points.Add(FVector2D(Key1_TimeValue.Key, Key1_TimeValue.Value)); Points.Add(FVector2D(Key2_TimeValue.Key, Key1_TimeValue.Value)); Points.Add(FVector2D(Key2_TimeValue.Key, Key1_TimeValue.Value)); Points.Add(FVector2D(Key2_TimeValue.Key, Key2_TimeValue.Value)); Points.Add(FVector2D(Key2_TimeValue.Key, Key1_TimeValue.Value)); }break; case RCIM_Linear: { Points.Add(FVector2D(Key1_TimeValue.Key, Key1_TimeValue.Value)); Points.Add(FVector2D(Key2_TimeValue.Key, Key2_TimeValue.Value)); }break; case RCIM_Cubic: { const float StepSize = 1.0f; //clamp to screen to avoid massive slowdown when zoomed in float StartX = FMath::Max(ScaleInfo.InputToLocalX(Key1_TimeValue.Key), 0.0f) ; float EndX = FMath::Min(ScaleInfo.InputToLocalX(Key2_TimeValue.Key),ScaleInfo.WidgetSize.X); for(;StartXEval(CurveIn); Points.Add(FVector2D(CurveIn,CurveOut)); } Points.Add(FVector2D(Key2_TimeValue.Key,Key2_TimeValue.Value)); }break; } //Transform to screen for(auto It = Points.CreateIterator();It;++It) { FVector2D Vec2D = *It; Vec2D.X = ScaleInfo.InputToLocalX(Vec2D.X); Vec2D.Y = ScaleInfo.OutputToLocalY(Vec2D.Y); *It = Vec2D; } } void SCurveEditor::PaintKeys(TSharedPtr CurveViewModel, FTrackScaleInfo &ScaleInfo, FSlateWindowElementList &OutDrawElements, int32 LayerId, int32 SelectedLayerId, const FGeometry &AllottedGeometry, const FSlateRect& MyCullingRect, ESlateDrawEffect DrawEffects, const FWidgetStyle &InWidgetStyle, bool bAnyCurveViewModelsSelected ) const { FLinearColor KeyColor = CurveViewModel->bIsLocked ? FLinearColor(0.1f,0.1f,0.1f,1.f) : InWidgetStyle.GetColorAndOpacityTint(); const bool bHasRichCurves = CurveOwner->HasRichCurves(); // Iterate over each key ERichCurveInterpMode LastInterpMode = RCIM_Linear; FRealCurve* Curve = CurveViewModel->CurveInfo.CurveToEdit; for (auto It(Curve->GetKeyHandleIterator()); It; ++It) { FKeyHandle KeyHandle = *It; // Work out where it is FVector2D KeyLocation( ScaleInfo.InputToLocalX(Curve->GetKeyTime(KeyHandle)), ScaleInfo.OutputToLocalY(Curve->GetKeyValue(KeyHandle))); FVector2D KeyIconLocation = KeyLocation - (CONST_KeySize / 2); // Get brush bool IsSelected = IsKeySelected(FSelectedCurveKey(Curve,KeyHandle)); const FSlateBrush* KeyBrush = IsSelected ? FAppStyle::GetBrush("CurveEd.CurveKeySelected") : FAppStyle::GetBrush("CurveEd.CurveKey"); int32 LayerToUse = IsSelected ? SelectedLayerId: LayerId; // Fade out keys that are not selected and whose curve is not selected as well. FLinearColor SelectionTint = !CurveViewModel->bIsSelected && !IsSelected && bAnyCurveViewModelsSelected ? FLinearColor(1.0f,1.0f,1.0f,0.2f) : FLinearColor(1.0f,1.0f,1.0f,1.0f); FSlateDrawElement::MakeBox( OutDrawElements, LayerToUse, AllottedGeometry.ToPaintGeometry( CONST_KeySize, FSlateLayoutTransform(KeyIconLocation) ), KeyBrush, DrawEffects, KeyBrush->GetTint( InWidgetStyle ) * InWidgetStyle.GetColorAndOpacityTint() * KeyColor * SelectionTint ); //Handle drawing the tangent controls for curve bool bIsTangentSelected = false; bool bIsArrivalSelected = false; bool bIsLeaveSelected = false; if (bHasRichCurves) { FRichCurve* RichCurve = (FRichCurve*)Curve; if (IsTangentVisible(RichCurve, KeyHandle, bIsTangentSelected, bIsArrivalSelected, bIsLeaveSelected) && (RichCurve->GetKeyInterpMode(KeyHandle) == RCIM_Cubic || LastInterpMode == RCIM_Cubic)) { PaintTangent(CurveViewModel, ScaleInfo, RichCurve, KeyHandle, KeyLocation, OutDrawElements, LayerId, AllottedGeometry, MyCullingRect, DrawEffects, LayerToUse, InWidgetStyle, bIsTangentSelected, bIsArrivalSelected, bIsLeaveSelected, bAnyCurveViewModelsSelected); } } LastInterpMode = Curve->GetKeyInterpMode(KeyHandle); } } void SCurveEditor::PaintTangent( TSharedPtr CurveViewModel, FTrackScaleInfo &ScaleInfo, FRichCurve* Curve, FKeyHandle KeyHandle, FVector2D KeyLocation, FSlateWindowElementList &OutDrawElements, int32 LayerId, const FGeometry &AllottedGeometry, const FSlateRect& MyCullingRect, ESlateDrawEffect DrawEffects, int32 LayerToUse, const FWidgetStyle &InWidgetStyle, bool bTangentSelected, bool bIsArrivalSelected, bool bIsLeaveSelected, bool bAnyCurveViewModelsSelected ) const { FVector2D ArriveTangentLocation, LeaveTangentLocation; GetTangentPoints(ScaleInfo, FSelectedCurveKey(Curve,KeyHandle), ArriveTangentLocation, LeaveTangentLocation); FVector2D ArriveTangentIconLocation = ArriveTangentLocation - (CONST_TangentSize / 2); FVector2D LeaveTangentIconLocation = LeaveTangentLocation - (CONST_TangentSize / 2); const FSlateBrush* TangentBrush = FAppStyle::GetBrush("CurveEd.Tangent"); const FSlateBrush* TangentBrushSelected = FAppStyle::GetBrush("CurveEd.TangentSelected"); const FLinearColor TangentColor = FAppStyle::GetColor("CurveEd.TangentColor"); const FLinearColor TangentColorSelected = FAppStyle::GetColor("CurveEd.TangentColorSelected"); bool LeaveTangentSelected = bTangentSelected && bIsLeaveSelected; bool ArriveTangentSelected = bTangentSelected && bIsArrivalSelected; FLinearColor LeaveSelectionTint = !CurveViewModel->bIsSelected && !LeaveTangentSelected && bAnyCurveViewModelsSelected ? FLinearColor(1.0f,1.0f,1.0f,0.2f) : FLinearColor(1.0f,1.0f,1.0f,1.0f); FLinearColor ArriveSelectionTint = !CurveViewModel->bIsSelected && !ArriveTangentSelected && bAnyCurveViewModelsSelected ? FLinearColor(1.0f,1.0f,1.0f,0.2f) : FLinearColor(1.0f,1.0f,1.0f,1.0f); //Add lines from tangent control point to 'key' TArray LinePoints; LinePoints.Add(FVector2D(KeyLocation)); LinePoints.Add(FVector2D(ArriveTangentLocation)); FSlateDrawElement::MakeLines( OutDrawElements, LayerId, AllottedGeometry.ToPaintGeometry(), LinePoints, DrawEffects, ArriveTangentSelected ? TangentColorSelected * ArriveSelectionTint : TangentColor * ArriveSelectionTint ); LinePoints.Empty(); LinePoints.Add(FVector2D(KeyLocation)); LinePoints.Add(FVector2D(LeaveTangentLocation)); FSlateDrawElement::MakeLines( OutDrawElements, LayerId, AllottedGeometry.ToPaintGeometry(), LinePoints, DrawEffects, LeaveTangentSelected ? TangentColorSelected * LeaveSelectionTint : TangentColor * LeaveSelectionTint ); //Arrive tangent control FSlateDrawElement::MakeBox( OutDrawElements, LayerToUse, AllottedGeometry.ToPaintGeometry( CONST_TangentSize, FSlateLayoutTransform(ArriveTangentIconLocation) ), ArriveTangentSelected ? TangentBrushSelected : TangentBrush, DrawEffects, ArriveTangentSelected ? TangentBrushSelected->GetTint( InWidgetStyle ) * ArriveSelectionTint : TangentBrush->GetTint( InWidgetStyle ) * InWidgetStyle.GetColorAndOpacityTint() * ArriveSelectionTint ); //Leave tangent control FSlateDrawElement::MakeBox( OutDrawElements, LayerToUse, AllottedGeometry.ToPaintGeometry( CONST_TangentSize, FSlateLayoutTransform(LeaveTangentIconLocation) ), LeaveTangentSelected ? TangentBrushSelected : TangentBrush, DrawEffects, LeaveTangentSelected ? TangentBrushSelected->GetTint( InWidgetStyle ) * LeaveSelectionTint : TangentBrush->GetTint( InWidgetStyle ) * InWidgetStyle.GetColorAndOpacityTint() * LeaveSelectionTint ); } float SCurveEditor::CalcGridLineStepDistancePow2(double RawValue) { return float(double(FMath::RoundUpToPowerOfTwo(uint32(RawValue*1024.0))>>1)/1024.0); } float SCurveEditor::GetTimeStep(FTrackScaleInfo &ScaleInfo) const { const float MaxGridPixelSpacing = 150.0f; const float GridPixelSpacing = FMath::Min(ScaleInfo.WidgetSize.GetMin()/1.5f, MaxGridPixelSpacing); double MaxTimeStep = ScaleInfo.LocalXToInput(ViewMinInput.Get() + GridPixelSpacing) - ScaleInfo.LocalXToInput(ViewMinInput.Get()); return CalcGridLineStepDistancePow2(MaxTimeStep); } void SCurveEditor::PaintGridLines(const FGeometry &AllottedGeometry, FTrackScaleInfo &ScaleInfo, FSlateWindowElementList &OutDrawElements, int32 LayerId, const FSlateRect& MyCullingRect, ESlateDrawEffect DrawEffects )const { const float MaxGridPixelSpacing = 150.0f; const float GridPixelSpacing = FMath::Min(ScaleInfo.WidgetSize.GetMin()/1.5f, MaxGridPixelSpacing); const FLinearColor GridTextColor = FLinearColor(1.0f,1.0f,1.0f, 0.75f) ; //Vertical grid(time) { float TimeStep = GetTimeStep(ScaleInfo); float ScreenStepTime = ScaleInfo.InputToLocalX(TimeStep) - ScaleInfo.InputToLocalX(0.0f); if(ScreenStepTime >= 1.0f) { float StartTime = ScaleInfo.LocalXToInput(0.0f); TArray LinePoints; float ScaleX = (TimeStep)/(AllottedGeometry.GetLocalSize().X); //draw vertical grid lines float StartOffset = -FMath::Fractional(StartTime / TimeStep)*ScreenStepTime; float Time = ScaleInfo.LocalXToInput(StartOffset); for(float X = StartOffset;X< AllottedGeometry.GetLocalSize().X;X+= ScreenStepTime, Time += TimeStep) { if(SMALL_NUMBER < FMath::Abs(X)) //don't show at 0 to avoid overlapping with center axis { LinePoints.Add(FVector2D(X, 0.0)); LinePoints.Add(FVector2D(X, AllottedGeometry.GetLocalSize().Y)); FSlateDrawElement::MakeLines( OutDrawElements, LayerId, AllottedGeometry.ToPaintGeometry(), LinePoints, DrawEffects, GridColor, false); //Show grid time if (bDrawInputGridNumbers) { FString TimeStr = FString::Printf(TEXT("%.2f"), Time); FSlateDrawElement::MakeText(OutDrawElements,LayerId,AllottedGeometry.MakeChild(FVector2D(1.0f, ScaleX ), FSlateLayoutTransform(FVector2D(X, 0.0))).ToPaintGeometry(),TimeStr, FAppStyle::GetFontStyle("CurveEd.InfoFont"), DrawEffects, GridTextColor ); } LinePoints.Empty(); } } } } //Horizontal grid(values) // This is only useful if the curves are visible if( AreCurvesVisible() ) { double MaxValueStep = ScaleInfo.LocalYToOutput(0) - ScaleInfo.LocalYToOutput(GridPixelSpacing) ; float ValueStep = CalcGridLineStepDistancePow2(MaxValueStep); float ScreenStepValue = ScaleInfo.OutputToLocalY(0.0f) - ScaleInfo.OutputToLocalY(ValueStep); if(ScreenStepValue >= 1.0f) { float StartValue = ScaleInfo.LocalYToOutput(0.0f); TArray LinePoints; float StartOffset = FMath::Fractional(StartValue / ValueStep)*ScreenStepValue; float Value = ScaleInfo.LocalYToOutput(StartOffset); float ScaleY = (ValueStep)/(AllottedGeometry.GetLocalSize().Y); for(float Y = StartOffset;Y< AllottedGeometry.GetLocalSize().Y;Y+= ScreenStepValue, Value-=ValueStep) { if(SMALL_NUMBER < FMath::Abs(Y)) //don't show at 0 to avoid overlapping with center axis { LinePoints.Add(FVector2D(0.0f, Y)); LinePoints.Add(FVector2D(AllottedGeometry.GetLocalSize().X,Y)); FSlateDrawElement::MakeLines( OutDrawElements, LayerId, AllottedGeometry.ToPaintGeometry(), LinePoints, DrawEffects, GridColor, false); //Show grid value if (bDrawOutputGridNumbers) { FString ValueStr = FString::Printf(TEXT("%.2f"), Value); FSlateFontInfo Font = FAppStyle::GetFontStyle("CurveEd.InfoFont"); const TSharedRef< FSlateFontMeasure > FontMeasureService = FSlateApplication::Get().GetRenderer()->GetFontMeasureService(); FVector2D DrawSize = FontMeasureService->Measure(ValueStr, Font); // draw at the start FSlateDrawElement::MakeText(OutDrawElements,LayerId,AllottedGeometry.MakeChild(FVector2D(ScaleY, 1.0f ), FSlateLayoutTransform(FVector2D(0.0f, Y))).ToPaintGeometry(), ValueStr, Font, DrawEffects, GridTextColor ); // draw at the last since sometimes start can be hidden FSlateDrawElement::MakeText(OutDrawElements,LayerId,AllottedGeometry.MakeChild(FVector2D(ScaleY, 1.0f ), FSlateLayoutTransform(FVector2D(AllottedGeometry.GetLocalSize().X-DrawSize.X, Y))).ToPaintGeometry(), ValueStr, Font, DrawEffects, GridTextColor ); } LinePoints.Empty(); } } } } } void SCurveEditor::PaintMarquee(const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId) const { FVector2D MarqueTopLeft( FMath::Min(MouseDownLocation.X, MouseMoveLocation.X), FMath::Min(MouseDownLocation.Y, MouseMoveLocation.Y) ); FVector2D MarqueBottomRight( FMath::Max(MouseDownLocation.X, MouseMoveLocation.X), FMath::Max(MouseDownLocation.Y, MouseMoveLocation.Y) ); FSlateDrawElement::MakeBox( OutDrawElements, LayerId, AllottedGeometry.ToPaintGeometry(MarqueBottomRight - MarqueTopLeft, FSlateLayoutTransform(MarqueTopLeft)), FAppStyle::GetBrush(TEXT("MarqueeSelection")) ); } float SCurveEditor::GetInputNumericEntryBoxDelta() const { return bInputSnappingEnabled.Get() ? InputSnap.Get() : 0; } float SCurveEditor::GetOutputNumericEntryBoxDelta() const { return bOutputSnappingEnabled.Get() ? OutputSnap.Get() : 0; } void SCurveEditor::SetCurveOwner(FCurveOwnerInterface* InCurveOwner, bool bCanEdit) { if(InCurveOwner != CurveOwner) { EmptyAllSelection(); } GradientViewer->SetCurveOwner(InCurveOwner); CurveOwner = InCurveOwner; bCanEditTrack = bCanEdit; if (bAreCurvesVisible.IsBound() == false || SetAreCurvesVisibleHandler.IsBound() == false) { bAreCurvesVisible = !IsLinearColorCurve(); } bIsGradientEditorVisible = IsLinearColorCurve(); CurveViewModels.Empty(); if(CurveOwner != NULL) { for (const FRichCurveEditInfo& CurveInfo : CurveOwner->GetCurves()) { CurveViewModels.Add(TSharedPtr(new FCurveViewModel(CurveInfo, CurveOwner->GetCurveColor(CurveInfo), !bCanEdit))); } if (bCanEdit) { CurveOwner->MakeTransactional(); } } ValidateSelection(); if (GetAutoFrame()) { if( bZoomToFitVertical ) { ZoomToFitVertical(); } if ( bZoomToFitHorizontal ) { ZoomToFitHorizontal(); } } CurveSelectionWidget.Pin()->SetContent(CreateCurveSelectionWidget()); } void SCurveEditor::RegisterToPropertyChangedEvent(const TSharedPtr& InRootPropertyHandle) { RootPropertyHandleWeak = InRootPropertyHandle.ToWeakPtr(); const auto OnChildPropertyChanged = TDelegate::CreateSP(this, &SCurveEditor::OnRootPropertyChanged); if (const TSharedPtr RootPropertyHandle = RootPropertyHandleWeak.Pin()) { RootPropertyHandle->SetOnChildPropertyValueChangedWithData(OnChildPropertyChanged); } } void SCurveEditor::SetZoomToFit(bool bNewZoomToFitVertical, bool bNewZoomToFitHorizontal) { bZoomToFitVertical = bNewZoomToFitVertical; bZoomToFitHorizontal = bNewZoomToFitHorizontal; } void SCurveEditor::SetRequireFocusToZoom(bool bInRequireFocusToZoom) { bRequireFocusToZoom = bInRequireFocusToZoom; } TOptional SCurveEditor::OnQueryShowFocus(const EFocusCause InFocusCause) const { if (bRequireFocusToZoom) { // Enable showing a focus rectangle when the widget has keyboard focus return TOptional(true); } return TOptional(); } FCurveOwnerInterface* SCurveEditor::GetCurveOwner() const { return CurveOwner; } FRealCurve* SCurveEditor::GetCurve(int32 CurveIndex) const { if(CurveIndex < CurveViewModels.Num()) { return CurveViewModels[CurveIndex]->CurveInfo.CurveToEdit; } return NULL; } void SCurveEditor::DeleteSelectedKeys() { const FScopedTransaction Transaction(LOCTEXT("CurveEditor_RemoveKeys", "Delete Key(s)")); CurveOwner->ModifyOwner(); TSet ChangedCurves; // While there are still keys while(SelectedKeys.Num() > 0) { // Pull one out of the selected set FSelectedCurveKey Key = SelectedKeys.Pop(); if(IsValidCurve(Key.Curve)) { // Remove from the curve Key.Curve->DeleteKey(Key.KeyHandle); ChangedCurves.Add(Key.Curve); } } TArray ChangedCurveEditInfos; for (auto CurveViewModel : CurveViewModels) { if (ChangedCurves.Contains(CurveViewModel->CurveInfo.CurveToEdit)) { ChangedCurveEditInfos.Add(CurveViewModel->CurveInfo); } } CurveOwner->OnCurveChanged(ChangedCurveEditInfos); } FReply SCurveEditor::OnMouseButtonDown( const FGeometry& InMyGeometry, const FPointerEvent& InMouseEvent) { // End any transactions that weren't ended cleanly EndDragTransaction(); const bool bLeftMouseButton = InMouseEvent.GetEffectingButton() == EKeys::LeftMouseButton; const bool bMiddleMouseButton = InMouseEvent.GetEffectingButton() == EKeys::MiddleMouseButton; const bool bRightMouseButton = InMouseEvent.GetEffectingButton() == EKeys::RightMouseButton; DragState = EDragState::PreDrag; MovementAxisLock = EMovementAxisLock::None; if (bLeftMouseButton || bMiddleMouseButton || bRightMouseButton) { MouseDownLocation = InMyGeometry.AbsoluteToLocal(InMouseEvent.GetScreenSpacePosition()); // Set keyboard focus to this so that selected text box doesn't try to apply to newly selected keys if(!HasKeyboardFocus()) { FSlateApplication::Get().SetKeyboardFocus(SharedThis(this), EFocusCause::SetDirectly); } // Always capture mouse if we left or right click on the widget return FReply::Handled().CaptureMouse(SharedThis(this)); } return FReply::Unhandled(); } void SCurveEditor::AddNewKey(FGeometry InMyGeometry, FVector2D ScreenPosition, TSharedPtr>> CurvesToAddKeysTo, bool bAddKeysInline) { const FScopedTransaction Transaction(LOCTEXT("CurveEditor_AddKey", "Add Key(s)")); CurveOwner->ModifyOwner(); TArray ChangedCurveEditInfos; for (TSharedPtr CurveViewModel : *CurvesToAddKeysTo) { if (!CurveViewModel->bIsLocked) { FRealCurve* SelectedCurve = CurveViewModel->CurveInfo.CurveToEdit; if (IsValidCurve(SelectedCurve)) { FTrackScaleInfo ScaleInfo(ViewMinInput.Get(), ViewMaxInput.Get(), ViewMinOutput.Get(), ViewMaxOutput.Get(), InMyGeometry.GetLocalSize()); FVector2D LocalClickPos = InMyGeometry.AbsoluteToLocal(ScreenPosition); float Input = ScaleInfo.LocalXToInput(LocalClickPos.X); float Output; if (bAddKeysInline) { Output = SelectedCurve->Eval(Input); } else { Output = ScaleInfo.LocalYToOutput(LocalClickPos.Y); } FVector2D NewKeyLocation = SnapLocation(FVector2D(Input, Output)); FKeyHandle NewKeyHandle = SelectedCurve->AddKey(NewKeyLocation.X, NewKeyLocation.Y); EmptyAllSelection(); AddToKeySelection(FSelectedCurveKey(SelectedCurve, NewKeyHandle)); ChangedCurveEditInfos.Add(CurveViewModel->CurveInfo); } } } if (ChangedCurveEditInfos.Num() > 0) { CurveOwner->OnCurveChanged(ChangedCurveEditInfos); } } void SCurveEditor::OnMouseCaptureLost(const FCaptureLostEvent& CaptureLostEvent) { // if we began a drag transaction we need to finish it to make sure undo doesn't get out of sync if (DragState == EDragState::DragKey || DragState == EDragState::FreeDrag || DragState == EDragState::DragTangent) { EndDragTransaction(); } DragState = EDragState::None; } FReply SCurveEditor::OnMouseButtonUp( const FGeometry& InMyGeometry, const FPointerEvent& InMouseEvent ) { if (this->HasMouseCapture()) { if (DragState == EDragState::PreDrag) { // If the user didn't start dragging, handle the mouse operation as a click. ProcessClick(InMyGeometry, InMouseEvent); } else { EndDrag(InMyGeometry, InMouseEvent); } return FReply::Handled().ReleaseMouseCapture(); } return FReply::Unhandled(); } void ClampViewRangeToDataIfBound( float& NewViewMin, float& NewViewMax, const TAttribute< TOptional > & DataMin, const TAttribute< TOptional > & DataMax, const float ViewRange) { // if we have data bound const TOptional & Min = DataMin.Get(); const TOptional & Max = DataMax.Get(); if ( Min.IsSet() && NewViewMin < Min.GetValue()) { // if we have min data set NewViewMin = Min.GetValue(); NewViewMax = ViewRange; } else if ( Max.IsSet() && NewViewMax > Max.GetValue() ) { // if we have min data set NewViewMin = Max.GetValue() - ViewRange; NewViewMax = Max.GetValue(); } } FReply SCurveEditor::OnMouseMove( const FGeometry& InMyGeometry, const FPointerEvent& InMouseEvent ) { UpdateCurveToolTip(InMyGeometry, InMouseEvent); FRealCurve* Curve = GetCurve(0); if( Curve != NULL && this->HasMouseCapture()) { if (DragState == EDragState::PreDrag) { TryStartDrag(InMyGeometry, InMouseEvent); } if (DragState != EDragState::None) { ProcessDrag(InMyGeometry, InMouseEvent); } MouseMoveLocation = InMyGeometry.AbsoluteToLocal(InMouseEvent.GetScreenSpacePosition()); return FReply::Handled(); } return FReply::Unhandled(); } void SCurveEditor::UpdateCurveToolTip(const FGeometry& InMyGeometry, const FPointerEvent& InMouseEvent) { if (Settings->GetShowCurveEditorCurveToolTips()) { TSharedPtr HoveredCurve = HitTestCurves(InMyGeometry, InMouseEvent); //Display the tooltip only when the curve is visible if (HoveredCurve.IsValid() && HoveredCurve->bIsVisible) { FTrackScaleInfo ScaleInfo(ViewMinInput.Get(), ViewMaxInput.Get(), ViewMinOutput.Get(), ViewMaxOutput.Get(), InMyGeometry.GetLocalSize()); const FVector2D HitPosition = InMyGeometry.AbsoluteToLocal(InMouseEvent.GetScreenSpacePosition()); float Time = ScaleInfo.LocalXToInput(HitPosition.X); float Value = HoveredCurve->CurveInfo.CurveToEdit->Eval(Time); FNumberFormattingOptions FormattingOptions; FormattingOptions.MaximumFractionalDigits = 2; CurveToolTipNameText = FText::FromName(HoveredCurve->CurveInfo.CurveName); CurveToolTipOutputText = FText::Format(LOCTEXT("CurveToolTipValueFormat", "{0}: {1}"), OutputAxisName, FText::AsNumber(Value, &FormattingOptions)); if (ShowTimeInFrames()) { CurveToolTipInputText = FText::Format(LOCTEXT("CurveToolTipFrameFormat", "{0}: {1}"), GetInputAxisName(), FText::AsNumber(TimeToFrame(Time))); } else { CurveToolTipInputText = FText::Format(LOCTEXT("CurveToolTipTimeFormat", "{0}: {1}"), GetInputAxisName(), FText::AsNumber(Time, &FormattingOptions)); } if (CurveToolTip.IsValid() == false) { SetToolTip( SAssignNew(CurveToolTip, SToolTip) .BorderImage( FCoreStyle::Get().GetBrush( "ToolTip.BrightBackground" ) ) [ SNew(SVerticalBox) + SVerticalBox::Slot() [ SNew(STextBlock) .Text(this, &SCurveEditor::GetCurveToolTipNameText) .Font(FCoreStyle::Get().GetFontStyle("ToolTip.LargerFont")) .ColorAndOpacity( FLinearColor::Black) ] + SVerticalBox::Slot() [ SNew(STextBlock) .Text(this, &SCurveEditor::GetCurveToolTipInputText) .Font(FCoreStyle::Get().GetFontStyle("ToolTip.LargerFont")) .ColorAndOpacity(FLinearColor::Black) ] + SVerticalBox::Slot() [ SNew(STextBlock) .Text(this, &SCurveEditor::GetCurveToolTipOutputText) .Font(FCoreStyle::Get().GetFontStyle("ToolTip.LargerFont")) .ColorAndOpacity(FLinearColor::Black) ] ]); } } else { CurveToolTip.Reset(); SetToolTip(CurveToolTip); } } } FReply SCurveEditor::OnMouseWheel(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) { if (!bRequireFocusToZoom || HasKeyboardFocus()) { ZoomView(FVector2D(MouseEvent.GetWheelDelta(), MouseEvent.GetWheelDelta())); return FReply::Handled(); } return FReply::Unhandled(); } void SCurveEditor::ZoomView(FVector2D Delta) { const FVector2D ZoomDelta = -0.1f * Delta; if (bAllowZoomOutput) { const float OutputViewSize = ViewMaxOutput.Get() - ViewMinOutput.Get(); const float OutputChange = OutputViewSize * ZoomDelta.Y; const float NewMinOutput = (ViewMinOutput.Get() - (OutputChange * 0.5f)); const float NewMaxOutput = (ViewMaxOutput.Get() + (OutputChange * 0.5f)); SetOutputMinMax(NewMinOutput, NewMaxOutput); } { const float InputViewSize = ViewMaxInput.Get() - ViewMinInput.Get(); const float InputChange = InputViewSize * ZoomDelta.X; const float NewMinInput = ViewMinInput.Get() - (InputChange * 0.5f); const float NewMaxInput = ViewMaxInput.Get() + (InputChange * 0.5f); SetInputMinMax(NewMinInput, NewMaxInput); } } FReply SCurveEditor::OnKeyDown( const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent ) { if (InKeyEvent.GetKey() == EKeys::Platform_Delete && SelectedKeys.Num() != 0) { DeleteSelectedKeys(); return FReply::Handled(); } else { if( Commands->ProcessCommandBindings( InKeyEvent ) ) { return FReply::Handled(); } return FReply::Unhandled(); } } void SCurveEditor::TryStartDrag(const FGeometry& InMyGeometry, const FPointerEvent& InMouseEvent) { const bool bLeftMouseButton = InMouseEvent.IsMouseButtonDown(EKeys::LeftMouseButton); const bool bMiddleMouseButton = InMouseEvent.IsMouseButtonDown(EKeys::MiddleMouseButton); const bool bRightMouseButton = InMouseEvent.IsMouseButtonDown(EKeys::RightMouseButton); const bool bControlDown = InMouseEvent.IsControlDown(); const bool bShiftDown = InMouseEvent.IsShiftDown(); const bool bAltDown = InMouseEvent.IsAltDown(); FVector2D MousePosition = InMyGeometry.AbsoluteToLocal(InMouseEvent.GetScreenSpacePosition()); FVector2D DragVector = MousePosition - MouseDownLocation; if (DragVector.SizeSquared() >= FMath::Square(DragThreshold)) { if (bShiftDown) { if(FMath::Abs(MousePosition.X - MouseDownLocation.X) > FMath::Abs(MousePosition.Y - MouseDownLocation.Y)) { MovementAxisLock = EMovementAxisLock::AxisLock_Horizontal; } else { MovementAxisLock = EMovementAxisLock::AxisLock_Vertical; } } if (bLeftMouseButton) { // Check if we should start dragging keys. FSelectedCurveKey HitKey = HitTestKeys(InMyGeometry, InMyGeometry.LocalToAbsolute(MouseDownLocation)); if (HitKey.IsValid()) { EmptyTangentSelection(); if (!IsKeySelected(HitKey)) { if (!bControlDown) { EmptyKeySelection(); } AddToKeySelection(HitKey); } BeginDragTransaction(); DragState = EDragState::DragKey; DraggedKeyHandle = HitKey.KeyHandle; PreDragKeyLocations.Empty(); for (auto SelectedKey : SelectedKeys) { PreDragKeyLocations.Add(SelectedKey.KeyHandle, FVector2D ( SelectedKey.Curve->GetKeyTime(SelectedKey.KeyHandle), SelectedKey.Curve->GetKeyValue(SelectedKey.KeyHandle) )); } } else { // Check if we should start dragging a tangent. FSelectedTangent Tangent = HitTestCubicTangents(InMyGeometry, InMyGeometry.LocalToAbsolute(MouseDownLocation)); if (Tangent.IsValid()) { EmptyKeySelection(); if (!IsTangentSelected(Tangent)) { if (!bControlDown) { EmptyTangentSelection(); } AddToTangentSelection(Tangent); } BeginDragTransaction(); DragState = EDragState::DragTangent; PreDragTangents.Empty(); for (auto SelectedTangent : SelectedTangents) { FRichCurve* Curve = (FRichCurve*)SelectedTangent.Key.Curve; FKeyHandle KeyHandle = SelectedTangent.Key.KeyHandle; float ArriveTangent = Curve->GetKey(KeyHandle).ArriveTangent; float LeaveTangent = Curve->GetKey(KeyHandle).LeaveTangent; PreDragTangents.Add(KeyHandle, FVector2D(ArriveTangent, LeaveTangent)); } } else { // Otherwise if the user left clicked on nothing and start a marquee select. DragState = EDragState::MarqueeSelect; } } } else if (bMiddleMouseButton) { if (bAltDown) { DragState = EDragState::Pan; } else if (SelectedTangents.Num()) { BeginDragTransaction(); DragState = EDragState::DragTangent; PreDragTangents.Empty(); for (auto SelectedTangent : SelectedTangents) { FRichCurve* Curve = (FRichCurve*)SelectedTangent.Key.Curve; FKeyHandle KeyHandle = SelectedTangent.Key.KeyHandle; float ArriveTangent = Curve->GetKey(KeyHandle).ArriveTangent; float LeaveTangent = Curve->GetKey(KeyHandle).LeaveTangent; PreDragTangents.Add(KeyHandle, FVector2D(ArriveTangent, LeaveTangent)); } } else if (SelectedKeys.Num()) { BeginDragTransaction(); DragState = EDragState::FreeDrag; PreDragKeyLocations.Empty(); for (auto selectedKey : SelectedKeys) { PreDragKeyLocations.Add(selectedKey.KeyHandle, FVector2D ( selectedKey.Curve->GetKeyTime(selectedKey.KeyHandle), selectedKey.Curve->GetKeyValue(selectedKey.KeyHandle) )); } } } else if (bRightMouseButton) { if (bAltDown) { DragState = EDragState::Zoom; } else { DragState = EDragState::Pan; } } else { DragState = EDragState::None; } } } /* Given a tangent value for a key, calculates the 2D delta vector from that key in curve space */ static inline FVector2D CalcTangentDir(float Tangent) { const float Angle = FMath::Atan(Tangent); return FVector2D( FMath::Cos(Angle), -FMath::Sin(Angle) ); } /*Given a 2d delta vector in curve space, calculates a tangent value */ static inline float CalcTangent(const FVector2D& HandleDelta) { // Ensure X is positive and non-zero. // Tangent is gradient of handle. return HandleDelta.Y / FMath::Max(HandleDelta.X, KINDA_SMALL_NUMBER); } void SCurveEditor::ProcessDrag(const FGeometry& InMyGeometry, const FPointerEvent& InMouseEvent) { FTrackScaleInfo ScaleInfo(ViewMinInput.Get(), ViewMaxInput.Get(), ViewMinOutput.Get(), ViewMaxOutput.Get(), InMyGeometry.GetLocalSize()); FVector2D ScreenDelta = InMouseEvent.GetCursorDelta(); FVector2D InputDelta; InputDelta.X = ScreenDelta.X / ScaleInfo.PixelsPerInput; InputDelta.Y = -ScreenDelta.Y / ScaleInfo.PixelsPerOutput; if (DragState == EDragState::DragKey) { FVector2D MousePosition = InMyGeometry.AbsoluteToLocal(InMouseEvent.GetScreenSpacePosition()); FVector2D NewLocation = FVector2D(ScaleInfo.LocalXToInput(MousePosition.X), ScaleInfo.LocalYToOutput(MousePosition.Y)); FVector2D SnappedNewLocation = SnapLocation(NewLocation); FVector2D Delta = SnappedNewLocation - PreDragKeyLocations[DraggedKeyHandle]; MoveSelectedKeys(Delta); } else if (DragState == EDragState::FreeDrag) { FVector2D MousePosition = InMyGeometry.AbsoluteToLocal(InMouseEvent.GetScreenSpacePosition()); FVector2D NewLocation = FVector2D(ScaleInfo.LocalXToInput(MousePosition.X), ScaleInfo.LocalYToOutput(MousePosition.Y)); FVector2D Delta = NewLocation - FVector2D(ScaleInfo.LocalXToInput(MouseDownLocation.X), ScaleInfo.LocalYToOutput(MouseDownLocation.Y)); MoveSelectedKeys(Delta); } else if (DragState == EDragState::DragTangent) { FVector2D MousePositionScreen = InMyGeometry.AbsoluteToLocal(InMouseEvent.GetScreenSpacePosition()); FVector2D MouseDownPositionScreen = MouseDownLocation; MoveTangents(ScaleInfo, MousePositionScreen - MouseDownPositionScreen); } else if (DragState == EDragState::Pan) { if (MovementAxisLock == EMovementAxisLock::AxisLock_Horizontal) { InputDelta.Y = 0; } else if (MovementAxisLock == EMovementAxisLock::AxisLock_Vertical) { InputDelta.X = 0; } // Output is not clamped. const float NewMinOutput = (ViewMinOutput.Get() - InputDelta.Y); const float NewMaxOutput = (ViewMaxOutput.Get() - InputDelta.Y); SetOutputMinMax(NewMinOutput, NewMaxOutput); // Input maybe clamped if DataMinInput or DataMaxOutput was set. float NewMinInput = ViewMinInput.Get() - InputDelta.X; float NewMaxInput = ViewMaxInput.Get() - InputDelta.X; ClampViewRangeToDataIfBound(NewMinInput, NewMaxInput, DataMinInput, DataMaxInput, ScaleInfo.ViewInputRange); SetInputMinMax(NewMinInput, NewMaxInput); } else if (DragState == EDragState::Zoom) { FVector2D Delta = FVector2D(ScreenDelta.X * 0.05f, ScreenDelta.X * 0.05f); if (MovementAxisLock == EMovementAxisLock::AxisLock_Horizontal) { Delta.Y = 0; } else if (MovementAxisLock == EMovementAxisLock::AxisLock_Vertical) { Delta.X = 0; Delta.Y = -ScreenDelta.Y * 0.1f; } ZoomView(Delta); } } void SCurveEditor::EndDrag(const FGeometry& InMyGeometry, const FPointerEvent& InMouseEvent) { const bool bControlDown = InMouseEvent.IsControlDown(); const bool bShiftDown = InMouseEvent.IsShiftDown(); if (DragState == EDragState::DragKey || DragState == EDragState::FreeDrag || DragState == EDragState::DragTangent) { EndDragTransaction(); } else if (DragState == EDragState::MarqueeSelect) { FVector2D MarqueTopLeft ( FMath::Min(MouseDownLocation.X, MouseMoveLocation.X), FMath::Min(MouseDownLocation.Y, MouseMoveLocation.Y) ); FVector2D MarqueBottomRight ( FMath::Max(MouseDownLocation.X, MouseMoveLocation.X), FMath::Max(MouseDownLocation.Y, MouseMoveLocation.Y) ); TArray SelectedCurveTangents = GetEditableTangentsWithinMarquee(InMyGeometry, MarqueTopLeft, MarqueBottomRight); TArray SelectedCurveKeys = GetEditableKeysWithinMarquee(InMyGeometry, MarqueTopLeft, MarqueBottomRight); if (!bControlDown && !bShiftDown) { EmptyAllSelection(); } if (SelectedCurveKeys.Num()) { EmptyTangentSelection(); for (auto SelectedCurveKey : SelectedCurveKeys) { if (IsKeySelected(SelectedCurveKey)) { RemoveFromKeySelection(SelectedCurveKey); } else { AddToKeySelection(SelectedCurveKey); } } } if (!SelectedCurveKeys.Num()) { EmptyKeySelection(); for (auto SelectedCurveTangent : SelectedCurveTangents) { if (IsTangentSelected(SelectedCurveTangent)) { RemoveFromTangentSelection(SelectedCurveTangent); } else { AddToTangentSelection(SelectedCurveTangent); } } } } DragState = EDragState::None; MovementAxisLock = EMovementAxisLock::None; } void SCurveEditor::ProcessClick(const FGeometry& InMyGeometry, const FPointerEvent& InMouseEvent) { const bool bLeftMouseButton = InMouseEvent.GetEffectingButton() == EKeys::LeftMouseButton; const bool bRightMouseButton = InMouseEvent.GetEffectingButton() == EKeys::RightMouseButton; const bool bControlDown = InMouseEvent.IsControlDown(); const bool bShiftDown = InMouseEvent.IsShiftDown(); FSelectedCurveKey HitKey = HitTestKeys(InMyGeometry, InMouseEvent.GetScreenSpacePosition()); FSelectedTangent HitTangent = HitTestCubicTangents(InMyGeometry, InMouseEvent.GetScreenSpacePosition()); if (bLeftMouseButton) { // If the user left clicked a key, update selection based on modifier key state. if (HitKey.IsValid()) { EmptyTangentSelection(); if (!IsKeySelected(HitKey)) { if (!bControlDown && !bShiftDown) { EmptyAllSelection(); } AddToKeySelection(HitKey); } else if (bControlDown) { RemoveFromKeySelection(HitKey); } } else if (HitTangent.IsValid()) { EmptyKeySelection(); if (!IsTangentSelected(HitTangent)) { if (!bControlDown && !bShiftDown) { EmptyAllSelection(); } AddToTangentSelection(HitTangent); } else if (bControlDown) { RemoveFromTangentSelection(HitTangent); } } else { // If the user didn't click a key, add a new one if shift is held down, or try to select a curve. if (bShiftDown && IsEditingEnabled()) { TSharedPtr>> CurvesToAddKeysTo = MakeShareable(new TArray>()); TSharedPtr HoveredCurve = HitTestCurves(InMyGeometry, InMouseEvent); bool bAddKeysInline; //To snap a point on the hovered curve the curve must be visible and unlock if (HoveredCurve.IsValid() && !HoveredCurve->bIsLocked && HoveredCurve->bIsVisible) { CurvesToAddKeysTo->Add(HoveredCurve); bAddKeysInline = true; } else { //Add all unlock curves in the editable array for (auto CurveViewModel : CurveViewModels) { if (!CurveViewModel->bIsLocked) { CurvesToAddKeysTo->Add(CurveViewModel); } } //If linear color curve and no show curve, always insert inline //If the user is holding shift-ctrl we snap all curve to the mouse position. (false value) //If the user is holding shift we snap to mouse only if there is only one editable curve. (false value) //In all other case we add key directly on the curve. (true value) bAddKeysInline = (IsLinearColorCurve() && !bAreCurvesVisible.Get()) || ((!bControlDown) && (CurvesToAddKeysTo->Num() != 1)); } AddNewKey(InMyGeometry, InMouseEvent.GetScreenSpacePosition(), CurvesToAddKeysTo, bAddKeysInline); } else { // clicking on background clears all selection EmptyAllSelection(); } } } else if (bRightMouseButton) { // If the user right clicked, handle opening context menus. if (HitKey.IsValid()) { // Make sure key is selected in readiness for context menu EmptyTangentSelection(); if (!IsKeySelected(HitKey)) { EmptyAllSelection(); AddToKeySelection(HitKey); } PushKeyMenu(InMyGeometry, InMouseEvent); } else if (HitTangent.IsValid()) { // Make sure key is selected in readiness for context menu EmptyKeySelection(); if (!IsTangentSelected(HitTangent)) { EmptyAllSelection(); AddToTangentSelection(HitTangent); } PushKeyMenu(InMyGeometry, InMouseEvent); } else { CreateContextMenu(InMyGeometry, InMouseEvent); } } } TOptional SCurveEditor::OnGetTime() const { TOptional Time; // Return the time if all selected keys have the same time, otherwise return an unset value if (SelectedKeys.Num() > 0) { Time = GetKeyTime(SelectedKeys[0]); for (int32 i = 1; i < SelectedKeys.Num(); i++) { TOptional NewTime = GetKeyTime(SelectedKeys[i]); bool bAreEqual = ((!Time.IsSet() && !NewTime.IsSet()) || (Time.IsSet() && NewTime.IsSet() && Time.GetValue() == NewTime.GetValue())); if (!bAreEqual) { return TOptional(); } } } return Time; } void SCurveEditor::OnTimeComitted(float NewTime, ETextCommit::Type CommitType) { // Don't digest the number if we just clicked away from the pop-up if ( !bIsUsingSlider && ((CommitType == ETextCommit::OnEnter) || ( CommitType == ETextCommit::OnUserMovedFocus )) ) { for(FSelectedCurveKey Key : SelectedKeys) { UpdateCurveTimeSingleKey(Key, NewTime); } FSlateApplication::Get().DismissAllMenus(); } } void SCurveEditor::OnTimeChanged(float NewTime) { if ( bIsUsingSlider ) { for (FSelectedCurveKey Key : SelectedKeys) { UpdateCurveTimeSingleKey(Key, NewTime); } } } TOptional SCurveEditor::OnGetTimeInFrames() const { TOptional Time; // Return the time in frames if all selected keys have the same time, otherwise return an unset value if (SelectedKeys.Num() > 0) { Time = GetKeyTime(SelectedKeys[0]); for (int32 i = 1; i < SelectedKeys.Num(); i++) { TOptional NewTime = GetKeyTime(SelectedKeys[i]); bool bAreEqual = ((!Time.IsSet() && !NewTime.IsSet()) || (Time.IsSet() && NewTime.IsSet() && Time.GetValue() == NewTime.GetValue())); if (!bAreEqual) { return TOptional(); } } } if (Time.IsSet()) { return TOptional(TimeToFrame(Time.GetValue())); } return TOptional(); } void SCurveEditor::OnTimeInFramesComitted(int32 NewFrame, ETextCommit::Type CommitType) { // Don't digest the number if we just clicked away from the pop-up if ( !bIsUsingSlider && ((CommitType == ETextCommit::OnEnter) || ( CommitType == ETextCommit::OnUserMovedFocus )) ) { for (FSelectedCurveKey Key : SelectedKeys) { UpdateCurveTimeSingleKey(Key, NewFrame); } FSlateApplication::Get().DismissAllMenus(); } } void SCurveEditor::OnTimeInFramesChanged(int32 NewFrame) { if ( bIsUsingSlider ) { for (FSelectedCurveKey Key : SelectedKeys) { UpdateCurveTimeSingleKey(Key, NewFrame); } } } void SCurveEditor::UpdateCurveTimeSingleKey(FSelectedCurveKey Key, float NewTime, bool bSetFromFrame) { // If the curve is valid and doesn't already have a key at that time if (IsValidCurve(Key.Curve)) { if (!Key.Curve->KeyExistsAtTime(NewTime)) { FText TransactionText = bSetFromFrame ? LOCTEXT("CurveEditor_NewFrame", "New Frame Entered") : LOCTEXT("CurveEditor_NewTime", "New Time Entered"); const FScopedTransaction Transaction(TransactionText); CurveOwner->ModifyOwner(); Key.Curve->SetKeyTime(Key.KeyHandle, NewTime); TArray ChangedCurveEditInfos; ChangedCurveEditInfos.Add(GetViewModelForCurve(Key.Curve)->CurveInfo); CurveOwner->OnCurveChanged(ChangedCurveEditInfos); } // if the existing key is not this key else if (Key.Curve->FindKey(NewTime) != Key.KeyHandle) { LogAndToastCurveTimeWarning(Key.Curve); } } } void SCurveEditor::UpdateCurveTimeSingleKey(FSelectedCurveKey Key, int32 NewFrame) { UpdateCurveTimeSingleKey(Key, FrameToTime(NewFrame), true); } void SCurveEditor::LogAndToastCurveTimeWarning(FRealCurve* Curve) { FText Error = FText::Format(LOCTEXT("KeyTimeCollision","A key on {0} could not be moved because there was already a key at that time."), FText::FromName(GetViewModelForCurve(Curve)->CurveInfo.CurveName)); FNotificationInfo Info(Error); Info.ExpireDuration = 5.0f; FSlateNotificationManager::Get().AddNotification(Info); UE_LOG(LogCurveEditor, Warning, TEXT("%s"), *Error.ToString()); } TOptional SCurveEditor::OnGetValue() const { TOptional Value; // Return the value string if all selected keys have the same output string, otherwise empty if ( SelectedKeys.Num() > 0 ) { Value = GetKeyValue(SelectedKeys[0]); for ( int32 i=1; i < SelectedKeys.Num(); i++ ) { TOptional NewValue = GetKeyValue(SelectedKeys[i]); bool bAreEqual = ( ( !Value.IsSet() && !NewValue.IsSet() ) || ( Value.IsSet() && NewValue.IsSet() && Value.GetValue() == NewValue.GetValue() ) ); if ( !bAreEqual ) { return TOptional(); } } } return Value; } void SCurveEditor::OnValueComitted(float NewValue, ETextCommit::Type CommitType) { // Don't digest the number if we just clicked away from the popup if ( !bIsUsingSlider && ((CommitType == ETextCommit::OnEnter) || ( CommitType == ETextCommit::OnUserMovedFocus )) ) { const FScopedTransaction Transaction( LOCTEXT( "CurveEditor_NewValue", "New Value Entered" ) ); CurveOwner->ModifyOwner(); TSet ChangedCurves; // Iterate over selected set for ( int32 i=0; i < SelectedKeys.Num(); i++ ) { auto Key = SelectedKeys[i]; if ( IsValidCurve(Key.Curve) ) { // Fill in each element of this key Key.Curve->SetKeyValue(Key.KeyHandle, NewValue); ChangedCurves.Add(Key.Curve); } } TArray ChangedCurveEditInfos; for (auto CurveViewModel : CurveViewModels) { if (ChangedCurves.Contains(CurveViewModel->CurveInfo.CurveToEdit)) { ChangedCurveEditInfos.Add(CurveViewModel->CurveInfo); } } CurveOwner->OnCurveChanged(ChangedCurveEditInfos); FSlateApplication::Get().DismissAllMenus(); } } void SCurveEditor::OnValueChanged(float NewValue) { if ( bIsUsingSlider ) { const FScopedTransaction Transaction(LOCTEXT("CurveEditor_NewValue", "New Value Entered")); TSet ChangedCurves; // Iterate over selected set for ( int32 i=0; i < SelectedKeys.Num(); i++ ) { auto Key = SelectedKeys[i]; if ( IsValidCurve(Key.Curve) ) { CurveOwner->ModifyOwner(); // Fill in each element of this key Key.Curve->SetKeyValue(Key.KeyHandle, NewValue); ChangedCurves.Add(Key.Curve); } } TArray ChangedCurveEditInfos; for (auto CurveViewModel : CurveViewModels) { if (ChangedCurves.Contains(CurveViewModel->CurveInfo.CurveToEdit)) { ChangedCurveEditInfos.Add(CurveViewModel->CurveInfo); } } CurveOwner->OnCurveChanged(ChangedCurveEditInfos); } } void SCurveEditor::OnBeginSliderMovement(FText TransactionName) { bIsUsingSlider = true; GEditor->BeginTransaction(TransactionName); } void SCurveEditor::OnEndSliderMovement(float NewValue) { bIsUsingSlider = false; GEditor->EndTransaction(); } void SCurveEditor::OnEndSliderMovement(int32 NewValue) { bIsUsingSlider = false; GEditor->EndTransaction(); } SCurveEditor::FSelectedCurveKey SCurveEditor::HitTestKeys(const FGeometry& InMyGeometry, const FVector2D& HitScreenPosition) { FSelectedCurveKey SelectedKey(NULL,FKeyHandle()); if( AreCurvesVisible() ) { bool bAnyCurveViewModelsSelected = AnyCurveViewModelsSelected(); FTrackScaleInfo ScaleInfo(ViewMinInput.Get(), ViewMaxInput.Get(), ViewMinOutput.Get(), ViewMaxOutput.Get(), InMyGeometry.GetLocalSize()); const FVector2D HitPosition = InMyGeometry.AbsoluteToLocal( HitScreenPosition ); for(auto CurveViewModel : CurveViewModels) { if (IsCurveSelectable(CurveViewModel)) { FRealCurve* Curve = CurveViewModel->CurveInfo.CurveToEdit; if(Curve != NULL) { for (auto It(Curve->GetKeyHandleIterator()); It; ++It) { float KeyScreenX = ScaleInfo.InputToLocalX(Curve->GetKeyTime(*It)); float KeyScreenY = ScaleInfo.OutputToLocalY(Curve->GetKeyValue(*It)); if( HitPosition.X > (KeyScreenX - (0.5f * CONST_KeySize.X)) && HitPosition.X < (KeyScreenX + (0.5f * CONST_KeySize.X)) && HitPosition.Y > (KeyScreenY - (0.5f * CONST_KeySize.Y)) && HitPosition.Y < (KeyScreenY + (0.5f * CONST_KeySize.Y)) ) { return FSelectedCurveKey(Curve, *It); } } } } } } return SelectedKey; } void SCurveEditor::MoveSelectedKeys(FVector2D Delta) { TArray ChangedCurveEditInfos; const FScopedTransaction Transaction( LOCTEXT("CurveEditor_MoveKeys", "Move Keys") ); CurveOwner->ModifyOwnerChange(); // track all unique curves encountered so their tangents can be updated later TSet UniqueCurves; // The total move distance for all keys is the difference between the current snapped location // and the start location of the key which was actually dragged. FVector2D TotalMoveDistance = Delta; for (int32 i = 0; i < SelectedKeys.Num(); i++) { FSelectedCurveKey OldKey = SelectedKeys[i]; if (!IsValidCurve(OldKey.Curve)) { continue; } FKeyHandle OldKeyHandle = OldKey.KeyHandle; FRealCurve* Curve = OldKey.Curve; FVector2D PreDragLocation = PreDragKeyLocations[OldKeyHandle]; FVector2D NewLocation = PreDragLocation + TotalMoveDistance; // Update the key's value without updating the tangents. if (MovementAxisLock != EMovementAxisLock::AxisLock_Horizontal) { Curve->SetKeyValue(OldKeyHandle, NewLocation.Y, false); } // Changing the time of a key returns a new handle, so make sure to update existing references. if (MovementAxisLock != EMovementAxisLock::AxisLock_Vertical) { Curve->SetKeyTime(OldKeyHandle, NewLocation.X); SelectedKeys[i] = FSelectedCurveKey(Curve, OldKeyHandle); PreDragKeyLocations.Remove(OldKeyHandle); PreDragKeyLocations.Add(OldKeyHandle, PreDragLocation); } UniqueCurves.Add(Curve); ChangedCurveEditInfos.Add(GetViewModelForCurve(Curve)->CurveInfo); } if (CurveOwner->HasRichCurves()) { // update auto tangents for all curves encountered, once each only for(TSet::TIterator SetIt(UniqueCurves);SetIt;++SetIt) { ((FRichCurve*)(*SetIt))->AutoSetTangents(); } } if (ChangedCurveEditInfos.Num()) { CurveOwner->OnCurveChanged(ChangedCurveEditInfos); } } TOptional SCurveEditor::GetKeyValue(FSelectedCurveKey Key) const { if(IsValidCurve(Key.Curve)) { return Key.Curve->GetKeyValue(Key.KeyHandle); } return TOptional(); } TOptional SCurveEditor::GetKeyTime(FSelectedCurveKey Key) const { if ( IsValidCurve(Key.Curve) ) { return Key.Curve->GetKeyTime(Key.KeyHandle); } return TOptional(); } void SCurveEditor::EmptyKeySelection() { SelectedKeys.Empty(); } void SCurveEditor::AddToKeySelection(FSelectedCurveKey Key) { SelectedKeys.AddUnique(Key); } void SCurveEditor::RemoveFromKeySelection(FSelectedCurveKey Key) { SelectedKeys.Remove(Key); } bool SCurveEditor::IsKeySelected(FSelectedCurveKey Key) const { return SelectedKeys.Contains(Key); } bool SCurveEditor::AreKeysSelected() const { return SelectedKeys.Num() > 0; } void SCurveEditor::EmptyTangentSelection() { SelectedTangents.Empty(); } void SCurveEditor::AddToTangentSelection(FSelectedTangent Tangent) { SelectedTangents.Add(Tangent); } void SCurveEditor::RemoveFromTangentSelection(FSelectedTangent Tangent) { SelectedTangents.Remove(Tangent); } bool SCurveEditor::IsTangentSelected(FSelectedTangent Tangent) const { return SelectedTangents.Contains(Tangent); } bool SCurveEditor::AreTangentsSelected() const { return SelectedTangents.Num() > 0; } bool SCurveEditor::IsTangentVisible(FRichCurve* Curve, FKeyHandle KeyHandle, bool& bIsTangentSelected, bool& bIsArrivalSelected, bool& bIsLeaveSelected) const { bIsTangentSelected = false; bIsArrivalSelected = false; bIsLeaveSelected = false; bool IsSelected = IsKeySelected(FSelectedCurveKey(Curve,KeyHandle)); for (auto SelectedTangent : SelectedTangents) { if (SelectedTangent.Key.KeyHandle == KeyHandle) { if (SelectedTangent.bIsArrival) bIsArrivalSelected = true; else bIsLeaveSelected = true; bIsTangentSelected = true; } } return (IsSelected || bIsTangentSelected || Settings->GetTangentVisibility() == ECurveEditorTangentVisibility::AllTangents) && Settings->GetTangentVisibility() != ECurveEditorTangentVisibility::NoTangents; } void SCurveEditor::EmptyAllSelection() { EmptyKeySelection(); EmptyTangentSelection(); } void SCurveEditor::ValidateSelection() { //remove all selection if we no longer have a curve interface (Curve Owner) if (!CurveOwner) { EmptyAllSelection(); return; } //remove any invalid keys for(int32 i = 0;iGetAutoFrameCurveEditor() && GetAllowAutoFrame(); } TArray SCurveEditor::GetCurvesToFit() const { TArray FitCurves; for(const TSharedPtr& CurveViewModel : CurveViewModels) { if (CurveViewModel->bIsVisible && CurveViewModel->CurveInfo.CurveToEdit != nullptr) { FitCurves.Add(CurveViewModel->CurveInfo.CurveToEdit); } } return FitCurves; } void SCurveEditor::ZoomToFitHorizontal(const bool bZoomToFitAll) { TArray CurvesToFit = GetCurvesToFit(); if(CurveViewModels.Num() > 0) { float InMin = FLT_MAX; float InMax = -FLT_MAX; int32 TotalKeys = 0; if (SelectedKeys.Num() && !bZoomToFitAll) { for (auto SelectedKey : SelectedKeys) { TotalKeys++; float KeyTime = SelectedKey.Curve->GetKeyTime(SelectedKey.KeyHandle); InMin = FMath::Min(KeyTime, InMin); InMax = FMath::Max(KeyTime, InMax); FKeyHandle NextKeyHandle = SelectedKey.Curve->GetNextKey(SelectedKey.KeyHandle); if (SelectedKey.Curve->IsKeyHandleValid(NextKeyHandle)) { float NextKeyTime = SelectedKey.Curve->GetKeyTime(NextKeyHandle); InMin = FMath::Min(NextKeyTime, InMin); InMax = FMath::Max(NextKeyTime, InMax); } FKeyHandle PreviousKeyHandle = SelectedKey.Curve->GetPreviousKey(SelectedKey.KeyHandle); if (SelectedKey.Curve->IsKeyHandleValid(PreviousKeyHandle)) { float PreviousKeyTime = SelectedKey.Curve->GetKeyTime(PreviousKeyHandle); InMin = FMath::Min(PreviousKeyTime, InMin); InMax = FMath::Max(PreviousKeyTime, InMax); } } } else { for (FRealCurve* Curve : CurvesToFit) { float MinTime, MaxTime; Curve->GetTimeRange(MinTime, MaxTime); InMin = FMath::Min(MinTime, InMin); InMax = FMath::Max(MaxTime, InMax); TotalKeys += Curve->GetNumKeys(); } } if (TotalKeys > 0) { // Clamp the minimum size float Size = InMax - InMin; if (Size < CONST_MinViewRange) { InMin -= (0.5f*CONST_MinViewRange); InMax += (0.5f*CONST_MinViewRange); Size = InMax - InMin; } // add margin InMin -= CONST_FitMargin*Size; InMax += CONST_FitMargin*Size; } else { InMin = -CONST_FitMargin*2.0f; InMax = (CONST_DefaultZoomRange + CONST_FitMargin)*2.0; } SetInputMinMax(InMin, InMax); } } FReply SCurveEditor::ZoomToFitHorizontalClicked() { ZoomToFitHorizontal(); return FReply::Handled(); } /** Set Default output values when range is too small **/ void SCurveEditor::SetDefaultOutput(const float MinZoomRange) { const float NewMinOutput = (ViewMinOutput.Get() - (0.5f*MinZoomRange)); const float NewMaxOutput = (ViewMaxOutput.Get() + (0.5f*MinZoomRange)); SetOutputMinMax(NewMinOutput, NewMaxOutput); } void SCurveEditor::ZoomToFitVertical(const bool bZoomToFitAll) { TArray CurvesToFit = GetCurvesToFit(); if(CurvesToFit.Num() > 0) { float InMin = FLT_MAX; float InMax = -FLT_MAX; int32 TotalKeys = 0; if (SelectedKeys.Num() != 0 && !bZoomToFitAll) { for (auto SelectedKey : SelectedKeys) { TotalKeys++; float KeyValue = SelectedKey.Curve->GetKeyValue(SelectedKey.KeyHandle); InMin = FMath::Min(KeyValue, InMin); InMax = FMath::Max(KeyValue, InMax); FKeyHandle NextKeyHandle = SelectedKey.Curve->GetNextKey(SelectedKey.KeyHandle); if (SelectedKey.Curve->IsKeyHandleValid(NextKeyHandle)) { float NextKeyValue = SelectedKey.Curve->GetKeyValue(NextKeyHandle); InMin = FMath::Min(NextKeyValue, InMin); InMax = FMath::Max(NextKeyValue, InMax); } FKeyHandle PreviousKeyHandle = SelectedKey.Curve->GetPreviousKey(SelectedKey.KeyHandle); if (SelectedKey.Curve->IsKeyHandleValid(PreviousKeyHandle)) { float PreviousKeyValue = SelectedKey.Curve->GetKeyValue(PreviousKeyHandle); InMin = FMath::Min(PreviousKeyValue, InMin); InMax = FMath::Max(PreviousKeyValue, InMax); } } } else { for (FRealCurve* Curve : CurvesToFit) { float MinVal, MaxVal; Curve->GetValueRange(MinVal, MaxVal); InMin = FMath::Min(MinVal, InMin); InMax = FMath::Max(MaxVal, InMax); TotalKeys += Curve->GetNumKeys(); } } const float MinZoomRange = (TotalKeys > 0 ) ? CONST_MinViewRange: CONST_DefaultZoomRange; // if in max and in min is same, then include 0.f if (InMax == InMin) { InMax = FMath::Max(InMax, 0.f); InMin = FMath::Min(InMin, 0.f); } // Clamp the minimum size float Size = InMax - InMin; if( Size < MinZoomRange ) { SetDefaultOutput(MinZoomRange); InMin = ViewMinOutput.Get(); InMax = ViewMaxOutput.Get(); Size = InMax - InMin; } // add margin const float NewMinOutput = (InMin - CONST_FitMargin*Size); const float NewMaxOutput = (InMax + CONST_FitMargin*Size); SetOutputMinMax(NewMinOutput, NewMaxOutput); } } FReply SCurveEditor::ZoomToFitVerticalClicked() { ZoomToFitVertical(); return FReply::Handled(); } void SCurveEditor::ZoomToFit(const bool bZoomToFitAll) { ZoomToFitHorizontal(bZoomToFitAll); ZoomToFitVertical(bZoomToFitAll); } void SCurveEditor::ToggleInputSnapping() { if (bInputSnappingEnabled.IsBound() == false) { bInputSnappingEnabled = !bInputSnappingEnabled.Get(); } } void SCurveEditor::ToggleOutputSnapping() { if (bOutputSnappingEnabled.IsBound() == false) { bOutputSnappingEnabled = !bOutputSnappingEnabled.Get(); } } bool SCurveEditor::IsInputSnappingEnabled() const { return bInputSnappingEnabled.Get(); } bool SCurveEditor::IsOutputSnappingEnabled() const { return bOutputSnappingEnabled.Get(); } bool SCurveEditor::ShowTimeInFrames() const { return bShowTimeInFrames.Get(); } void SCurveEditor::CreateContextMenu(const FGeometry& InMyGeometry, const FPointerEvent& InMouseEvent) { TSharedPtr>> CurvesToAddKeysTo = MakeShareable(new TArray>()); bool bHoveredCurveValid = false; TSharedPtr HoveredCurve = HitTestCurves(InMyGeometry, InMouseEvent); //Curve must be visible and unlocked to show context menu if (HoveredCurve.IsValid() && !HoveredCurve->bIsLocked && HoveredCurve->bIsVisible) { CurvesToAddKeysTo->Add(HoveredCurve); bHoveredCurveValid = true; } else { // Get all editable curves for (auto CurveViewModel : CurveViewModels) { if (!CurveViewModel->bIsLocked) { CurvesToAddKeysTo->Add(CurveViewModel); } } } const bool bCreateExternalCurve = OnCreateAsset.IsBound() && IsEditingEnabled(); const bool bShowLinearColorCurve = IsLinearColorCurve(); // Early out if there's no menu items to show if (CurvesToAddKeysTo->Num() == 0 && !bCreateExternalCurve && !bShowLinearColorCurve) { return; } const FVector2D& ScreenPosition = InMouseEvent.GetScreenSpacePosition(); const bool CloseAfterSelection = true; FMenuBuilder MenuBuilder( CloseAfterSelection, NULL ); MenuBuilder.BeginSection("EditCurveEditorActions", LOCTEXT("Actions", "Actions")); { FText MenuItemLabel; FText MenuItemToolTip; FText AddKeyToCurveLabelFormat = LOCTEXT("AddKeyToCurveLabelFormat", "Add key to {0}"); FText AddKeyToCurveToolTipFormat = LOCTEXT("AddKeyToCurveToolTipFormat", "Add a new key at the hovered time to the {0} curve. Keys can also be added with Shift + Click."); FVector2D Position = InMouseEvent.GetScreenSpacePosition(); if (bHoveredCurveValid) { MenuItemLabel = FText::Format(AddKeyToCurveLabelFormat, FText::FromName(HoveredCurve->CurveInfo.CurveName)); MenuItemToolTip = FText::Format(AddKeyToCurveToolTipFormat, FText::FromName(HoveredCurve->CurveInfo.CurveName)); FUIAction Action = FUIAction(FExecuteAction::CreateSP(this, &SCurveEditor::AddNewKey, InMyGeometry, Position, CurvesToAddKeysTo, true)); MenuBuilder.AddMenuEntry(MenuItemLabel, MenuItemToolTip, FSlateIcon(), Action); } else { if (CurvesToAddKeysTo->Num() == 1) { MenuItemLabel = FText::Format(AddKeyToCurveLabelFormat, FText::FromName((*CurvesToAddKeysTo)[0]->CurveInfo.CurveName)); MenuItemToolTip = FText::Format(AddKeyToCurveToolTipFormat, FText::FromName((*CurvesToAddKeysTo)[0]->CurveInfo.CurveName)); FUIAction Action = FUIAction(FExecuteAction::CreateSP(this, &SCurveEditor::AddNewKey, InMyGeometry, Position, CurvesToAddKeysTo, false)); MenuBuilder.AddMenuEntry(MenuItemLabel, MenuItemToolTip, FSlateIcon(), Action); } else if (CurvesToAddKeysTo->Num() > 1) //Dont show the menu if we cannot edit any curve { //add key to all curve menu entry MenuItemLabel = LOCTEXT("AddKeyToAllCurves", "Add key to all curves"); MenuItemToolTip = LOCTEXT("AddKeyToAllCurveToolTip", "Adds a key at the hovered time to all curves. Keys can also be added with Shift + Click."); FUIAction Action = FUIAction(FExecuteAction::CreateSP(this, &SCurveEditor::AddNewKey, InMyGeometry, Position, CurvesToAddKeysTo, true)); MenuBuilder.AddMenuEntry(MenuItemLabel, MenuItemToolTip, FSlateIcon(), Action); //This menu is not required when there is no curve display (color track can hide and show curves) if (bAreCurvesVisible.Get()) { //add key and value to all curve menu entry MenuItemLabel = LOCTEXT("AddKeyValueToAllCurves", "Add key & value to all curves"); MenuItemToolTip = LOCTEXT("AddKeyValueToAllCurveToolTip", "Adds a key & value at the hovered time to all curves. Keys can also be added with Shift + ctrl + Click."); Action = FUIAction(FExecuteAction::CreateSP(this, &SCurveEditor::AddNewKey, InMyGeometry, Position, CurvesToAddKeysTo, false)); MenuBuilder.AddMenuEntry(MenuItemLabel, MenuItemToolTip, FSlateIcon(), Action); } } } } MenuBuilder.EndSection(); MenuBuilder.BeginSection("CurveEditorActions", LOCTEXT("CurveAction", "Curve Actions") ); { if( bCreateExternalCurve ) { FUIAction Action = FUIAction( FExecuteAction::CreateSP( this, &SCurveEditor::OnCreateExternalCurveClicked ) ); MenuBuilder.AddMenuEntry ( LOCTEXT("CreateExternalCurve", "Create External Curve"), LOCTEXT("CreateExternalCurve_ToolTip", "Create an external asset using this internal curve"), FSlateIcon(), Action ); } if( IsLinearColorCurve() && !bAlwaysDisplayColorCurves ) { FUIAction ShowCurveAction( FExecuteAction::CreateSP( this, &SCurveEditor::OnShowCurveToggled ), FCanExecuteAction(), FIsActionChecked::CreateSP( this, &SCurveEditor::AreCurvesVisible ) ); MenuBuilder.AddMenuEntry ( LOCTEXT("ShowCurves","Show Curves"), LOCTEXT("ShowCurves_ToolTip", "Toggles displaying the curves for linear colors"), FSlateIcon(), ShowCurveAction, NAME_None, EUserInterfaceActionType::ToggleButton ); } if( IsLinearColorCurve() && !bAlwaysHideGradientEditor) { FUIAction ShowGradientAction( FExecuteAction::CreateSP( this, &SCurveEditor::OnShowGradientToggled ), FCanExecuteAction(), FIsActionChecked::CreateSP( this, &SCurveEditor::IsGradientEditorVisible ) ); MenuBuilder.AddMenuEntry ( LOCTEXT("ShowGradient","Show Gradient"), LOCTEXT("ShowGradient_ToolTip", "Toggles displaying the gradient for linear colors"), FSlateIcon(), ShowGradientAction, NAME_None, EUserInterfaceActionType::ToggleButton ); } } MenuBuilder.EndSection(); FWidgetPath WidgetPath = InMouseEvent.GetEventPath() != nullptr ? *InMouseEvent.GetEventPath() : FWidgetPath(); FSlateApplication::Get().PushMenu(SharedThis(this), WidgetPath, MenuBuilder.MakeWidget(), FSlateApplication::Get().GetCursorPos(), FPopupTransitionEffect(FPopupTransitionEffect::ContextMenu)); } void SCurveEditor::OnCreateExternalCurveClicked() { OnCreateAsset.ExecuteIfBound(); } void SCurveEditor::OnShowCurveToggled() { if (bAreCurvesVisible.IsBound() && SetAreCurvesVisibleHandler.IsBound()) { SetAreCurvesVisibleHandler.Execute(!bAreCurvesVisible.Get()); } else { bAreCurvesVisible = !bAreCurvesVisible.Get(); } } UObject* SCurveEditor::CreateCurveObject( TSubclassOf CurveType, UObject* PackagePtr, FName& AssetName ) { UObject* NewObj = NULL; CurveFactory = Cast(NewObject(GetTransientPackage(), UCurveFactory::StaticClass())); if(CurveFactory) { CurveFactory->CurveClass = CurveType; NewObj = CurveFactory->FactoryCreateNew( CurveFactory->GetSupportedClass(), PackagePtr, AssetName, RF_Public|RF_Standalone, NULL, GWarn ); } CurveFactory = NULL; return NewObj; } bool SCurveEditor::IsEditingEnabled() const { return bCanEditTrack; } void SCurveEditor::AddReferencedObjects( FReferenceCollector& Collector ) { Collector.AddReferencedObject( Settings ); Collector.AddReferencedObject( CurveFactory ); } TSharedPtr SCurveEditor::GetCommands() { return Commands; } bool SCurveEditor::IsValidCurve( FRealCurve* Curve ) const { bool bIsValid = false; if(Curve && CurveOwner) { for(auto CurveViewModel : CurveViewModels) { if(CurveViewModel->CurveInfo.CurveToEdit == Curve && CurveOwner->IsValidCurve(CurveViewModel->CurveInfo)) { bIsValid = true; break; } } } return bIsValid; } void SCurveEditor::SetInputMinMax(float NewMin, float NewMax) { if (SetInputViewRangeHandler.IsBound()) { SetInputViewRangeHandler.Execute(NewMin, NewMax); } else { //if no delegate and view min input isn't using a delegate just set value directly if (ViewMinInput.IsBound() == false) { ViewMinInput.Set(NewMin); } if (ViewMaxInput.IsBound() == false) { ViewMaxInput.Set(NewMax); } } } void SCurveEditor::SetOutputMinMax(float NewMin, float NewMax) { if (SetOutputViewRangeHandler.IsBound()) { SetOutputViewRangeHandler.Execute(NewMin, NewMax); } else { //if no delegate and view min output isn't using a delegate just set value directly if (ViewMinOutput.IsBound() == false) { ViewMinOutput.Set(NewMin); } if (ViewMaxOutput.IsBound() == false) { ViewMaxOutput.Set(NewMax); } } } void SCurveEditor::EmptyAllCurveViewModels() { CurveViewModels.Empty(); } void SCurveEditor::ClearSelectedCurveViewModels() { for(auto CurveViewModel : CurveViewModels) { CurveViewModel->bIsSelected = false; } } void SCurveEditor::SetSelectedCurveViewModel(FRealCurve* CurveToSelect) { TSharedPtr ViewModel = GetViewModelForCurve(CurveToSelect); if (ViewModel.IsValid()) { ViewModel.Get()->bIsSelected = true; } } bool SCurveEditor::AnyCurveViewModelsSelected() const { for (auto CurveViewModel : CurveViewModels) { if (CurveViewModel->bIsSelected) { return true; } } return false; } TSharedPtr SCurveEditor::HitTestCurves( const FGeometry& InMyGeometry, const FPointerEvent& InMouseEvent ) { if( AreCurvesVisible() ) { FTrackScaleInfo ScaleInfo(ViewMinInput.Get(), ViewMaxInput.Get(), ViewMinOutput.Get(), ViewMaxOutput.Get(), InMyGeometry.GetLocalSize()); const FVector2D HitPosition = InMyGeometry.AbsoluteToLocal( InMouseEvent.GetScreenSpacePosition() ); TArray CurvesHit; for(auto CurveViewModel : CurveViewModels) { FRealCurve* Curve = CurveViewModel->CurveInfo.CurveToEdit; if (IsValidCurve(Curve)) { float Time = ScaleInfo.LocalXToInput(HitPosition.X); float KeyScreenY = ScaleInfo.OutputToLocalY(Curve->Eval(Time)); if( HitPosition.Y > (KeyScreenY - (0.5f * CONST_CurveSize.Y)) && HitPosition.Y < (KeyScreenY + (0.5f * CONST_CurveSize.Y))) { return CurveViewModel; } } } } return TSharedPtr(); } bool SCurveEditor::IsCurveSelectable(TSharedPtr CurveViewModel) const { bool bAnyCurveViewModelsSelected = AnyCurveViewModelsSelected(); bool bDisabled = bAnyCurveViewModelsSelected && !CurveViewModel->bIsSelected; return !CurveViewModel->bIsLocked && CurveViewModel->bIsVisible && !bDisabled; } SCurveEditor::FSelectedTangent SCurveEditor::HitTestCubicTangents( const FGeometry& InMyGeometry, const FVector2D& HitScreenPosition ) { FSelectedTangent Tangent; if( AreCurvesVisible() && CurveOwner && CurveOwner->HasRichCurves() ) { FTrackScaleInfo ScaleInfo(ViewMinInput.Get(), ViewMaxInput.Get(), ViewMinOutput.Get(), ViewMaxOutput.Get(), InMyGeometry.GetLocalSize()); const FVector2D HitPosition = InMyGeometry.AbsoluteToLocal( HitScreenPosition); for (auto CurveViewModel : CurveViewModels) { if (IsCurveSelectable(CurveViewModel)) { FRichCurve* Curve = (FRichCurve*)CurveViewModel->CurveInfo.CurveToEdit; if (Curve != NULL) { for (auto It(Curve->GetKeyHandleIterator()); It; ++It) { FKeyHandle KeyHandle = *It; FSelectedCurveKey SelectedCurveKey(Curve, KeyHandle); if(SelectedCurveKey.IsValid()) { bool bIsTangentSelected = false; bool bIsArrivalSelected = false; bool bIsLeaveSelected = false; bool bIsTangentVisible = IsTangentVisible(Curve, KeyHandle, bIsTangentSelected, bIsArrivalSelected, bIsLeaveSelected); if (bIsTangentVisible) { float Time = ScaleInfo.LocalXToInput(HitPosition.X); float KeyScreenY = ScaleInfo.OutputToLocalY(Curve->Eval(Time)); FVector2D Arrive, Leave; GetTangentPoints(ScaleInfo, SelectedCurveKey, Arrive, Leave); if( HitPosition.Y > (Arrive.Y - (0.5f * CONST_CurveSize.Y)) && HitPosition.Y < (Arrive.Y + (0.5f * CONST_CurveSize.Y)) && HitPosition.X > (Arrive.X - (0.5f * CONST_TangentSize.X)) && HitPosition.X < (Arrive.X + (0.5f * CONST_TangentSize.X))) { Tangent.Key = SelectedCurveKey; Tangent.bIsArrival = true; break; } if( HitPosition.Y > (Leave.Y - (0.5f * CONST_CurveSize.Y)) && HitPosition.Y < (Leave.Y + (0.5f * CONST_CurveSize.Y)) && HitPosition.X > (Leave.X - (0.5f * CONST_TangentSize.X)) && HitPosition.X < (Leave.X + (0.5f * CONST_TangentSize.X))) { Tangent.Key = SelectedCurveKey; Tangent.bIsArrival = false; break; } } } } } } } } return Tangent; } void SCurveEditor::OnSelectInterpolationMode(ERichCurveInterpMode InterpMode, ERichCurveTangentMode TangentMode) { const bool bHasRichCurves = CurveOwner->HasRichCurves(); if((SelectedKeys.Num() > 0 || SelectedTangents.Num() > 0) && (InterpMode != RCIM_Cubic || bHasRichCurves)) { const FScopedTransaction Transaction(LOCTEXT("CurveEditor_SetInterpolationMode", "Select Interpolation Mode")); CurveOwner->ModifyOwner(); if (bHasRichCurves) { for (auto It = SelectedKeys.CreateIterator(); It; ++It) { FSelectedCurveKey& Key = *It; FRichCurve* RichCurve = (FRichCurve*)Key.Curve; check(IsValidCurve(RichCurve)); RichCurve->SetKeyInterpMode(Key.KeyHandle, InterpMode); RichCurve->SetKeyTangentMode(Key.KeyHandle, TangentMode); } for (auto It = SelectedTangents.CreateIterator(); It; ++It) { FSelectedTangent& Tangent = *It; FRichCurve* RichCurve = (FRichCurve*)Tangent.Key.Curve; check(IsValidCurve(RichCurve)); RichCurve->SetKeyInterpMode(Tangent.Key.KeyHandle, InterpMode); RichCurve->SetKeyTangentMode(Tangent.Key.KeyHandle, TangentMode); } } else { for (auto It = SelectedKeys.CreateIterator(); It; ++It) { FSelectedCurveKey& Key = *It; FSimpleCurve* SimpleCurve = (FSimpleCurve*)Key.Curve; check(IsValidCurve(SimpleCurve)); SimpleCurve->SetKeyInterpMode(InterpMode); } } TArray ChangedCurveEditInfos; CurveOwner->OnCurveChanged(ChangedCurveEditInfos); } } bool SCurveEditor::IsInterpolationModeSelected(ERichCurveInterpMode InterpMode, ERichCurveTangentMode TangentMode) { const bool bHasRichCurves = CurveOwner->HasRichCurves(); if (SelectedKeys.Num() > 0) { for (auto SelectedKey : SelectedKeys) { if (SelectedKey.Curve->GetKeyInterpMode(SelectedKey.KeyHandle) != InterpMode || (bHasRichCurves && ((FRichCurve*)SelectedKey.Curve)->GetKeyTangentMode(SelectedKey.KeyHandle) != TangentMode)) { return false; } } return true; } else if (SelectedTangents.Num() > 0) { for (auto SelectedTangent : SelectedTangents) { if (SelectedTangent.Key.Curve->GetKeyInterpMode(SelectedTangent.Key.KeyHandle) != InterpMode || ((FRichCurve*)SelectedTangent.Key.Curve)->GetKeyTangentMode(SelectedTangent.Key.KeyHandle) != TangentMode) { return false; } } return true; } else { return false; } } bool SCurveEditor::HasRichCurves() const { return CurveOwner->HasRichCurves(); } void SCurveEditor::OnFlattenOrStraightenTangents(bool bFlattenTangents) { if ((SelectedKeys.Num() > 0 || SelectedTangents.Num() > 0) && CurveOwner->HasRichCurves()) { const FScopedTransaction Transaction(LOCTEXT("CurveEditor_FlattenTangents", "Flatten Tangents")); CurveOwner->ModifyOwner(); auto OnFlattenOrStraightenTangents_Internal = [this, bFlattenTangents](FSelectedCurveKey& Key) { FRichCurve* RichCurve = (FRichCurve*)Key.Curve; check(IsValidCurve(RichCurve)); float LeaveTangent = RichCurve->GetKey(Key.KeyHandle).LeaveTangent; float ArriveTangent = RichCurve->GetKey(Key.KeyHandle).ArriveTangent; if (bFlattenTangents) { LeaveTangent = 0; ArriveTangent = 0; } else { LeaveTangent = (LeaveTangent + ArriveTangent) * 0.5f; ArriveTangent = LeaveTangent; } RichCurve->GetKey(Key.KeyHandle).LeaveTangent = LeaveTangent; RichCurve->GetKey(Key.KeyHandle).ArriveTangent = ArriveTangent; if (RichCurve->GetKey(Key.KeyHandle).InterpMode == RCIM_Cubic && RichCurve->GetKey(Key.KeyHandle).TangentMode == RCTM_Auto) { RichCurve->GetKey(Key.KeyHandle).TangentMode = RCTM_User; } }; for(auto It = SelectedKeys.CreateIterator();It;++It) { OnFlattenOrStraightenTangents_Internal(*It); } for(auto It = SelectedTangents.CreateIterator();It;++It) { OnFlattenOrStraightenTangents_Internal(It->Key); } TArray ChangedCurveEditInfos; CurveOwner->OnCurveChanged(ChangedCurveEditInfos); } } void SCurveEditor::OnBakeCurve() { float BakeSampleRate = InputSnap.IsSet() ? InputSnap.Get() : 0.05f; // Display dialog and let user enter sample rate. GenericTextEntryModeless( NSLOCTEXT("CurveEditor.Popups", "BakeSampleRate", "Sample Rate"), FText::AsNumber( BakeSampleRate ), FOnTextCommitted::CreateSP(this, &SCurveEditor::OnBakeCurveSampleRateCommitted) ); } void SCurveEditor::OnBakeCurveSampleRateCommitted(const FText& InText, ETextCommit::Type CommitInfo) { CloseEntryPopupMenu(); if (CommitInfo == ETextCommit::OnEnter) { double NewBakeSampleRate = FCString::Atod(*InText.ToString()); const bool bIsNumber = InText.IsNumeric(); if (!bIsNumber) { return; } if (NewBakeSampleRate <= 0.0) { UE_LOG(LogCurveEditor, Error, TEXT("Invalid Bake Sample Rate")); return; } const float BakeSampleRate = (float)NewBakeSampleRate; const FScopedTransaction Transaction(LOCTEXT("CurveEditor_BakeCurve", "Bake Curve")); CurveOwner->ModifyOwner(); bool bAnyCurveViewModelsSelected = AnyCurveViewModelsSelected(); TArray ChangedCurveEditInfos; // If keys are selected, bake between them TMap > CurveRangeMap; for (auto SelectedKey : SelectedKeys) { float SelectedTime = SelectedKey.Curve->GetKeyTime(SelectedKey.KeyHandle); if (CurveRangeMap.Find(SelectedKey.Curve) != nullptr) { CurveRangeMap[SelectedKey.Curve].Include(SelectedTime); } else { CurveRangeMap.Add(SelectedKey.Curve, TInterval(SelectedTime, SelectedTime)); } } if (CurveRangeMap.Num()) { for (auto CurveToBake : CurveRangeMap) { if (CurveToBake.Value.Min != CurveToBake.Value.Max) { CurveToBake.Key->BakeCurve(BakeSampleRate, CurveToBake.Value.Min, CurveToBake.Value.Max); ChangedCurveEditInfos.Add(GetViewModelForCurve(CurveToBake.Key)->CurveInfo); } else { UE_LOG(LogCurveEditor, Warning, TEXT("Unable to bake single-point curve. Check if you don't have a single key selected before Baking.")); } } } else { for (auto CurveViewModel : CurveViewModels) { if (!bAnyCurveViewModelsSelected || CurveViewModel->bIsSelected) { CurveViewModel->CurveInfo.CurveToEdit->BakeCurve(BakeSampleRate); ChangedCurveEditInfos.Add(CurveViewModel->CurveInfo); } } } if (ChangedCurveEditInfos.Num()) { CurveOwner->OnCurveChanged(ChangedCurveEditInfos); } } } void SCurveEditor::OnReduceCurve() { // Display dialog and let user enter tolerance. GenericTextEntryModeless( NSLOCTEXT("CurveEditor.Popups", "ReduceCurveTolerance", "Tolerance"), FText::AsNumber( ReduceTolerance ), FOnTextCommitted::CreateSP(this, &SCurveEditor::OnReduceCurveToleranceCommitted) ); } void SCurveEditor::OnReduceCurveToleranceCommitted(const FText& InText, ETextCommit::Type CommitInfo) { CloseEntryPopupMenu(); if (CommitInfo == ETextCommit::OnEnter) { double NewTolerance = FCString::Atod(*InText.ToString()); const bool bIsNumber = InText.IsNumeric(); if(!bIsNumber) return; ReduceTolerance = (float)NewTolerance; const FScopedTransaction Transaction(LOCTEXT("CurveEditor_ReduceCurve", "Reduce Curve")); CurveOwner->ModifyOwner(); bool bAnyCurveViewModelsSelected = AnyCurveViewModelsSelected(); TArray ChangedCurveEditInfos; // If keys are selected, bake between them TMap > CurveRangeMap; for (auto SelectedKey : SelectedKeys) { float SelectedTime = SelectedKey.Curve->GetKeyTime(SelectedKey.KeyHandle); if (CurveRangeMap.Find(SelectedKey.Curve) != nullptr) { CurveRangeMap[SelectedKey.Curve].Include(SelectedTime); } else { CurveRangeMap.Add(SelectedKey.Curve, TInterval(SelectedTime, SelectedTime)); } } if (CurveRangeMap.Num()) { for (auto CurveToBake : CurveRangeMap) { CurveToBake.Key->RemoveRedundantKeys(ReduceTolerance, CurveToBake.Value.Min, CurveToBake.Value.Max); ChangedCurveEditInfos.Add(GetViewModelForCurve(CurveToBake.Key)->CurveInfo); } } else { for (auto CurveViewModel : CurveViewModels) { if (!bAnyCurveViewModelsSelected || CurveViewModel->bIsSelected) { CurveViewModel->CurveInfo.CurveToEdit->RemoveRedundantKeys(ReduceTolerance); ChangedCurveEditInfos.Add(CurveViewModel->CurveInfo); } } } if (ChangedCurveEditInfos.Num()) { CurveOwner->OnCurveChanged(ChangedCurveEditInfos); } } } void SCurveEditor::OnSelectPreInfinityExtrap(ERichCurveExtrapolation Extrapolation) { const FScopedTransaction Transaction(LOCTEXT("CurveEditor_SetPreInfinityExtrapolation", "Set Pre-Infinity Extrapolation")); CurveOwner->ModifyOwner(); bool bAnyCurveViewModelsSelected = AnyCurveViewModelsSelected(); TArray ChangedCurveEditInfos; for (auto CurveViewModel : CurveViewModels) { if (!bAnyCurveViewModelsSelected || CurveViewModel->bIsSelected) { if (CurveViewModel->CurveInfo.CurveToEdit->PreInfinityExtrap != Extrapolation) { CurveViewModel->CurveInfo.CurveToEdit->PreInfinityExtrap = Extrapolation; ChangedCurveEditInfos.Add(CurveViewModel->CurveInfo); } } } if (ChangedCurveEditInfos.Num()) { CurveOwner->OnCurveChanged(ChangedCurveEditInfos); } } bool SCurveEditor::IsPreInfinityExtrapSelected(ERichCurveExtrapolation Extrapolation) { bool bAnyCurveViewModelsSelected = AnyCurveViewModelsSelected(); for (auto CurveViewModel : CurveViewModels) { // If there are any curves selected, the setting must match all of the selected curves if (bAnyCurveViewModelsSelected) { if (CurveViewModel->bIsSelected) { if (CurveViewModel->CurveInfo.CurveToEdit->PreInfinityExtrap != Extrapolation) { return false; } } } else { if (CurveViewModel->CurveInfo.CurveToEdit->PreInfinityExtrap != Extrapolation) { return false; } } } return CurveViewModels.Num() > 0; } void SCurveEditor::OnSelectPostInfinityExtrap(ERichCurveExtrapolation Extrapolation) { const FScopedTransaction Transaction(LOCTEXT("CurveEditor_SetPostInfinityExtrapolation", "Set Post-Infinity Extrapolation")); CurveOwner->ModifyOwner(); bool bAnyCurveViewModelsSelected = AnyCurveViewModelsSelected(); TArray ChangedCurveEditInfos; for (auto CurveViewModel : CurveViewModels) { if (!bAnyCurveViewModelsSelected || CurveViewModel->bIsSelected) { if (CurveViewModel->CurveInfo.CurveToEdit->PostInfinityExtrap != Extrapolation) { CurveViewModel->CurveInfo.CurveToEdit->PostInfinityExtrap = Extrapolation; ChangedCurveEditInfos.Add(CurveViewModel->CurveInfo); } } } if (ChangedCurveEditInfos.Num()) { CurveOwner->OnCurveChanged(ChangedCurveEditInfos); } } bool SCurveEditor::IsPostInfinityExtrapSelected(ERichCurveExtrapolation Extrapolation) { bool bAnyCurveViewModelsSelected = AnyCurveViewModelsSelected(); for (auto CurveViewModel : CurveViewModels) { // If there are any curves selected, the setting must match all of the selected curves if (bAnyCurveViewModelsSelected) { if (CurveViewModel->bIsSelected) { if (CurveViewModel->CurveInfo.CurveToEdit->PostInfinityExtrap != Extrapolation) { return false; } } } else { if (CurveViewModel->CurveInfo.CurveToEdit->PostInfinityExtrap != Extrapolation) { return false; } } } return CurveViewModels.Num() > 0; } void SCurveEditor::MoveTangents(FTrackScaleInfo& ScaleInfo, FVector2D Delta) { TArray ChangedCurveEditInfos; CurveOwner->ModifyOwnerChange(); for (const FSelectedTangent& SelectedTangent : SelectedTangents) { FRichCurveKey& RichKey = ((FRichCurve*)SelectedTangent.Key.Curve)->GetKey(SelectedTangent.Key.KeyHandle); const FSelectedCurveKey &Key = SelectedTangent.Key; float PreDragArriveTangent = PreDragTangents[SelectedTangent.Key.KeyHandle][0]; float PreDragLeaveTangent = PreDragTangents[SelectedTangent.Key.KeyHandle][1]; // Get tangent points in screen space FVector2D ArriveTangentDir = CalcTangentDir( PreDragArriveTangent ); FVector2D LeaveTangentDir = CalcTangentDir( PreDragLeaveTangent ); FVector2D KeyPosition( Key.Curve->GetKeyTime(Key.KeyHandle),Key.Curve->GetKeyValue(Key.KeyHandle) ); ArriveTangentDir.Y *= -1.0f; LeaveTangentDir.Y *= -1.0f; FVector2D ArrivePosition = -ArriveTangentDir + KeyPosition; FVector2D LeavePosition = LeaveTangentDir + KeyPosition; FVector2D Arrive = FVector2D(ScaleInfo.InputToLocalX(ArrivePosition.X), ScaleInfo.OutputToLocalY(ArrivePosition.Y)); FVector2D Leave = FVector2D(ScaleInfo.InputToLocalX(LeavePosition.X), ScaleInfo.OutputToLocalY(LeavePosition.Y)); FVector2D KeyScreenPosition = FVector2D(ScaleInfo.InputToLocalX(KeyPosition.X), ScaleInfo.OutputToLocalY(KeyPosition.Y)); FVector2D ToArrive = Arrive - KeyScreenPosition; ToArrive.Normalize(); Arrive = KeyScreenPosition + ToArrive*CONST_KeyTangentOffset; FVector2D ToLeave = Leave - KeyScreenPosition; ToLeave.Normalize(); Leave = KeyScreenPosition + ToLeave*CONST_KeyTangentOffset; // New arrive and leave directions in screen space if (SelectedTangent.bIsArrival) { Arrive += Delta; Leave -= Delta; } else { Arrive -= Delta; Leave += Delta; } // Convert back to input/output space FVector2D NewArriveDir(ScaleInfo.LocalXToInput(Arrive.X), ScaleInfo.LocalYToOutput(Arrive.Y)); FVector2D NewLeaveDir(ScaleInfo.LocalXToInput(Leave.X), ScaleInfo.LocalYToOutput(Leave.Y)); // Compute tangents float NewArriveTangent = CalcTangent(-1.f*(NewArriveDir - KeyPosition)); float NewLeaveTangent = CalcTangent(NewLeaveDir - KeyPosition); if(RichKey.TangentMode != RCTM_Break) { RichKey.ArriveTangent = NewArriveTangent; RichKey.LeaveTangent = NewLeaveTangent; RichKey.TangentMode = RCTM_User; } else { if(SelectedTangent.bIsArrival) { RichKey.ArriveTangent = NewArriveTangent; } else { RichKey.LeaveTangent = NewLeaveTangent; } } RichKey.InterpMode = RCIM_Cubic; ChangedCurveEditInfos.Add(GetViewModelForCurve(SelectedTangent.Key.Curve)->CurveInfo); } if (ChangedCurveEditInfos.Num()) { CurveOwner->OnCurveChanged(ChangedCurveEditInfos); } } void SCurveEditor::GetTangentPoints( FTrackScaleInfo &ScaleInfo, const FSelectedCurveKey &Key, FVector2D& Arrive, FVector2D& Leave ) const { FVector2D ArriveTangentDir = CalcTangentDir( ((FRichCurve*)Key.Curve)->GetKey(Key.KeyHandle).ArriveTangent); FVector2D LeaveTangentDir = CalcTangentDir( ((FRichCurve*)Key.Curve)->GetKey(Key.KeyHandle).LeaveTangent); FVector2D KeyPosition( Key.Curve->GetKeyTime(Key.KeyHandle),Key.Curve->GetKeyValue(Key.KeyHandle) ); ArriveTangentDir.Y *= -1.0f; LeaveTangentDir.Y *= -1.0f; FVector2D ArrivePosition = -ArriveTangentDir + KeyPosition; FVector2D LeavePosition = LeaveTangentDir + KeyPosition; Arrive = FVector2D(ScaleInfo.InputToLocalX(ArrivePosition.X), ScaleInfo.OutputToLocalY(ArrivePosition.Y)); Leave = FVector2D(ScaleInfo.InputToLocalX(LeavePosition.X), ScaleInfo.OutputToLocalY(LeavePosition.Y)); FVector2D KeyScreenPosition = FVector2D(ScaleInfo.InputToLocalX(KeyPosition.X), ScaleInfo.OutputToLocalY(KeyPosition.Y)); FVector2D ToArrive = Arrive - KeyScreenPosition; ToArrive.Normalize(); Arrive = KeyScreenPosition + ToArrive*CONST_KeyTangentOffset; FVector2D ToLeave = Leave - KeyScreenPosition; ToLeave.Normalize(); Leave = KeyScreenPosition + ToLeave*CONST_KeyTangentOffset; } TArray SCurveEditor::GetEditableKeysWithinMarquee(const FGeometry& InMyGeometry, FVector2D MarqueeTopLeft, FVector2D MarqueeBottomRight) const { TArray KeysWithinMarquee; if (AreCurvesVisible()) { FTrackScaleInfo ScaleInfo(ViewMinInput.Get(), ViewMaxInput.Get(), ViewMinOutput.Get(), ViewMaxOutput.Get(), InMyGeometry.GetLocalSize()); for (auto CurveViewModel : CurveViewModels) { if (IsCurveSelectable(CurveViewModel)) { FRealCurve* Curve = CurveViewModel->CurveInfo.CurveToEdit; if (Curve != NULL) { for (auto It(Curve->GetKeyHandleIterator()); It; ++It) { float KeyScreenX = ScaleInfo.InputToLocalX(Curve->GetKeyTime(*It)); float KeyScreenY = ScaleInfo.OutputToLocalY(Curve->GetKeyValue(*It)); if (KeyScreenX >= (MarqueeTopLeft.X - (0.5f * CONST_KeySize.X)) && KeyScreenX <= (MarqueeBottomRight.X + (0.5f * CONST_KeySize.X)) && KeyScreenY >= (MarqueeTopLeft.Y - (0.5f * CONST_KeySize.Y)) && KeyScreenY <= (MarqueeBottomRight.Y + (0.5f * CONST_KeySize.Y))) { KeysWithinMarquee.Add(FSelectedCurveKey(Curve, *It)); } } } } } } return KeysWithinMarquee; } TArray SCurveEditor::GetEditableTangentsWithinMarquee(const FGeometry& InMyGeometry, FVector2D MarqueeTopLeft, FVector2D MarqueeBottomRight) const { FBox MarqueeBox; MarqueeBox.Min = FVector(MarqueeTopLeft.X, MarqueeTopLeft.Y, 0); MarqueeBox.Max = FVector(MarqueeBottomRight.X, MarqueeBottomRight.Y, 0); TArray TangentsWithinMarquee; if (AreCurvesVisible() && CurveOwner && CurveOwner->HasRichCurves()) { FTrackScaleInfo ScaleInfo(ViewMinInput.Get(), ViewMaxInput.Get(), ViewMinOutput.Get(), ViewMaxOutput.Get(), InMyGeometry.GetLocalSize()); for (auto CurveViewModel : CurveViewModels) { if (IsCurveSelectable(CurveViewModel)) { FRichCurve* Curve = (FRichCurve*)CurveViewModel->CurveInfo.CurveToEdit; if (Curve != NULL) { for (auto It(Curve->GetKeyHandleIterator()); It; ++It) { FKeyHandle KeyHandle = *It; FSelectedCurveKey SelectedCurveKey(Curve, KeyHandle); if(SelectedCurveKey.IsValid()) { bool bIsTangentSelected = false; bool bIsArrivalSelected = false; bool bIsLeaveSelected = false; bool bIsTangentVisible = IsTangentVisible(Curve, KeyHandle, bIsTangentSelected, bIsArrivalSelected, bIsLeaveSelected); if (bIsTangentVisible) { FVector2D Arrive, Leave; GetTangentPoints(ScaleInfo, SelectedCurveKey, Arrive, Leave); bool bArriveInside = MarqueeBox.IsInsideOrOn(FVector(Arrive.X, Arrive.Y, 0)); bool bLeaveInside = MarqueeBox.IsInsideOrOn(FVector(Leave.X, Leave.Y, 0)); if (bArriveInside || bLeaveInside) { FSelectedTangent SelectedTangent(SelectedCurveKey); SelectedTangent.bIsArrival = bArriveInside; TangentsWithinMarquee.Add(SelectedTangent); } } } } } } } } return TangentsWithinMarquee; } void SCurveEditor::BeginDragTransaction() { TransactionIndex = GEditor->BeginTransaction( LOCTEXT("CurveEditor_Drag", "Mouse Drag") ); CurveOwner->ModifyOwner(); } void SCurveEditor::EndDragTransaction() { if ( TransactionIndex >= 0 ) { TArray ChangedCurveEditInfos; if (DragState == EDragState::DragKey || DragState == EDragState::FreeDrag) { for (auto SelectedKey : SelectedKeys) { ChangedCurveEditInfos.Add(GetViewModelForCurve(SelectedKey.Curve)->CurveInfo); } } else if (DragState == EDragState::DragTangent) { for (auto SelectedTangent : SelectedTangents) { ChangedCurveEditInfos.Add(GetViewModelForCurve(SelectedTangent.Key.Curve)->CurveInfo); } } if (ChangedCurveEditInfos.Num()) { CurveOwner->OnCurveChanged(ChangedCurveEditInfos); } GEditor->EndTransaction(); TransactionIndex = -1; } } bool SCurveEditor::FSelectedTangent::IsValid() const { return Key.IsValid(); } void SCurveEditor::UndoAction() { GEditor->UndoTransaction(); } void SCurveEditor::RedoAction() { GEditor->RedoTransaction(); } void SCurveEditor::RefreshDetailsView() { check(!bIsPendingRebuilt); bIsPendingRebuilt = true; //To clean up dangling references EmptyAllSelection(); EmptyAllCurveViewModels(); // Refresh will still happen in the current frame, but it's important to make it asynchronous because its possible the user sets value through property handle multiple times inside an atomic action // Making it synchronous will invalidate property nodes for all the subsequent property handle set value operation. RegisterActiveTimer(0.f, FWidgetActiveTimerDelegate::CreateSPLambda( this, [this](double, float) { if(const TSharedPtr PropertyUtilities = PropertyUtilitiesWeak.Pin()) { //Rebuild the widget so it will reference to the new correct address of Curves, and show things correctly PropertyUtilities->ForceRefresh(); } return EActiveTimerReturnType::Stop; } )); ValidateSelection(); } void SCurveEditor::OnObjectPropertyChanged(UObject* Object, FPropertyChangedEvent& PropertyChangedEvent) { if (CurveOwner && CurveOwner->GetOwners().Contains(Object)) { if (!bIsPendingRebuilt) { if (GIsTransacting) { RefreshDetailsView(); } } } } void SCurveEditor::OnRootPropertyChanged(const FPropertyChangedEvent& PropertyChangedEvent) { if (!bIsPendingRebuilt) { // CurveEditor will hold dangling references to CurveData and access them in some cases : // - Curve Data is inline allocated, or wrapped by InstancedStruct, in a container. And we do Array Remove/Clear op, or Add that causes resize // - Curve Data is wrapped by InstancedStruct in a Container, and we do undo/redo(will trigger emptying and refilling the container, causing address changed) // - Curve Data is wrapped by InstancedStruct and InstancedStruct is being reset/replaced, causing actual memory address changed // todo: should only rebuild when it's the InstanceStruct with ValueSet. But currently there is no way to detect if there is an InstanceStruct in the PropertyNode Hierarchy. if (PropertyChangedEvent.ChangeType & (EPropertyChangeType::ArrayAdd | EPropertyChangeType::ArrayRemove | EPropertyChangeType::ArrayClear | EPropertyChangeType::ValueSet | EPropertyChangeType::ResetToDefault)) { RefreshDetailsView(); } } ValidateSelection(); } void SCurveEditor::HandlePackageReloaded(const EPackageReloadPhase InPackageReloadPhase, FPackageReloadedEvent* InPackageReloadedEvent) { if (InPackageReloadPhase == EPackageReloadPhase::OnPackageFixup && CurveOwner) { // Our curve owner may be an object that has been reloaded, so we need to check that and update the curve editor appropriately // We have to do this via the interface as the object addresses stored in the remap table will be offset from the interface pointer due to multiple inheritance FCurveOwnerInterface* NewCurveOwner = nullptr; if (CurveOwner->RepointCurveOwner(*InPackageReloadedEvent, NewCurveOwner)) { SetCurveOwner(NewCurveOwner, bCanEditTrack); } } } void SCurveEditor::PostUndo(bool bSuccess) { ValidateSelection(); } bool SCurveEditor::IsLinearColorCurve() const { return CurveOwner && CurveOwner->IsLinearColorCurve(); } FVector2D SCurveEditor::SnapLocation(FVector2D InLocation) { if (bInputSnappingEnabled.Get()) { const float InputSnapNow = InputSnap.Get(); InLocation.X = InputSnapNow != 0 ? FMath::RoundToInt(InLocation.X / InputSnapNow) * InputSnapNow : InLocation.X; } if (bOutputSnappingEnabled.Get()) { const float OutputSnapNow = OutputSnap.Get(); InLocation.Y = OutputSnapNow != 0 ? FMath::RoundToInt(InLocation.Y / OutputSnapNow) * OutputSnapNow : InLocation.Y; } return InLocation; } TSharedPtr SCurveEditor::GetViewModelForCurve(FRealCurve* InCurve) { for (auto CurveViewModel : CurveViewModels) { if (InCurve == CurveViewModel->CurveInfo.CurveToEdit) { return CurveViewModel; } } return TSharedPtr(); } void SCurveEditor::GenericTextEntryModeless(const FText& DialogText, const FText& DefaultText, FOnTextCommitted OnTextComitted) { TSharedRef TextEntryPopup = SNew(STextEntryPopup) .Label(DialogText) .DefaultText(DefaultText) .OnTextCommitted(OnTextComitted) .ClearKeyboardFocusOnCommit(false) .SelectAllTextWhenFocused(true) .MaxWidth(1024.0f); EntryPopupMenu = FSlateApplication::Get().PushMenu( SharedThis(this), FWidgetPath(), TextEntryPopup, FSlateApplication::Get().GetCursorPos(), FPopupTransitionEffect(FPopupTransitionEffect::TypeInPopup) ); } void SCurveEditor::CloseEntryPopupMenu() { if (EntryPopupMenu.IsValid()) { EntryPopupMenu.Pin()->Dismiss(); } } int32 SCurveEditor::TimeToFrame(float InTime) const { const float FrameRate = InputSnap.IsSet() ? 1.0f / InputSnap.Get() : 1.f; float Frame = InTime * FrameRate; return FMath::RoundToInt(Frame); } float SCurveEditor::FrameToTime(int32 InFrame) const { const float FrameRate = InputSnap.IsSet() ? 1.0f / InputSnap.Get() : 1.f; return InFrame / FrameRate; } #undef LOCTEXT_NAMESPACE